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

[vJASS] Creating MUI spells with periodic timers in scopes

Status
Not open for further replies.
Level 4
Joined
Sep 25, 2005
Messages
71
In an effort to make my stuff a lot more readable and organized, I've started adventuring into the realm of scopes. However, I'm having some issues, primarily in the fact that I have no idea how to pass data (a struct in this case) to a periodic timer without using outside globals:

JASS:
// Blizzard Counter -
//  Responds to an enemy attack with a cast of Blizzard
//  with a cooldown of five seconds

scope BlizzardCounter

    globals
        private constant integer ABIL_ID = 'A00D'
        private constant real COUNTER_CD = 5.00
    endglobals
    
    globals
        private BlizzardData array blizzarray // Gives an undefined type error
        private integer instances
    endglobals

    private struct BlizzardData
        unit attacked
        unit attacker
        timer cdtimer
        integer level
        real cooldown
        real x
        real y
    
    private static method BlizzCast takes nothing returns nothing
    // Empty for now
    endmethod
    
    private method Blizzard takes nothing returns nothing
        local thistype localblizzard = thistype.allocate()
        
        set .attacked = GetTriggerUnit()
        set .attacker = GetAttacker()
        set .cdtimer = CreateTimer()
        set .level = GetUnitAbilityLevel(GetTriggerUnit(), ABIL_ID)
        set .cooldown = COUNTER_CD
        set .x = GetUnitX(.attacker)
        set .y = GetUnitY(.attacker)
        
        set blizzarray[instances] = localblizzard

        call TimerStart(.cdtimer, .5, true, function thistype.BlizzCast)
    endmethod
    

    private static method Check takes nothing returns boolean
        if IsUnitAlly(GetAttacker(), GetOwningPlayer(GetTriggerUnit())) == false /*
        */and GetUnitAbilityLevel(GetTriggerUnit(), ABIL_ID) >= 1 /*
        */and ValidGroundTarget(GetAttacker(), GetTriggerUnit()) == true /*
        */then
            return true
        endif
        return false
    endmethod

    private static method DefaultTrue takes nothing returns boolean
        return true
    endmethod
    
    private method Init takes nothing returns nothing
        local trigger t = CreateTrigger()
        local integer i = 0
        loop
            exitwhen i > 15
            call TriggerAddCondition(t, Filter(function thistype.Check))
            call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_ATTACKED, Filter(function thistype.DefaultTrue) )
            set i = i + 1
        endloop
    endmethod
    
    endstruct
    
endscope

I need to be able to have multiple instances of the structs passed to the timer. I have no idea how to go forward, keeping it all bundle in a scope, and don't intend to use anything like another system (trying to learn.) I clearly can't define an array of a struct type from within the same scope, at least if its defined before the struct, which allows it to reference the global.

I was told I could use a hashtable, but my google fu is too weak to find a single comprehensive non-GUI, jass guide to hashtables. I also heard that having over 8120~ instances of a struct in an array prevents further, and although it's not in my agenda to create that many instances of the spell, I'd like to know if there's a way to circumvent this hard limit using other methods than just creating an array of structs, and iterating through the array for the number of instances of a spell to keep it multi-use.

Either way, if someone has a solution, or a lesson, I'm open to hearing it. Until then, I'm sort of stunted.
 
Last edited:
Level 18
Joined
Sep 14, 2012
Messages
3,413
Table library is cool.
It can have HandleTable ;)

Don't make a condition inside the loop !
JASS:
    private static method Check takes nothing returns boolean
        if IsUnitAlly(GetAttacker(), GetOwningPlayer(GetTriggerUnit())) == false /*
        */and GetUnitAbilityLevel(GetTriggerUnit(), ABIL_ID) >= 1 /*
        */and ValidGroundTarget(GetAttacker(), GetTriggerUnit()) == true /*
        */then
            return true
        endif
        return false
    endmethod
