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

Quick Question: Custom Events

Status
Not open for further replies.
Level 15
Joined
Nov 30, 2007
Messages
1,202
JASS:
scope AbilityFinish initializer Init

    globals
        unit lastSpellTarget
        unit lastSpellCaster
    endglobals

    function Main takes nothing returns boolean
        local integer spell = GetSpellAbilityId()
        set lastSpellTarget = GetSpellTargetUnit()  // What if no target, just set to null or mess every thing up?
        set lastSpellCaster = GetTriggerUnit()
        if spell == FOOD_ON or spell == FOOD_OFF then
            set customEvent = EVENT_RESET
            set customEvent = EVENT_AB_FOOD
        elseif spell == DEMOLISH then 
            set customEvent = EVENT_RESET
            set customEvent = EVENT_AB_DEMOLISH
        endif 
        
        // Etc...
        
        return false 
    endfunction

    private function Init takes nothing returns nothing
        local trigger t = CreateTrigger()
        local integer i = 0
        loop
            call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_SPELL_FINISH, null)
            set  i = i + 1
            exitwhen i == bj_MAX_PLAYER_SLOTS
        endloop  
        call TriggerAddCondition(t, Condition(function Main))
    endfunction
endscope

Is it reasonable to centralize all your spell triggers in this manner?
 
Level 24
Joined
Aug 1, 2013
Messages
4,657
that is one way... however when having 2000 spells and you cast the last one, you will have 1999 integer checks that will return false.

What you can do as well is set customEvent to GetSpellAbilityId() instead.
Then you will have to take care of the proper numbers but it works 1999 times better.

The last thing you can do is save all the responses (the triggers that run when an event is fired) inside a hashtable with the id of the spell as a key.
then you can evaluate the trigger of that spell specificly.
 
Level 15
Joined
Nov 30, 2007
Messages
1,202
that is one way... however when having 2000 spells and you cast the last one, you will have 1999 integer checks that will return false.

What you can do as well is set customEvent to GetSpellAbilityId() instead.
Then you will have to take care of the proper numbers but it works 1999 times better.

The last thing you can do is save all the responses (the triggers that run when an event is fired) inside a hashtable with the id of the spell as a key.
then you can evaluate the trigger of that spell specificly.

It doesn't greet me, how rude. ;/

JASS:
globals
    hashtable spellHash = InitHashtable()
    constant integer SPELL = 'Aroa'
    trigger t = CreateTrigger()
endglobals

scope AbilitySetup initializer Init
    
    function test takes nothing returns nothing
        call BJDebugMsg("Greetings!")
    endfunction
    
    private function Init takes nothing returns nothing
        call SaveTriggerActionHandle(spellHash, 0, SPELL, TriggerAddAction(t, function test))
        call LoadTriggerActionHandle(spellHash, 0, SPELL) //?
    endfunction
endscope

I can use this tho..
JASS:
call ExecuteFunc("test")
 
Last edited:

Dr Super Good

Spell Reviewer
Level 64
Joined
Jan 18, 2005
Messages
27,198
The idea is that you create a custom event system for specific events rather than trying to branch to the right code. This way you can request the service from the service user code rather than having to modify the service provider code to add a new service user. One way has reasonable coupling (many things depend on the service) while the other way is unreasonable (the service depends on many things).

For convenience you should try and simulate normal events. This means that your function declarations could look like any of the following.
JASS:
TriggerRegisterPlayerUnitSpellFinishEvent takes trigger whichTrigger,player whichPlayer,integer abilityID returns nothing
TriggerRegisterGenericUnitSpellFinishEvent takes trigger whichTrigger,integer abilityID returns nothing
TriggerRegisterUnitSpellFinishEvent takes trigger whichTrigger,unit whichUnit,integer abilityID returns nothing

The ability triggers can then call them instead of the generic event attachment.

The function definition then sets up some appropriate mappings inside hashtables so that a single generic event response can query any appropriate triggers to run and evaluate them. This might not be the fastest way to do it however it is the easiest and backwards compatible with most trigger abilities simply by changing the required events. It should be a ton faster than dozens of generic events.

How it is implemented and what features it supports is entirely up to you. You might want de-register functions to remove triggers for example.

Below I give a simple example implementation which only supports generic units (not specific or player filtered) and can register at most 1 trigger per ability. Such an implementation would be suitable for most trigger enhanced abilities that are fully MUI.

