• 🏆 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!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[Snippet] CustomEvent

Level 8
Joined
Jan 23, 2015
Messages
121
JASS:
//! zinc
library CustomEvent /*
*/ //! novjass
* by Trokkin, Version 1.5 *"    http://www.hiveworkshop.com/members/trokkin.243282/        "
****************************************************************************************************
* This snippet is made for purpose of creating custom events and listening to them the most
* efficient and easy way.
*
* Credits to
*        AGD for his great advises, which shaped the snippet itself by my hands.
*        Bribe for his Table.
*
* To implement the snippet, you only have to copy-paste this code to your map.
****************************************************************************************************
*    API:
*        struct Event {
*
*                $ Set this code to listener of event
*            method subscribe(code c)
*                $ Unset this code to listener of event
*            method unsubscribe(code c)
*                $ Clears the entire event from code listeners
*            method clear()
*
*                $ Two methods working around one boolean that can disable any .fire() on that Event.
*            method enable(boolean flag)
*            method enabled() -> boolean
*                $ You can also use non-private field directly instead of methods.
*            boolean isEnabled;
*
*                $ Set this trigger to listener of event; remember that codes is ran earlier than triggers.
*            method linkTrigger(trigger t)
*                // I personally dont recommend using dynamic events with triggers. And triggers overall.
*
*                $ Runs event so each registered code will be executed.
*                $ Has an ability to attach some data with single run.
*                $ (one integer should be enough to attach any other data through arrays/structs)
*            method fire(integer data)
*            $ Event has two additional fields to use in executed code:
*                    $ represents current fired event
*                public static operator thistype fired() -> Event
*                    $ represents attached data of event
*                public integer data
*            /* So basicly you'd want to use */Event.fired.data/* to asquire attached data */
*
*                $ no commentaries needed.
*            static method create() -> Event
*            method destroy()
*                // If there are triggers subscribed on it previously, then a shadow of it will remain,
*                // preventing from using the same index twice and so linked triggers.
*
*        }
*
****************************************************************************************************
//! endnovjass
/************************************************************************************
*
*   */ requires /*
*   */ Alloc,   /* by Sevion
*   http://www.hiveworkshop.com/threads/snippet-alloc.192348/
*   */ Table    /* by Bribe
*   http://www.hiveworkshop.com/forums/jass-resources-412/snippet-new-table-188084/
*
*    Since it's Zinc, ofc you need JNGP.
*
************************************************************************************/
{
    public struct Event[] {
        module Alloc;
        public integer data;
        private {
            static real variable = 0.0;
            boolean hasLinkedTriggers;
            trigger trigger;
            Table hash;
        }
      
        boolean isEnabled;
        method enable(boolean flag) { this.
        isEnabled = flag; }
        method enabled() -> boolean { return this.isEnabled; }
    
        method register(code c) {
            filterfunc filter = Filter(c);
            integer i = GetHandleId(filter);
            debug { if(hash.triggercondition.has(i)) BJDebugMsg("[CustomEvent] ERROR: tried to add one function twice on event "+I2S(this)+".");}
            if(!hash.triggercondition.has(i))
                hash.triggercondition[i] = TriggerAddCondition(this.trigger, filter);
            filter = null;
        }
        method unregister(code c) {
            integer i = GetHandleId(Filter(c));
            debug { if(!hash.triggercondition.has(i)) BJDebugMsg("[CustomEvent] ERROR: tried to remove unadded funciton on event "+I2S(this)+"."); }
            if(hash.triggercondition.has(i)) {
                TriggerRemoveCondition(this.trigger,hash.triggercondition[i]);
                hash.triggercondition.remove(i);
            }
        }
        method linkTrigger(trigger t) {
            this.hasLinkedTriggers = true;
            TriggerRegisterVariableEvent(t, "s__Event_variable", EQUAL, this);
        }
        method clear() { TriggerClearConditions(this.trigger); this.hash.flush(); }
      
        static method operator fired() -> thistype { return R2I(thistype.variable); }
        method fire(integer data) {
            real pTrigger = thistype.variable;
            integer pData = this.data;
            if(!this.isEnabled) return;
            thistype.variable = this + 0.5;    // the +0.5 sets registered codes in priority to linked triggers
            this.data = data;                // while keeping fired() returns always rounded down exact value
          
            TriggerEvaluate(this.trigger);    // fire subscribers
            thistype.variable = this;        // fire listeners
          
            thistype.variable = pTrigger;
            this.data = pData;
        }
      
        static method create() -> thistype {
            thistype this = thistype.allocate();
            this.data = 0;
            this.hash = Table.create();
            this.hasLinkedTriggers = false;
            this.isEnabled = true;
            if(this.trigger == null) this.trigger = CreateTrigger();
            return this;
        }
      
        method destroy() {
            this.clear();
            this.hash.destroy();
            this.isEnabled = false;
            if(!this.hasLinkedTriggers) this.deallocate();
        }
    }
}
//! endzinc
And a simple demo for this snippet showing pretty much all its functionality:
JASS:
scope MyScope initializer onInit
    globals
        Event Timer_Event
        Event Kill_Event
        trigger first_blood
        trigger onKillT
        integer time
        Table UDex
    endglobals
 
    private function annoy takes nothing returns nothing
        if(Event.fired.data < 120) then
            call BJDebugMsg("It's " + I2S(Event.fired.data) + " seconds of gametime.")
            call BJDebugMsg("Sorry, I'm so stupid that I can't count the time normally.")
        else
            call BJDebugMsg("Oh! It's only one minute passed!")
            call Timer_Event.enable(false)
            call BJDebugMsg("Now I can't count time any more.")
        endif
    endfunction
    private function FirstBlood takes nothing returns boolean
        call BJDebugMsg(GetUnitName(UDex.unit[Event.fired.data]) + " is sooo bad, he died first!")
        call DestroyTrigger(first_blood)
        return false
    endfunction
 
    private function onKill takes nothing returns boolean
        set UDex.unit[GetHandleId(GetTriggerUnit())] = GetTriggerUnit()
        call Kill_Event.fire(GetHandleId(GetTriggerUnit())) // Doesn't matter how bad is this rly
        return false
    endfunction
    private function onTime takes nothing returns nothing
        call Timer_Event.fire(time)
        set time = time + 1
    endfunction
 
    private function onInit takes nothing returns nothing
        // Init sample kill event workaround
        set Kill_Event = Event.create()
        set onKillT = CreateTrigger()
        call TriggerAddCondition(onKillT,Filter(function onKill))
        call TriggerRegisterAnyUnitEventBJ(onKillT, EVENT_PLAYER_UNIT_DEATH)
        set UDex = Table.create()
   
        // Init sample clock
        set Timer_Event = Event.create()
        set time = 0
        call TimerStart(CreateTimer(), 0.5, true, function onTime)
   
        // Init listeners
        set first_blood = CreateTrigger()
        call TriggerAddCondition(first_blood,Filter(function FirstBlood))
        call Kill_Event.linkTrigger(first_blood)
        call Timer_Event.register(function annoy)
    endfunction