-->
JASS:
    private static method Check takes nothing returns boolean
        return //Write your conditions
    endmethod
 
Level 4
Joined
Sep 25, 2005
Messages
71
Table library is cool.
It can have HandleTable ;)

Don't make a condition inside the loop !

Fixed up check, made the condition prior to assigning events loop.

JASS:
   private static method Check takes nothing returns boolean
        return IsUnitAlly(GetAttacker(), GetOwningPlayer(GetTriggerUnit()))/*
        */and GetUnitAbilityLevel(GetTriggerUnit(), ABIL_ID) >= 1/*
        */and ValidGroundTarget(GetAttacker(), GetTriggerUnit())
    endmethod

    private static method DefaultTrue takes nothing returns boolean
        return true
    endmethod
    
    private method Init takes nothing returns nothing
        local trigger t = CreateTrigger()
        local integer i = 0
        call TriggerAddCondition(t, Filter(function thistype.Check))
        loop
            exitwhen i > 15
            call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_ATTACKED, Filter(function thistype.DefaultTrue) )
            set i = i + 1
        endloop
    endmethod

What is table library? What is handle table?

I hate to say it but the terms you're using are alien and you're speaking as though I have any understanding of them. It's why I asked if anyone had any advice/guides/a primer on as much.

I know handles are non-real/non-int things saved as variables, but that's it.
 
I need to be able to have multiple instances of the structs passed to the timer. I have no idea how to go forward, keeping it all bundle in a scope, and don't intend to use anything like another system (trying to learn.) I clearly can't define an array of a struct type from within the same scope, at least if its defined before the struct, which allows it to reference the global.

One thing you can do is define the global within the struct. To make a perfectly-global-esque variable within a struct, you must use the keyword static:
JASS:
private struct BlizzardData
    private static BlizzardData array blizzarray
    private static integer instances = 0
    
endstruct
From there, you can either reference it as BlizzardData.blizzarray or simply blizzarray if you are using it within the struct. If you are using it within the struct, you can also choose to write thistype instead of BlizzardData (it makes things less verbose and it'll be easier to change the struct's name, but things won't be as explicit)

Static variables are not associated with an instance. So if you have something like this:
JASS:
local thistype this = thistype.allocate()
set this.instances = 5 // static member
set this.attacked = GetTriggerUnit() // struct member (non-static)
I don't remember if it compiles, but either way it won't save a member for that instance. It will be editing it just as if it is a global. So internally, it would look like:
JASS:
local thistype this = thistype.allocate()
set instances = 5 // static
set attacked[this] = GetTriggerUnit() // associated with "this"

I was told I could use a hashtable, but my google fu is too weak to find a single comprehensive non-GUI, jass guide to hashtables.

Here is a simple example using your spell's code:
JASS:
// Blizzard Counter -
//  Responds to an enemy attack with a cast of Blizzard
//  with a cooldown of five seconds

scope BlizzardCounter

    globals
        private constant integer ABIL_ID = 'A00D'
        private constant real COUNTER_CD = 5.00
    endglobals
    
    globals
        //private BlizzardData array blizzarray // Gives an undefined type error
        //private integer instances
        private hashtable hash = InitHashtable()
    endglobals

    private struct BlizzardData
        unit attacked
        unit attacker
        timer cdtimer
        integer level
        real cooldown
        real x
        real y
    
    private static method BlizzCast takes nothing returns nothing
        local timer t = GetExpiredTimer() // gets the timer that expired
        local thistype this = LoadInteger(hash, 0, GetHandleId(t)) // loads the instance
        // write your code
        set t = null
    endmethod
    
    private method Blizzard takes nothing returns nothing
        local thistype localblizzard = thistype.allocate()
        
        set .attacked = GetTriggerUnit()
        set .attacker = GetAttacker()
        set .cdtimer = CreateTimer()
        set .level = GetUnitAbilityLevel(GetTriggerUnit(), ABIL_ID)
        set .cooldown = COUNTER_CD
        set .x = GetUnitX(.attacker)
        set .y = GetUnitY(.attacker)
        
        //set blizzarray[instances] = localblizzard
        call SaveInteger(hash, 0, GetHandleId(.cdtimer), this) // saves the instance
        // attaches it to the ID of the timer

        call TimerStart(.cdtimer, .5, true, function thistype.BlizzCast)
    endmethod
    

    private static method Check takes nothing returns boolean
        if IsUnitAlly(GetAttacker(), GetOwningPlayer(GetTriggerUnit())) == false /*
        */and GetUnitAbilityLevel(GetTriggerUnit(), ABIL_ID) >= 1 /*
        */and ValidGroundTarget(GetAttacker(), GetTriggerUnit()) == true /*
        */then
            return true
        endif
        return false
    endmethod

    private static method DefaultTrue takes nothing returns boolean
        return true
    endmethod
    
    private method Init takes nothing returns nothing
        local trigger t = CreateTrigger()
        local integer i = 0
        loop
            exitwhen i > 15
            call TriggerAddCondition(t, Filter(function thistype.Check))
            call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_ATTACKED, Filter(function thistype.DefaultTrue) )
            set i = i + 1
        endloop
    endmethod
    
    endstruct
    