JASS:
library GenericUnitSpellFinish initializer Init

    globals
        private hashtable ResponseMap = InitHashtable()
        private trigger EventEntry = CreateTrigger()
    endglobals

    TriggerRegisterGenericUnitSpellFinishEvent takes trigger whichTrigger,integer abilityID returns nothing
        call SaveTriggerHandle(ResponseMap, abilityID, 0, whichTrigger)
    endfunction


    TriggerRegisterGenericUnitSpellFinishEventUnhook takes integer abilityID returns nothing
        call FlushChildHashtable(ResponseMap, abilityID)
    endfunction

    private function Handler takes nothing returns nothing
        local integer said = GetSpellAbilityId()
        local trigger trig

        // fetch trigger
        if HaveSavedHandle(ResponseMap, said, 0) then
            // have a trigger to run
            set trig = LoadTriggerHandle(ResponseMap, said, 0)
        else
            // nothing to run
            return
        endif

        // run trigger
        if TriggerEvaluate(trig) then
            call TriggerExecute(trig)
        endif

        // local handle reference count bug fix
        set trig = null
    endfunction

    private function Init takes nothing returns nothing
        local integer i = 0
        loop
            call TriggerRegisterPlayerUnitEvent(EventEntry, Player(i), EVENT_PLAYER_UNIT_SPELL_FINISH, null)
            set  i = i + 1
            exitwhen i == bj_MAX_PLAYER_SLOTS
        endloop  
        call TriggerAddAction(EventEntry, function Handler)
    endfunction
endlibrary

With such systems implementation speed is not that important as the event fires quite seldom (at most a few dozen ability casts per minute). Instead algorithm complexity is important so that the overhead remains constant no mater how many abilities use the system as that is where the most time can be gained or lost. Common GUI ways of MUI trigger enhanced abilities have a complexity of O(n) where n is the number of such triggers as for each ability cast they run script to check if it is the appropriate ability to respond to. On the other hand the above demonstration library would operate at O(1) complexity due to the hashtable lookup allowing only the appropriate trigger to run. Sure this system has larger overhead than a single trigger enhanced ability testing for the appropriate ability however the better complexity will mean significant time savings for large numbers of trigger enhanced abilities.
 
Level 15
Joined
Nov 30, 2007
Messages
1,202
Wow! Big THANKS for the guide!

Would change one thing in your Handle function though:

JASS:
private function Main takes nothing returns nothing
        local integer spell = GetSpellAbilityId()
        local trigger t = LoadTriggerHandle(hash,  spell, 0)
        if t == null then 
            return
        endif
        if TriggerEvaluate(t) then
            call TriggerExecute(t)
        endif
        set t = null
    endfunction

Not sure if TriggerEvaluate(t) is needed either, because either it received a trigger when we saved or it didn't compile properly.
 
Level 24
Joined
Aug 1, 2013
Messages
4,657
evaluate and execute depend on how your other triggers are structurized.
evaluate calls the conditions
execute calls the actions
if you know for sure that you only use conditions in your spell effect triggers, you can just use triggerevaluate, but having both in this order is good as well.
 

Dr Super Good

Spell Reviewer
Level 64
Joined
Jan 18, 2005
Messages
27,198
Would change one thing in your Handle function though:
At one stage loading non-existent mappings or mappings of the wrong type could cause fatal errors. Hence the check.

Not sure if TriggerEvaluate(t) is needed either, because either it received a trigger when we saved or it didn't compile properly.
This is needed to be compatible with triggers that have conditions so that the condition is correctly evaluated before the actions run.

Both should inherit terms like Triggering Unit correctly judging from what GUI users have reported.
 
Level 15
Joined
Nov 30, 2007
Messages
1,202
Both should inherit terms like Triggering Unit correctly judging from what GUI users have reported.

It didn't feel correctly to me, but why not just send a global caster and a global target to the other triggers? To clarify, in the actual spell trigger, I couldn't use such things as: local unit u = GetTriggerUnit()

______

Don't know what I did. But the abilities become uncastable. To be more specific I can morph and the FOOD_ON ability will be passed correctly, however, if I toggle it on and FOOD_OFF ability shall be passed instead nothing happens and I can't even morph any more. Whats more the unit can't even build after this bug. hmm?

FOOD_ON and OFF are based on Roar, and CITY_MORPH_1 is based on bear form.

Never mind, it was something with the EVENT being SPELL_CAST instead of SPELL_FINISH

JASS:
library GenericUnitSpellFinish initializer Init

    globals
        private hashtable hash  = InitHashtable()
        unit genLastSpellTarget
        unit genLastSpellCaster
    endglobals
    
    function TrgRegisterGenericSpellFinish takes trigger t, integer abilityId returns nothing
        call SaveTriggerHandle(hash, abilityId, 0, t)
    endfunction
    
    function TrgDeregisterGenericSpellFinish takes integer abilityId returns nothing
        call FlushChildHashtable(hash, abilityId)
    endfunction
    
    private function Main takes nothing returns nothing
        local integer spell = GetSpellAbilityId()
        local trigger t 
        if HaveSavedHandle(hash, spell, 0) then
            set t = LoadTriggerHandle(hash,  spell, 0)
        else
            return
        endif
        set genLastSpellTarget = GetSpellTargetUnit()
        set genLastSpellCaster = GetTriggerUnit()
        if TriggerEvaluate(t) then
            call TriggerExecute(t)
        endif
        set t = null
    endfunction
    
    private function Init takes nothing returns nothing
        local trigger t = CreateTrigger()
        local integer i = 0
        loop
            call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_SPELL_CAST, null)
            set i = i + 1
            exitwhen i == bj_MAX_PLAYER_SLOTS
        endloop
        call TriggerAddAction(t, function Main)
    endfunction