endscope
 
Last edited:

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
This looks like a plain simple wrappers basically. I also wonder why you used textmacros over functions/methods. When used many times, this would easily make the map script size very much greater compared to just calling methods/functions. Making an Event struct for example, would be much more efficient than these macros.


I also spot some errors in the code:
JASS:
    static method on$NAME$Event takes code c, trigger t returns nothing
        set thistype.on$NAME$T = AddConditionToTrigger(thistype.on$NAME$T, c)
    endmethod
The trigger argument is never used


JASS:
    static method create$NAME$Trigger takes nothing returns trigger
        local trigger t = CreateTrigger()
        call TriggerRegisterVariableEvent(t, "", EQUAL, 1.0)
        set thistype.on$NAME$Real = 0.0
    endmethod
This should return a trigger


At its current state, the submission is too simple for approval and yet the implementation of the API for users on the other hand can better be made much simpler (Yes, by not using textmacros).
Consider adding more functionality to the resource such as the ability to remove registered codes, as well as a proper creation and destruction of a custom event, because currently, there's no way a user can have dynamic custom events.
 
Level 8
Joined
Jan 23, 2015
Messages
121
Fixed.

Ouch, forgot to fill in real string - I just don't know how to compile it for member of unknown struct.

I've just imagined a way doing it with structs, thanks for advise. Though it won't have ability to directly call function, which I prefer over all other ways of shouting an event, and textmacros is the only method to do it directly.

Also, I'm not pursuing smaller sizes of map, at least in such terms when you'll have create about 100 events to add considerable map size difference between structs, which will also provably show themselves slower, if called periodically which is, I guess, much usual case than 100 different events.

ability to remove registered code
Is there any way doing it without deleting trigger? Because otherwise there already is a way in what I've done: triggers created through create#Trigger() can be manipulated easily.
I don't think creating trigger array just for possibility of clearing one of codes subscribed is worth itself.

P.s. can we register variable event on real array member?
 
Last edited:

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
Ouch, forgot to fill in real string - I just don't know how to compile it for member of unknown struct.
struct variables are prefixed with 's__' plus the name of the struct and an underscore. In this case, it should be "s__$STRUCTNAME$_on$NAME$Real".

Though it won't have ability to directly call function, which I prefer over all other ways of shouting an event, and textmacros is the only method to do it directly.
I think it would make more sense to just call the function manually each time before of after you fire an event

Is there any way doing it without deleting trigger? Because otherwise there already is a way in what I've done: triggers created through create#Trigger() can be manipulated easily.
I don't think creating trigger array just for possibility of clearing one of codes subscribed is worth itself.
You can easily use TriggerRemoveCondition() instead, with this you can remove only the specific triggercondition you want to remove instead of destroying the trigger which will clear all the conditions (though this functionality should also be provided). There are ways to do this. One and the simpliest way for this is by having the users pass a triggercondition as a parameter and then call TriggerRemoveCondition(). Another way which is more convenient for the users I think is by having users pass a code argument instead of triggercondition and the function/method will search for the corresponding triggercondition to be removed.

P.s. can we register variable event on real array member?
Afaik, it's not possible
 
Level 8
Joined
Jan 23, 2015
Messages
121
You can easily use TriggerRemoveCondition() instead, with this you can remove only the specific triggercondition you want to remove instead of destroying the trigger which will clear all the conditions (though this functionality should also be provided). There are ways to do this. One and the simpliest way for this is by having the users pass a triggercondition as a parameter and then call TriggerRemoveCondition(). Another way which is more convenient for the users I think is by having users pass a code argument instead of triggercondition and the function/method will search for the corresponding triggercondition to be removed.
Thanks for it - I haven't seen this funciton before. Updated completely to struct method.

Now I'm almost done with rewriting this system to easy way of unsubscribing, but struck how to search the corresponding triggercondtion since code is not something I can save or get it's id or something... Is there any function that can return same saveable value for one code passed in several times?
 
Last edited:

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
It's starting to look better :). Anyway, checking if the code passed in the subscribe and unsubscribe method in not necessary because TriggerAddCondition() already returns null when the code passed is null (it won't add an empty triggercondition to the trigger) and also, maybe it would be better if you could take advantage of the real event, thistype fired -> real fired because users can easily typecast it to integer then do thistype(fired) to check what event is currently fired. It would give users the option to pass triggers instead of code and this can be handy especially if the user wants to use TriggerSleepAction() inside his code, they just wouldn't be able to unregister a trigger though.