endscope

Now, all this does is save the instance, "attached" to the timer. Each agent (a type of handle) is assigned a unique handle ID. This is perfect for hashtables, because this prevents hashtable collisions/overwriting data, so long as you use one timer per spell instance. Now, why do we attach it to the timer? Why not the caster, or the target? It is because when you use the native:
JASS:
call TimerStart(t, 0.03125, true, function Expire)
It starts a new thread, and most of the event responses will not return their proper values. However, we can still refer to the timer that expired through GetExpiredTimer(). Thus, we just save the data under the ID of the timer.

I also heard that having over 8120~ instances of a struct in an array prevents further, and although it's not in my agenda to create that many instances of the spell, I'd like to know if there's a way to circumvent this hard limit using other methods than just creating an array of structs, and iterating through the array for the number of instances of a spell to keep it multi-use.

8192. First thing to know is that this is not a limit to the number of instances of a spell; it is a limit to the number of simultaneous instances of a spell. Each time you create an instance, the counter for that struct is increased by 1. Likewise, each time you destroy an instance, the counter is reduced by 1.

That is why no one really pays heed to the limit. If the limit is hit, it is likely due to bad instancing/undestroyed instances/bad coding, etc. If you really need to increase the limit, you can append a bracket to the struct to increase the instance size. This will enable you to use higher indexes, but it will slow down how quickly retrieving instances is (it will perform an if-then-else check), and it will generate another variable or so. Here is an example:
JASS:
struct Test[16000]
endstruct

In general, don't worry about it. If you think you are going to hit 8192 simultaneous instances, then don't think about it. You risk a lot of efficiency by choosing to allow more instances.
 
Level 4
Joined
Sep 25, 2005
Messages
71
EDIT: Only reason it compiled was because I had the trigger disabled. Youch. I'm not sure the syntax for incorporating stuff within the struct anymore. Augh. I appear to have trainwrecked myself thusly. Ideally, we can talk hashes instead this go around. Scrapping most of it and using the functions you've given has at least let it compile. Questions is how to work with what you've given so far.

So we save the instance, we pass it to the timer... What are some functions for retrieving/utilizing what we passed it from within the timer? Do I use the localblizzard. syntax or something?

Thanks for your patience with me, by the way.
 
Last edited:
In the area where I put "//write your code", you can simply refer to the instance through "this".

So anything you assigned to the struct, you have access to:
JASS:
    private static method BlizzCast takes nothing returns nothing
        local timer t = GetExpiredTimer() // gets the timer that expired
        local thistype this = LoadInteger(hash, 0, GetHandleId(t)) // loads the instance
        // write your code
        call KillUnit(this.attacked)
        call KillUnit(this.attacker) // because killing units is always the best example
        if true then // put some condition to end the timer in place of "true"
            call this.destroy() 
            call RemoveSavedInteger(hash, 0, GetHandleId(t))
            call DestroyTimer(t)
        endif
        set t = null
    endmethod