endlibrary

scope ToggleFoodDistribution initializer Init
    globals
        constant integer FOOD_ON = 'Aroa'
        constant integer FOOD_OFF = 'Ara2'
        constant integer CITY_MORPH_1 = 'Abrf'
    endglobals
    
    private function AddToogleOnRemove takes nothing returns nothing
        local integer id = GetUnitUserData(genLastSpellCaster)
        local unit u = genLastSpellCaster
        call TriggerSleepAction(0.1)    // Waits for morph
        if town[id].autoFeed == true then
            call UnitAddAbility(u, FOOD_OFF)
        else 
            call UnitAddAbility(u, FOOD_ON)
        endif
        set u = null
    endfunction
    
    private function FoodOn takes nothing returns nothing 
        set town[GetUnitUserData(genLastSpellCaster)].autoFeed = true
        call UnitAddAbility(genLastSpellCaster, FOOD_OFF)
        call UnitRemoveAbility(genLastSpellCaster, FOOD_ON) 
    endfunction
    
    private function FoodOff takes nothing returns nothing
        set town[GetUnitUserData(genLastSpellCaster)].autoFeed = false
        call UnitAddAbility(genLastSpellCaster, FOOD_ON)
        call UnitRemoveAbility(genLastSpellCaster, FOOD_OFF) 
    endfunction

    private function Init takes nothing returns nothing
        local trigger t1 = CreateTrigger()
        local trigger t2 = CreateTrigger()
        local trigger t3 = CreateTrigger()
        call TriggerAddAction(t1, function FoodOn)
        call TriggerAddAction(t2, function FoodOff)
        call TriggerAddAction(t3, function AddToogleOnRemove)
        call TrgRegisterGenericSpellFinish(t1, FOOD_ON)
        call TrgRegisterGenericSpellFinish(t2, FOOD_OFF)
        call TrgRegisterGenericSpellFinish(t3, CITY_MORPH_1)
        set t1 = null
        set t2 = null
        set t3 = null
    endfunction
endscope
 
Last edited:
Level 15
Joined
Nov 30, 2007
Messages
1,202
Level 15
Joined
Nov 30, 2007
Messages
1,202
Follow up. When is this actually worth doing? Take this example, upon building finish:

JASS:
        if GetUnitTypeId(u) == GRANARY then 
           // Do stuff
        endif
        
        if GetUnitTypeId(u) == BARRACKS then 
           // Do stuff
        endif
        
        if GetUnitTypeId(u) == FARM then 
           // Do stuff
        endif
        
        // etc..

Can this too load and call instead?
 
Level 24
Joined
Aug 1, 2013
Messages
4,657
Everything that can be converted into an integer can be used as an event or event parameter.

In this case, you can use GetUnitTypeId() in the place where you used GetSpellAbilityId() before...
However, as you used a system which is specificly for abilities, it wont really work 100%.

On the other hand, the system is exactly the same.
 
Level 15
Joined
Nov 30, 2007
Messages
1,202
Everything that can be converted into an integer can be used as an event or event parameter.

In this case, you can use GetUnitTypeId() in the place where you used GetSpellAbilityId() before...
However, as you used a system which is specificly for abilities, it wont really work 100%.

On the other hand, the system is exactly the same.

Can I save and load functions instead of whole triggers, could you show such an example please? Or should I stick to the provided method?

But isn't it a bit silly to do this just to avoid if-statements?
 
Level 24
Joined
Aug 1, 2013
Messages
4,657
1. no, you cannot save functions inside an array.
Whay you could do is save the name of the function inside an array and use it to call the function but it is a slow operation.

2. It kind of is until you have numberless of those if-statements.
In your example, you should at least use "elseif" instead of "else \n if" but what you showed is exactly what happens in most maps.
It is silly to think that integer comparisons are a big deal, but it is not silly to replace numberless of integer comparisons with a hashtable load.
 
Level 17
Joined
Apr 27, 2008
Messages
2,455
ExecuteFunc doesn't crash anymore if the function doesn't exist, right ?

So instead of all this stuff i suppose you could just use ExecuteFunc(GetObjectName(GetSpellAbilityId())), but the spellName has to be the same for all wc3 languages though and yeah if you change it you have to the same in the script.

Or you could use the spell id as string, but that would be less intuitive.
Anyway, i'm just having fun there, nothing serious.
 
Status
Not open for further replies.
Top