Now I'm almost done with rewriting this system to easy way of unsubscribing, but struck how to search the corresponding triggercondtion since code is not something I can save or get it's id or something... Is there any function that can return same saveable value for one code passed in several times?
You can save the triggercondition in a hashtable with the id of the filterfunc from the code as the key, for example
JASS:
method add (code c) {
    filterfunc filter = Filter(c);
    SaveTriggerConditionHandle(hashtable, GetHandleId(filter), 0, TriggerAddCondition(this.trig, filter));
    filter = null; //though not necessary
}
Then you can easily retrieve this triggercondition later based on the code argument passed by the user.

Unfortunately, registering the same code again and again would create different triggercondition handles, so you may need to have a safety measure for this (atleast in DEBUG_MODE) and code registration to one per each kind of code, or alternatively if you want the users to register same code more than once, you can use same technique to what Looking_for_help used here.
 
Level 8
Joined
Jan 23, 2015
Messages
121
It's starting to look better :)
You spotted that I mentioned you in credits? :)


It would give users the option to pass triggers instead of code
Wait a moment... AAAH I got it. A bit complicated and it can be possible source of misunderstandings and mistakes to users, so I'm going to wrap it around...
Will update in several minutes.

based on the code argument passed by the user
So, you say that the Filter() func always return same handle on same code passed?
(Filter(function A)==Filter(function A) is always true?)
 

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
You spotted that I mentioned you in credits? :)
Yes, i see

So, you say that the
Filter()
func always return same handle on same code passed?
(
Filter(function A)==Filter(function A)
is always true?)
Yes, exactly. You can try doing this
JASS:
call BJDebugMsg(I2S(GetHandleId(Filter(function A))))
call BJDebugMsg(I2S(GetHandleId(Filter(function A))))
And you can see that it displays the same handle id.

This (!hash.has(i)) should be (!hash.triggercondition.has(i)), as well as this hash.remove(i); => hash.triggercondition.remove(i);. Or you can type handle instead of triggercondition.

The variable name should be "s__Event_trigger" instead of "s__CustomEvent__Event_trigger".

In the subscribe method, you called GetHandleId(filter) when you've already cached its value to i.

You also forgot to initialize Table hash in the create() method. Furthermore, I think it's more efficient to just initialize a TableArray at init instead of dynamically creating tables for each created Events.

I also suggest to make the 3 1-liner methods namely clear(), fired(), and listener() similar in format i.e.
JASS:
        method clear() {
            TriggerClearConditions(this.t);
        }
        method listener(trigger t) {
            TriggerRegisterVariableEvent(t, "s__CustomEvent__Event_trigger", EQUAL, this);
        }

        method operator fired() -> thistype {
            return R2I(this.trigger);
        }
// OR
        method clear() {TriggerClearConditions(this.t);}

        method listener(trigger t) {TriggerRegisterVariableEvent(t, "s__CustomEvent__Event_trigger", EQUAL, this);}

        method operator fired() -> thistype {return R2I(this.trigger);}
just for consistency in format. But it's not really necessary and it totally depends on you =).


PS:

I also encourage you to provide even just a simple sample usage/demo code.
 
Last edited:
Level 9
Joined
Jun 21, 2012
Messages
432
It's a lot of code for something so simple:

JASS:
library CodeArray/*
***************************************************************************************
*
*   ***********************************************************************************
*   */ uses /*
*       */ AllocModule /*
*       */ MemoryHacks /* hiveworkshop.com/threads/memory-hack.289508/
*       *  Pjass  * hiveworkshop.com/threads/pjass-updates.258738/
*   ***********************************************************************************
*
*   CodeArray (Previously HandlerCode)
*   ¯¯¯¯¯¯¯¯¯
*   v2.1.2.0
*   by Trigger.edge
*
*   Allow the use of array code.
*
*   struct Code extends array
*
*       - method run takes nothing returns nothing
*       - method operator code takes nothing returns code
*       - method operator code= takes code c returns Code
*
**************************************************************************************/
 
    struct Code extends array
        private integer i_function
        private static force bj_F=CreateForce()
        implement AllocModule

        method run takes nothing returns nothing
            call ForForce(bj_F,I2C(.i_function))
        endmethod

        method operator code takes nothing returns code
            return I2C(.i_function)
        endmethod
 
        method operator code= takes code c returns Code
            if(null!=c)then
                set this=alloc()
                set .i_function=C2I(c)
                return this
            else
                if(0!=.i_function)then
                    set .i_function=0
                    call .dealloc()
                endif
                return 0
            endif
        endmethod
 
        private static method onInit takes nothing returns nothing
            call ForceAddPlayer(bj_F,GetLocalPlayer())
        endmethod
    endstruct
endlibrary
 
Level 8
Joined
Jan 23, 2015
Messages
121
It's a lot of code for something so simple:

JASS:
library CodeArray/*
***************************************************************************************
*
*   ***********************************************************************************
*   */ uses /*
*       */ AllocModule /*
*       */ MemoryHacks /* hiveworkshop.com/threads/memory-hack.289508/
*       *  Pjass  * hiveworkshop.com/threads/pjass-updates.258738/
*   ***********************************************************************************
*
*   CodeArray (Previously HandlerCode)
*   ¯¯¯¯¯¯¯¯¯
*   v2.1.2.0
*   by Trigger.edge
*
*   Allow the use of array code.
*
*   struct Code extends array
*
*       - method run takes nothing returns nothing
*       - method operator code takes nothing returns code
*       - method operator code= takes code c returns Code
*
**************************************************************************************/
 
    struct Code extends array
        private integer i_function
        private static force bj_F=CreateForce()
        implement AllocModule

        method run takes nothing returns nothing
            call ForForce(bj_F,I2C(.i_function))
        endmethod

        method operator code takes nothing returns code
            return I2C(.i_function)
        endmethod
 
        method operator code= takes code c returns Code
            if(null!=c)then
                set this=alloc()
                set .i_function=C2I(c)
                return this
            else
                if(0!=.i_function)then
                    set .i_function=0
                    call .dealloc()
                endif
                return 0
            endif
        endmethod
 
        private static method onInit takes nothing returns nothing
            call ForceAddPlayer(bj_F,GetLocalPlayer())
        endmethod
    endstruct