I declared it as "this", but you can really declare it as whatever you want. You may be able to more easily understand this:
JASS:
    private static method BlizzCast takes nothing returns nothing
        local timer t = GetExpiredTimer() // gets the timer that expired
        local BlizzardData localBlizzard = LoadInteger(hash, 0, GetHandleId(t)) // loads the instance
        // write your code
        call KillUnit(localBlizzard.attacked)
        call KillUnit(localBlizzard.attacker) // because killing units is always the best example
        if true then // put some condition to end the timer in place of "true"
            call localBlizzard.destroy() 
            call RemoveSavedInteger(hash, 0, GetHandleId(t))
            call DestroyTimer(t)
        endif
        set t = null
    endmethod

The nice thing about using "this" as an identifier is that you can refer to the members just with a dot. So instead of:
set this.attacked = null
You could simply put:
set .attacked = null.
 
Level 4
Joined
Sep 25, 2005
Messages
71
Got it. So one it's "loaded," I use the identifier this which was set to the values loaded from the hash table.

I suppose I should get it actually working though, apparently there's some issue with it actually firing right now, and I feel pretty dumb for as much:

I need to "initialize" the scope at the start of the map, and the onInit method is suppose to create a trigger, right?

I tried converting another spell of mine to a scope relatively successfully, and used a function outside the primary struct and it seemed to work. The syntax that worked there is along the lines of:

JASS:
scope LightningCone initializer init

JASS:
private function init takes nothing returns nothing
	local trigger localTrigVar = CreateTrigger()
	call TriggerRegisterAnyUnitEventBJ(localTrigVar, EVENT_PLAYER_UNIT_SPELL_EFFECT)
	call TriggerAddCondition( localTrigVar, Condition(function checkLightningCone))
	set localTrigVar = null
endfunction

And that worked well. For here, am I missing something?

JASS:
scope BlizzardCounter initializer onInit

JASS:
    private static method Check takes nothing returns boolean
        return not IsUnitAlly(GetAttacker(), GetOwningPlayer(GetTriggerUnit()))/*
        */and GetUnitAbilityLevel(GetTriggerUnit(), ABIL_ID) >= 1 /*
        */and ValidGroundTarget(GetAttacker(), GetTriggerUnit())
    endmethod
   
    private static method onInit takes nothing returns nothing
        local trigger localTrigVar = CreateTrigger()
        call TriggerRegisterAnyUnitEventBJ(localTrigVar, EVENT_PLAYER_UNIT_SPELL_EFFECT)
        call TriggerAddCondition(localTrigVar, Filter(function thistype.Check))
        set localTrigVar = null
    endmethod

Does it have to do with how I changed my conditions?
 
JASS:
    private static method Check takes nothing returns boolean
        return not IsUnitAlly(GetAttacker(), GetOwningPlayer(GetTriggerUnit()))/*
        */and GetUnitAbilityLevel(GetTriggerUnit(), ABIL_ID) >= 1 /*
        */and ValidGroundTarget(GetAttacker(), GetTriggerUnit())
    endmethod
   
    private static method onInit takes nothing returns nothing
        local trigger localTrigVar = CreateTrigger()
        call TriggerRegisterAnyUnitEventBJ(localTrigVar, EVENT_PLAYER_UNIT_SPELL_EFFECT)
        call TriggerAddCondition(localTrigVar, Filter(function thistype.Check))
        set localTrigVar = null
    endmethod

Does it have to do with how I changed my conditions?

Yes, it is your condition that is the problem. In the condition, GetAttacker() will return null because the spell is responding to the event, "A unit starts the effect of a spell". You would have to have the event "EVENT_PLAYER_UNIT_ATTACKED" instead for that to work (or modify the filter to use spell target and the caster, or w/e)
 