endlibrary
Oh, for sure, but you use memhack and still no event api. Ofc you or me can create alternate version that won't use *so much* code but this snippet... I don't care really while I'm happy I managed to create this functionality without your hacks. Them are great, but it makes anything else doing the same functionality with about equal performance twice as good.

EDIT: oh, and as I know, ForForce still does run function in a new thread, so your Code doesn't make any sensible difference in comparison to default trigger run.
Until there's no straight way to call custom function, Code is somewhat useless (in terms of Event realization, it's still a good snippet for memhack users).
 
Last edited:

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
It's a lot of code for something so simple:

JASS:
library CodeArray/*
***************************************************************************************
*
*   ***********************************************************************************
*   */ uses /*
*       */ AllocModule /*
*       */ MemoryHacks /* hiveworkshop.com/threads/memory-hack.289508/
*       *  Pjass  * hiveworkshop.com/threads/pjass-updates.258738/
*   ***********************************************************************************
*
*   CodeArray (Previously HandlerCode)
*   ¯¯¯¯¯¯¯¯¯
*   v2.1.2.0
*   by Trigger.edge
*
*   Allow the use of array code.
*
*   struct Code extends array
*
*       - method run takes nothing returns nothing
*       - method operator code takes nothing returns code
*       - method operator code= takes code c returns Code
*
**************************************************************************************/
 
    struct Code extends array
        private integer i_function
        private static force bj_F=CreateForce()
        implement AllocModule

        method run takes nothing returns nothing
            call ForForce(bj_F,I2C(.i_function))
        endmethod

        method operator code takes nothing returns code
            return I2C(.i_function)
        endmethod
 
        method operator code= takes code c returns Code
            if(null!=c)then
                set this=alloc()
                set .i_function=C2I(c)
                return this
            else
                if(0!=.i_function)then
                    set .i_function=0
                    call .dealloc()
                endif
                return 0
            endif
        endmethod
 
        private static method onInit takes nothing returns nothing
            call ForceAddPlayer(bj_F,GetLocalPlayer())
        endmethod
    endstruct
endlibrary
Using ForForce() is lighter compared to TriggerEvaluate() but it won't allow coupling many codes into one as you can do with the Event library. Nice though.


Anyway, I believe these should be static variables (correct me if I'm wrong)
JASS:
            // instant-used locals driven out of methods for optimization
            integer i;
            filterfunc filter;
and one last thing is that maybe method listener could have a better naming? The name does not seem so intuitive currently.[/code]
 
Level 8
Joined
Jan 23, 2015
Messages
121
Anyway, I believe these should be static variables (correct me if I'm wrong)
I guess it could conflict if functions using them are run in separated threads, if wc3 local machine can work them parallel, so array would be better a bit... Nevermind, I just forgot to add that keyword. Thanks.
Anyway, I just thought this optimization is so unmentionable for, I believe, so rarely used methods... I replaced them back to locals.
and one last thing is that maybe method listener could have a better naming? The name does not seem so intuitive currently.
I agree with this, but I just have no idea about how to do it other way - my vocabulary ends here, so I need help of native english speaker who knows what I'm trying to write.
 

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
Perhaps Event.registerTrigger() will do. Similarly the subscribe/unsubscribe can be changed to register/unregister because they're what's commonly used so users will familiarize the api much quickly. But subscribe/unsubscribe will also do.

Edit:
Btw, looking at Jesus4Lyf's Event, you can also make it so that this resource can also unregister triggers.
 
Last edited:
Level 8
Joined
Jan 23, 2015
Messages
121
Perhaps Event.registerTrigger() will do. Similarly the subscribe/unsubscribe can be changed to register/unregister because they're what's commonly used so users will familiarize the api much quickly. But subscribe/unsubscribe will also do.
Reg/unreg will do, but I don't like that 15-symbols-wide name Event.registerTrigger().

Btw, looking at Jesus4Lyf's Event, you can also make it so that this resource can also unregister triggers.
Jesus registers and fires each trigger manually while I just subscribe them to one real value, and since we have no way to remove registered events from triggers, I have no way to implement mentioned functionality without just c&p the entire J4L's code. I don't like the idea of copy-pasting anything if I have no ideas of improving it, and I have none right now.

And it seems that the way he uses struct allocating is incompatible with mine, and his event is way more likely to reach the array index limitation than mine.

I'm just wondering now, what things can't I reach with code what I can with triggers?
The only one came in mind is that I can't DisableTrigger with several codes subcribed in one line, but I think it can be replaced with code manipulations that also looks clearer and don't require additional trigger overhead, though in several lines (which sounds not like an argument for me).

So now, triggers are unsafe with dynamic Events, and I'd like to get rid of trigger functionality rather than solve this unsafety with other workarounds since I have no proves triggers are really needed.
 
Last edited:

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
Reg/unreg will do, but I don't like that 15-symbols-wide name Event.registerTrigger().
How about Event.linkTrigger()?

Well, you're right regarding Jesus4Lyf's method. Not only that it makes the operations costly compared to just using a variable event but it would also make the script convoluted.
But I think it's still good to include trigger registration here. Apart from the fact that you cant do DisableTrigger() safely as what you said, it also allows users to use TriggerSleepAction() easily inside their code. Plus, the DisableTrigger() allows you to easily avoid recursion problems if there are any.
So I guess the current setup regarding a variable event is okay and the users just needed to be informed that they have to manually unlink the trigger from the Event if they need to.
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
JASS:
/*
*
************************************************************************************
*
*   struct Trigger extends array
*        
*       Fields
*       -------------------------
*
*           readonly trigger trigger
*           -   use to register events, nothing else
*           -   keep in mind that triggers referencing this trigger won't fire when events fire
*           -   this trigger will fire when triggers referencing this trigger are fired
*
*           boolean enabled
*
*       Methods
*       -------------------------
*
*           static method create takes boolean reversed returns Trigger
*           -   when reverse is true, the entire expression is run in reverse
*
*           method destroy takes nothing returns nothing
*
*           method register takes boolexpr expression returns TriggerCondition
*
*           method reference takes Trigger trig returns TriggerReference
*           -   like register, but for triggers instead
*
*           method fire takes nothing returns nothing
*
*           method clear takes nothing returns nothing
*           -   clears expressions
*           method clearReferences takes nothing returns nothing
*           -   clears trigger references
*           method clearBackReferences takes nothing returns nothing
*           -   removes references for all triggers referencing this trigger
*           method clearEvents takes nothing returns nothing
*           -   clears events
*
*           debug static method calculateMemoryUsage takes nothing returns integer
*           debug static method getAllocatedMemoryAsString takes nothing returns string
*
************************************************************************************
*
*   struct TriggerReference extends array
*        
*       Methods
*       -------------------------
*
*           method destroy takes nothing returns nothing
*
*           method replace takes Trigger trigger returns nothing
*
************************************************************************************
*
*   struct TriggerCondition extends array
*
*       Methods
*       -------------------------
*
*           method destroy takes nothing returns nothing
*
*           method replace takes boolexpr expr returns nothing
*
************************************************************************************/


Are simple events not enough? Triggers can reference other Triggers, which means that a Trigger
can have several entry points. This means that, for example, in a combat system, you could have
several phases of combat, all modular, all plug and play. Set up the combat system core as well
as basic phases and then let other libraries add new phases + new effects.

It also supports mixtures between global events (apply to all units) and local events (apply
to specific units) without the need for if-statements.

JASS:
/*
*   Here we have a very simple library
*
*   Whenever a unit is created, the eventTrigger will run
*
*   The library contains two triggers
*       eventTrigger
*       event
*
*   eventTrigger contains the library's own internal code (even a single function)
*   event is for users to register code to the library
*
*   There are two triggers that are registered in the following order
*
*       OnUnitCreationUserLibrary
*       OnUnitCreationUserLibrary2
*
*   From here, user code is registered in the following order
*
*       UserLib3    -> OnUnitCreationUserLibrary2
*       UserLib1    -> OnUnitCreationUserLibrary
*       User1       -> OnUnitCreation
*       UserLib2    -> OnUnitCreationUserLibrary
*
*   The resulting order of the code will be
*
*       UserLib1
*       UserLib2
*       UserLib3
*       User1
*
*   To understand order
*
*       OnUnitCreation
*           OnUnitCreationUserLibrary
*           OnUnitCreationUserLibrary2
*           User1
*
*       OnUnitCreationUserLibrary
*           UserLib1
*           UserLib2
*
*       OnUnitCreationUserLibrary2
*           UserLib3
*
*   If any of the functions return true, all of the functions after them will not be run
*
*   Only the primary system should ever return true. If wanting to enable/disable things, run internal
*   triggers via trigger.fire().
*
*   One possible design for a VERY fast DDS would be to create 1 trigger for each unit and then make it reference
*   the DDS library followed by the global DDS event. Anything registered to that specific unit will then fire only for that
*   unit, and anything registered to the global trigger will fire for every unit. No traditional trigger.fire() is used in
*   this design, but the design uses extra memory as each unit gets its very own trigger.
*/

library OnUnitCreation uses Trigger
    struct OnUnitCreation extends array
        readonly static Trigger event                              //event response trigger
      
        private static integer instanceCount = 0
      
        private static method onCreate takes nothing returns boolean
            call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,"Main Library: Indexed Unit")
      
            set instanceCount = instanceCount + 1
            call SetUnitUserData(GetTriggerUnit(), instanceCount)
          
            return false
        endmethod
  
        private static method init takes nothing returns nothing
            local rect worldBounds = GetWorldBounds()
            local region worldBoundsRegion = CreateRegion()
          
            call RegionAddRect(worldBoundsRegion, worldBounds)
            call RemoveRect(worldBounds)
            set worldBounds = null
          
            set event = Trigger.create(false)
          
            call TriggerRegisterEnterRegion(event.trigger, worldBoundsRegion, null)
          
            set worldBoundsRegion = null
          
            call event.register(Condition(function thistype.onCreate))
        endmethod
  
        implement Init
    endstruct
endlibrary

/*
*   Libraries?
*/
struct OnUnitCreationUserLibrary extends array
    readonly static Trigger event

    private static method onInit takes nothing returns nothing
        set event = Trigger.create(false)
  
        call OnUnitCreation.event.reference(event)
    endmethod
endstruct

/*
*   Libraries?
*/
struct OnUnitCreationUserLibrary2 extends array
    readonly static Trigger event

    private static method onInit takes nothing returns nothing
        set event = Trigger.create(false)
  
        call OnUnitCreation.event.reference(event)
    endmethod
endstruct

/*
*   Event Response
*/
struct UserLib3 extends array
    private static method onCreate takes nothing returns boolean
        call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,"Uses OnUnitCreationUserLibrary2 (3)")
        return false
    endmethod

    private static method onInit takes nothing returns nothing
        call OnUnitCreationUserLibrary2.event.register(Condition(function thistype.onCreate))
    endmethod
endstruct

struct UserLib1 extends array
    private static method onCreate takes nothing returns boolean
        call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,"Uses OnUnitCreationUserLibrary (1)")
        return false
    endmethod

    private static method onInit takes nothing returns nothing
        call OnUnitCreationUserLibrary.event.register(Condition(function thistype.onCreate))
    endmethod
endstruct

struct User1 extends array
    private static method onCreate takes nothing returns boolean
        call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,"Uses OnUnitCreation (1)")
        return false
    endmethod

    private static method onInit takes nothing returns nothing
        call OnUnitCreation.event.register(Condition(function thistype.onCreate))
    endmethod