Level 18
Joined
Sep 14, 2012
Messages
3,413
native SaveInteger takes hashtable table, integer parentKey, integer childKey, integer value returns nothing
Hashtable are like 2D arrays, you have to put two keys. So with saving you have to enter the hastable, which first key, which second key and what integer to save.

native LoadInteger takes hashtable table, integer parentKey, integer childKey returns integer
As above enter the hashtable, which keys.
 
Level 4
Joined
Sep 25, 2005
Messages
71
native SaveInteger takes hashtable table, integer parentKey, integer childKey, integer value returns nothing
Hashtable are like 2D arrays, you have to put two keys. So with saving you have to enter the hastable, which first key, which second key and what integer to save.

native LoadInteger takes hashtable table, integer parentKey, integer childKey returns integer
As above enter the hashtable, which keys.

What exactly are "keys?"

Also, with it set as a private global hashtable, as written, is it correctly instanced?
 
Level 4
Joined
Sep 25, 2005
Messages
71
This appears to work, I'm just wondering: should I do something to the values saved in the hash after I'm done? Should I use a map-wide global variable hashtable or is it a good idea to use it for individual scopes?

JASS:
scope CounternovaInit initializer onInit

    globals
        private constant integer ABIL_ID     = 'A00N'
        private constant integer REAL_ID     = 'A00O'
        private constant real COUNTER_CD     = 5
        private constant real CD_REDUC_LV    = 1.
    endglobals
   
    globals
        private hashtable hash = InitHashtable()
    endglobals

    private struct Counternova
        unit attacked
        unit attacker
        timer cdtimer
        integer level
        real cooldown
        real x
        real y
   
    private static method NovaCast takes nothing returns nothing
        local timer t = GetExpiredTimer()
        local thistype this = LoadInteger(hash, 0, GetHandleId(t))

        if GetUnitAbilityLevel(.attacked, ABIL_ID) == 0 and GetUnitAbilityLevel(.attacked, REAL_ID) >= .level then
            call UnitAddAbility(.attacked,ABIL_ID)     
        endif
        set t = null
        call .destroy()
    endmethod
   
    private static method Counternova takes nothing returns nothing
        local thistype this = thistype.allocate()        
       
        set .attacked = GetTriggerUnit()
        set .attacker = GetAttacker()
        set .cdtimer = CreateTimer()
        set .level = GetUnitAbilityLevel(.attacked, REAL_ID)
        set .cooldown = COUNTER_CD - CD_REDUC_LV * .level
        set .x = GetUnitX(.attacker)
        set .y = GetUnitY(.attacker)
        call UnitRemoveAbility(.attacked, ABIL_ID)
        call CounternovaData.create(.attacked, .attacker, null, .level)  
            
        call SaveInteger(hash, 0, GetHandleId(.cdtimer), this)        
        call TimerStart(.cdtimer, .cooldown, false, function thistype.NovaCast)
    endmethod
   

    private static method Check takes nothing returns boolean
        return not IsUnitAlly(GetAttacker(), GetOwningPlayer(GetTriggerUnit()))/*
        */and GetUnitAbilityLevel(GetTriggerUnit(), ABIL_ID) >= 1 /*
        */and ValidGroundTarget(GetAttacker(), GetTriggerUnit())
    endmethod
   
    private static method onInit takes nothing returns nothing
        local trigger localTrigVar = CreateTrigger()
        call TriggerRegisterAnyUnitEventBJ(localTrigVar, EVENT_PLAYER_UNIT_ATTACKED)
        call TriggerAddCondition(localTrigVar, Filter(function thistype.Check))
        call TriggerAddAction(localTrigVar, function thistype.Counternova)
        set localTrigVar = null
    endmethod
   
    endstruct
   
endscope
 
Status
Not open for further replies.
Top