endstruct

struct UserLib2 extends array
    private static method onCreate takes nothing returns boolean
        call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,"Uses OnUnitCreationUserLibrary (2)")
        return false
    endmethod

    private static method onInit takes nothing returns nothing
        call OnUnitCreationUserLibrary.event.register(Condition(function thistype.onCreate))
    endmethod
endstruct

/*
*   Run
*/
struct UserCode extends array
    private static method onInit takes nothing returns nothing
        call CreateUnit(Player(0), 'hfoo', 0, 0, 270)
    endmethod
endstruct


What happens when data relies on other data? Triggers can be run in reverse order!
Furthermore, reversing a Trigger only effects that Trigger's direct data, not the
data of referenced Triggers. This means that on 1 Trigger, some of the data can run
in reverse and some of the data can run in order. Why does this matter? See the below.

It would be best to open the map and run this example. It's included in the map.

JASS:
/*
*   Here, the entry point of a system for some event is A
*
*   B depends on the data of A
*/

library A uses Trigger
    struct A extends array
        static Trigger event
        static Trigger start
        static Trigger end
        static integer data = 0
   
        private static method s takes nothing returns boolean
            set data = 100
           
            call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 60000, "A Start -> " + I2S(data))
           
            return false
        endmethod
       
        private static method e takes nothing returns boolean
            set data = 0
           
            call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 60000, "A End -> " + I2S(data))
           
            return false
        endmethod
   
        private static method onInit takes nothing returns nothing
            set event = Trigger.create(false)
            set start = Trigger.create(false)
            set end = Trigger.create(true)
           
            call start.register(Condition(function thistype.s))
            call end.register(Condition(function thistype.e))
           
            call event.reference(start)
            call event.reference(end)
        endmethod
    endstruct
endlibrary

library B uses A
    /*
    *   can do sub events if desired with triggers in B
    */
    struct B extends array
        static integer data = 0
   
        private static method s takes nothing returns boolean
            set data = A.data*2
           
            call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 60000, "B Start -> " + I2S(A.data) + ", " + I2S(data))
           
            return false
        endmethod
       
        private static method e takes nothing returns boolean
            set data = 0
           
            call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 60000, "B End -> " + I2S(A.data) + ", " + I2S(data))
           
            return false
        endmethod
   
        private static method onInit takes nothing returns nothing
            call A.start.register(Condition(function thistype.s))
            call A.end.register(Condition(function thistype.e))
        endmethod
    endstruct
endlibrary

library User uses A, B
    struct User extends array
        static integer data = 0
       
        private static method s takes nothing returns boolean
            set data = B.data + A.data
           
            call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 60000, "User Start -> " + I2S(A.data) + ", " + I2S(B.data) + ", " + I2S(data))
           
            return false
        endmethod
       
        private static method e takes nothing returns boolean
            set data = 0
       
            call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 60000, "User End -> " + I2S(A.data) + ", " + I2S(B.data) + ", " + I2S(data))
           
            return false
        endmethod
   
        private static method onInit takes nothing returns nothing
            call A.start.register(Condition(function thistype.s))
            call A.end.register(Condition(function thistype.e))
        endmethod
    endstruct
endlibrary

struct Run extends array
    private static method onInit takes nothing returns nothing
        call A.event.fire()
    endmethod
endstruct


Triggers can handle TriggerRemoveCondition. Native triggers can't.

JASS:
/*
*   Here we have 4 triggers
*
*       OnEscape runs whenever player 0 hits escape
*
*   C references B
*   B references A
*
*   OnEscape references C so that C will run when the player hits escape, it is otherwise empty (another way to register events)
*
*   When the player hits escape 5 times, B will be destroyed
*
*   When OnEscape is run, it will run in this order
*
*       A, B, C
*
*   When B is destroyed, only C will run as C loses its connection to A
*
*   One interesting thing to note is that when B is destroyed, C is still run
*
*   Try the same example with plain triggers and triggerconditions to see what happens (native trigger stability example)
*/

struct OnEscape extends array
    readonly static Trigger trigger
   
    private static method onInit takes nothing returns nothing
        set trigger = Trigger.create(false)
       
        call TriggerRegisterPlayerEvent(trigger.trigger, Player(0), EVENT_PLAYER_END_CINEMATIC)
    endmethod
endstruct

struct A extends array
    readonly static Trigger trigger
   
    private static method onFire takes nothing returns boolean
        call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,"Ran A")
       
        return false
    endmethod
       
    private static method onInit takes nothing returns nothing
        set trigger = Trigger.create(false)
       
        call trigger.register(Condition(function thistype.onFire))
    endmethod
endstruct

struct B extends array
    readonly static Trigger trigger
    private static integer escapeCount = 0
   
    private static method onFire takes nothing returns boolean
        set escapeCount = escapeCount + 1
       
        if (escapeCount == 5) then
            call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,"Destroyed B")
            call trigger.destroy()
        else
            call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,"Ran B: " + I2S(escapeCount))
        endif
       
        return false
    endmethod
       
    private static method onInit takes nothing returns nothing
        set trigger = Trigger.create(false)
       
        call trigger.reference(A.trigger)
       
        call trigger.register(Condition(function thistype.onFire))
    endmethod
endstruct

struct C extends array
    readonly static Trigger trigger
   
    private static method onFire takes nothing returns boolean
        call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,"Ran C\n------------------------------------------------")
       
        return false
    endmethod
       
    private static method onInit takes nothing returns nothing
        set trigger = Trigger.create(false)
       
        call trigger.reference(B.trigger)
       
        call trigger.register(Condition(function thistype.onFire))
       
        call OnEscape.trigger.reference(trigger)
    endmethod
endstruct

JASS:
/*
*   Notice that when B is destroyed, C is never run
*/

struct OnEscape extends array
    readonly static trigger trigger
   
    private static method onInit takes nothing returns nothing
        set trigger = CreateTrigger()
       
        call TriggerRegisterPlayerEvent(trigger, Player(0), EVENT_PLAYER_END_CINEMATIC)
    endmethod
endstruct

struct A extends array
    private static method onFire takes nothing returns boolean
        call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,"Ran A")
       
        return false
    endmethod
       
    private static method onInit takes nothing returns nothing
        call TriggerAddCondition(OnEscape.trigger, Condition(function thistype.onFire))
    endmethod
endstruct

struct B extends array
    private static triggercondition tc
    private static integer escapeCount = 0
   
    private static method onFire takes nothing returns boolean
        set escapeCount = escapeCount + 1
       
        if (escapeCount == 5) then
            call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,"Destroyed B")
            call TriggerRemoveCondition(GetTriggeringTrigger(), tc)
            set tc = null
        else
            call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,"Ran B: " + I2S(escapeCount))
        endif
       
        return false
    endmethod
       
    private static method onInit takes nothing returns nothing
        set tc = TriggerAddCondition(OnEscape.trigger, Condition(function thistype.onFire))
    endmethod
endstruct

struct C extends array
    private static method onFire takes nothing returns boolean
        call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60000,"Ran C\n------------------------------------------------")
       
        return false
    endmethod
       
    private static method onInit takes nothing returns nothing
        call TriggerAddCondition(OnEscape.trigger, Condition(function thistype.onFire))
    endmethod
endstruct
 
Last edited:
Level 8
Joined
Jan 23, 2015
Messages
121
Apart from the fact that you cant do DisableTrigger() safely as what you said, it also allows users to use TriggerSleepAction() easily inside their code. Plus, the DisableTrigger() allows you to easily avoid recursion problems if there are any.
Seems like I didn't make myself clear - DisableTrigger() is safe, the only thing I'm curious about is that if an Event is destroyed and replaced with another Event any trigger registered on destroyed Event would be fired at new Event.
And any DisableTrigger() can be replaced with several Event.unregister(function ###) lines (except for cases when trigger subscribed to Event has dynamic conditions... but that case should be replaced with just another Event).

Also, I think about implementing Event.disable() method but don't know if it is really needed?
 
Level 8
Joined
Jan 23, 2015
Messages
121
I never knew about Trigger before Netharus posted it here. Thank you for it.
I've researched into it, and I find it actually better than my snippet, still I'm scared of that big requirement list (even after every thing of it is useful). My snippet, I guess, is better by its simplicity with both simple usage and pretty simple arrangement (its almos 7 times shorter than Trigger and requires only Table), and so it could be better in some cases.
Though it seems to me that Trigger and other snippets it requires is much more useful to me, and I guess I'll stop developing this snippet because of not using it.
Even after that I find Trigger coded such way that I want to rewrite it by myself, at least to increase its readability and decrease the amount of code.

Updated with last changes and fixes I've done. Now Event should work flawlessly.
 
Level 8
Joined
Jan 23, 2015
Messages
121
I don't think you can decrease the amount of code in Trigger : ).
Well, maybe I don't understand something about debug, but that giant static ifs in struct Trigger that just doubles the code in order to make functions have postfix _p if debug is on - that makes it both fat and less understandable. And I really don't understand it's purposes, since new _p functions seems like BJ functions doing nothing else other than calling other function.
And I think that you could decrease the amount of structs you're using, making that relatively complex data structure easier at least because all table fields would be concentrated to one or two places. And then you probably won't use endless typecasting (also decreasing code amount for a bit). One learning your system would know that there's only 3 types of index used here, for Trigger, for Reference and for Condition - now it's hard to learn by yourself because there's so many structs typecasting to each other and containing only a bit of data.
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
Well, maybe I don't understand something about debug, but that giant static ifs in
struct Trigger
that just doubles the code in order to make functions have postfix _p if debug is on - that makes it both fat and less understandable.

There is a trick here that you are missing. It is not a BJ function call : ). What happens when a method calls a method that is defined after it is? It turns into a trigger evaluation. The duplicated code is to stop op limit. I remembered why I did it as I finished this post ;D. This isn't good as it changes behavior in the code. What may cause an op limit in prod won't cause an op limit in dev, which breaks testing. The anti-op limit measures taken only in debug should either be applied to both or not at all. To keep this system relevant, the anti-op limit debug stuff should be removed.

And I think that you could decrease the amount of structs you're using, making that relatively complex data structure easier at least because all table fields would be concentrated to one or two places.

Some of the structs are fields for other structs

JASS:
    private struct TriggerConditionDataCollection extends array
        implement UniqueNxListT
    endstruct

    // Trigger {
    //    List<Code> code
    // }

The system was organized into different components rather than mixed together.

Things weren't mixed together to keep the API of each struct clean and also to make the code easier to navigate. Common elements are grouped together with the methods that act upon them. It also clears out circular references. TriggerRefs and TriggerBackRefs both rely on TriggerConditionData, which relies on TriggerRefs and TriggerBackRefs. By splitting the data from the algorithms, you fix this.


Combining multiple data structures into a single struct will just result in spaghetti code. Part of the reason everything was split up was so that things could access the table fields without generating triggers ; ).


Code:
public class Trigger
{
    private static class TriggerReference
    {
        public List<TriggerReference>.Node positionInList;
        public Trigger trigger; // the Trigger being referenced
        public BooleanExpression booleanExpression // the expression that the referenced trigger's expression is contained in
    }

    private static class TriggerBackReference
    {
         public Trigger trigger; // the Trigger referencing this trigger
         public List<TriggerBackReference>.Node positionInList
    }

    private List<TriggerBackReference> backReferences; // triggers that this trigger has been added to
    private List<TriggerReference> references; // triggers that have been added to Trigger

    private trigger trigger; // native trigger
    private triggercondition triggercondition // represents the boolexpr on the trigger
    private BooleanExpression booleanExpression // contains the native boolexpr
}

BooleanExpression itself is two parallel AVL trees. One tree is the actual boolexpr, combined with Or. The other tree allows navigation over the boolexpr tree. In this way, all code can be combined into a single balanced boolexpr, which maximizes speed. By combining all boolexpr into a single one instead of putting each one in its own triggercondition, this also allows one to remove boolexpr from a trigger without crashing them =). TriggerRemoveCondition will work for the currently executing boolexpr because there is only one actual boolexpr in the trigger. The tree itself remains in scope until evaluation has finished, even if elements in the tree have been destroyed.

So, actually not that complicated.


Here is the BooleanExpression code

JASS/script.j at master · nestharus/JASS · GitHub


Keep in mind that you will have to update every BooleanExpression that a Trigger is on when that Trigger is destroyed. This is not only backrefs. This is backrefs of backrefs of backrefs. This method here and another one is the reason I had added the anti-op limit stuff to debug mode.

JASS:
        method updateExpression takes nothing returns nothing
            /*
            *   Iterate over all triggers referencing this trigger
            */
            set node = first
            loop
                exitwhen node == 0
         
                /*
                *   Replace expression and then update the target trigger
                */
                call node.ref.expr.replace(expr)
                call node.trig.updateTrigger()
                call TriggerReferencedList(node.trig).updateExpression()
         
                set node = node.next
            endloop
        endmethod
 
Last edited:
Level 8
Joined
Jan 23, 2015
Messages
121
There is a trick here that you are missing. It is not a BJ function call : ). What happens when a method calls a method that is defined after it is? It turns into a trigger evaluation. The duplicated code is to stop op limit.
The system was organized into different components rather than mixed together.

Things weren't mixed together to keep the API of each struct clean and also to make the code easier to navigate. Common elements are grouped together with the methods that act upon them. It also clears out circular references. TriggerRefs and TriggerBackRefs both rely on TriggerConditionData, which relies on TriggerRefs and TriggerBackRefs. By splitting the data from the algorithms, you fix this.


Combining multiple data structures into a single struct will just result in spaghetti code. Part of the reason everything was split up was so that things could access the table fields without generating triggers ; ).


BooleanExpression itself is two parallel AVL trees. One tree is the actual boolexpr, combined with Or. The other tree allows navigation over the boolexpr tree. In this way, all code can be combined into a single balanced boolexpr, which maximizes speed. By combining all boolexpr into a single one instead of putting each one in its own triggercondition, this also allows one to remove boolexpr from a trigger without crashing them =). TriggerRemoveCondition will work for the currently executing boolexpr because there is only one actual boolexpr in the trigger. The tree itself remains in scope until evaluation has finished, even if elements in the tree have been destroyed.

So, actually not that complicated.
Wow.
That's really hard to understand without help. If it would be described somehow in the system, there would be no questions, but no comments I saw explains that. The question is - does it needs this understandability, since it's so "not" complicated... As for me it works and works incredibly good as far as I know, and I don't really want to understand that things completely.
Yep.
Still I think that this way greater math isn't needed in several situations and then some simplier systems would be better to use, just like mine Event.

The last thing I'm thinking about for Event is how to manage uncreated Event access.

UPD: A little bit offtop, but Netharus, I don't understand where's your TimerTools has gone to :( It is by far the best timer system because of awesome optimization, and I probably need it - the only last thing I managed to find is v3.0.0.3 in the last post to TT topic, but last change of the main post is dated later than it's last version...
 
Last edited:
Level 31
Joined
Jul 10, 2007
Messages
6,306
Last edited:
Level 8
Joined
Jan 23, 2015
Messages
121
JASS/jass/Systems/TimerTools at master · nestharus/JASS · GitHub

It seems to break native timers at the moment and needs to be fixed.


Timer Allocation explanation of how system works



There should be a repeating timer and a 1-shot timer. The 1-shot timer would guarantee accuracy of the first tick. The repeating timer wouldn't guarantee accuracy of first tick.
But you said it somewhere that all bugs was fixed... Meh. Sad actually.

Thanks for GitHub link. I assume there I can find any of your creations in its last version, right?
...wow, there's lots of cool systems...
Also, I saw your post about attack indexing, where you said you'll release an attack indexing system soon - does it exists somewhere already?
 

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
Sorry for the very late response, I've been really busy due to school stuffs. Anyway, a thing I haven't considered previously is to keep the debug protection only in DEBUG_MODE like in the methods register/unregister and similarly in destroy method, you should only not deallocate the instance if the check passes in DEBUG_MODE. This is a good practice since mappers should thoroughly debug in DEBUG_MODE first before in non-debug mode anyway.
 
Level 8
Joined
Jan 23, 2015
Messages
121
Anyway thanks for your reply, I'm glad to see you're here again :)
Well if I'll make that destroy check debug-only feature, then it may break something later on because we can't remove trigger's registered events - destroyed event had an id some trigger was registered to, but then on same id was created just another event - and that trigger will fire each time you fire event it wasn't in idea registered to, so last thing I can do is to place there a debug message. Anyway I don't recommend using linkTrigger() feature, but it's needed for some rare cases. The worst case is dynamic events with dynamic functions, where system will soon run out of free indexes, but else probably break something.
 

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
Well, if there's an error message in DEBUG_MODE telling a destroyed Event has an attached trigger, they'll be required to fix it so that they do not destroy an Event with a trigger throughout their map. Well yes, I forgot to say that you should also display an error message before making the protection debug only so that they will we forced to fix the problem. It's also okay if you choose not to make the protection debug only atleast in the destroy method but if so, you should also destroy the trigger handle and null it because that instance won't be allocated again meaning, the trigger is not anymore used. Btw, the method names in subscribe/unsubscribe in the documentation is not updated.
 
Top