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

[vJASS] Pushing Information to Callbacks for Spell

Status
Not open for further replies.
Level 2
Joined
Apr 7, 2008
Messages
19
I have a bit of vJASS for a spell I am making.

The spell targets a point and creates a "gravity well" at the location. For as long as the well is alive, it pulls in enemies of the caster.

To implement this, what I am trying to do is start two timers for each well: one for its total duration and one for its periodic interval that will cause the pull-in effect.

The callbacks for the timers are easy enough to handle -- I can use a TimerUtils library I got from wc3c to attach index information to each spell instance's timers and get any required information from there, a la array of structs.

However, I am having trouble in recovering the same information in the callback for a group enumeration. I want to get all of the enemies near the spell's target point. However, to do that, I need to know the casting unit (in order to get its player), and I do not know how to do this.

My original plan was to save the caster in a hashtable under the key represented by the enumeration condition's handle, and in the callback-condition itself, get its own handle which would be the correct key and then from there, getting the caster unit.

Unfortunately, I do not know of any "this" keyword like from Java. I tried a workaround but it looks to me that I'm basically just using a global bucket to solve the problem, which is /not/ what I want because I want to ensure MUI.

How can I push sufficient information to the callback such that the spell is entirely MUI? (Do I even need to do that? Is a temporary global variable enough?) Do I have any alternatives here?

JASS:
scope GravityWell

    globals
        // properties
        private real array rDur // duration based on level. Match with A00E
        private real rInterval = 1/16 // second/frames
        private real rRadius = 300 // area of effect
        private real array rForce // force pulling PER SECOND
        private conditionfunc cf
        // array
        private unit array uCasters
        private location array lPoints // target points
        private timer array tTimers // count duration of well
        private timer array tIntervals // counting intervals
        private integer array iLvls // level of ability at time of cast
        private integer iSize = 0
        // other
        private hashtable htCb = InitHashtable() // hashtable to hold info needed in callbacks
    endglobals
    
    function GravityWell_cbInterval_Condition takes nothing returns boolean
        local unit u = GetFilterUnit()
        local unit cb_uCaster = LoadUnitHandle(htCb, GetHandleId(cf), 0)
        local player p = GetOwningPlayer(cb_uCaster)
        if (IsUnitEnemy(u, p) == false) then
            return false
        endif
        if (GetUnitTypeId(u) == 'u004' or IsUnitType(u, UNIT_TYPE_STRUCTURE)) then // this will work on pretty much anything.
            return false
        endif
        // clean up
        set p = null
        set u = null
        set cb_uCaster = null
        return true
    endfunction
    
    function GravityWell_Init takes nothing returns nothing
    set rDur[1] = 4
    set rDur[2] = 8
    set rDur[3] = 12
    set rForce[1] = 100 / rInterval
    set rForce[2] = 160 / rInterval
    set rForce[3] = 220 / rInterval
    endfunction

    function Trig_GravityWell_Conditions takes nothing returns boolean
        if (GetSpellAbilityId() != 'A00E') then
            return false
        endif
        return true
    endfunction
    
    function GravityWell_cbEnd takes nothing returns nothing
        local integer i = GetTimerData(GetExpiredTimer())
        // clean up
        set uCasters[i] = null
        call ReleaseTimer(tTimers[i])
        call ReleaseTimer(tIntervals[i])
        call RemoveLocation(lPoints[i])
    endfunction
    
    function GravityWell_cbInterval_cbGroup takes nothing returns nothing
        local unit u = GetEnumUnit()
        local location loc = GetUnitLoc(u)
        local location mov // where to move the unit?
        // clean up
        call RemoveLocation(loc)
        set u = null
    endfunction
    
    function GravityWell_cbInterval takes nothing returns nothing
        local integer i = GetTimerData(GetExpiredTimer())
        local group ug = CreateGroup()
        set cf = Condition(function GravityWell_cbInterval_Condition)
        call SaveUnitHandle(htCb, GetHandleId(cf), 0, uCasters[i])
        call GroupEnumUnitsInRangeOfLoc(ug, lPoints[i], rRadius, cf)
        call ForGroup(ug, function GravityWell_cbInterval_cbGroup)
        // clean up
        call DestroyCondition(cf)
        call DestroyGroup(ug)
    endfunction

    function Trig_GravityWell_Actions takes nothing returns nothing
        local integer i = 0
        // Find empty array location.
        loop
            exitwhen (i > C_ARRAYLIMIT) // no free space
            exitwhen (uCasters[i] == null) // found free space
            set i = i + 1
        endloop
        if (i >= iSize and iSize <= C_ARRAYLIMIT) then // index indicates that size must be increased
            set iSize = iSize + 1
        endif
        if (i > C_ARRAYLIMIT) then
            call BJDebugMsg("WARNING: GravityWell: no free space for spell")
        else
            set uCasters[i] = GetTriggerUnit()
            set iLvls[i] = GetUnitAbilityLevel(uCasters[i], 'A00E')
            set lPoints[i] = LocFix(GetSpellTargetLoc())
            set tTimers[i] = NewTimerEx(i)
            set tIntervals[i] = NewTimerEx(i)
            call TimerStart(tTimers[i], rDur[iLvls[i]], false, function GravityWell_cbEnd)
            call TimerStart(tIntervals[i], rInterval, true, function GravityWell_cbInterval)
        endif
    endfunction

endscope
 
Level 26
Joined
Aug 18, 2009
Messages
4,097
You only call the filter/ForGroup function from the timer. There cannot be a recursion, so a single global is enough. Afaik there is no function like GetTriggeringTrigger/GetExpiredTimer for filter/ForGroup that would identify the instance of the thread you are in. However, you cannot have TriggerSleepAction there either, so the execution order is a fixed LIFO. To handle recursion, you can mime that and build a stack.

JASS:
library Group
    globals
        private integer array DATA_STACK
        private integer DATA_STACK_NESTING = -1
    endglobals

    function ForGroupEx_GetTriggerData takes nothing returns integer
        return DATA_STACK[DATA_STACK_NESTING]
    endfunction

    function ForGroupEx takes group g, code action, integer data returns nothing
        set DATA_STACK_NESTING = DATA_STACK_NESTING + 1

        set DATA_STACK[DATA_STACK_NESTING] = data

        call ForGroup(g, action)

        set DATA_STACK_NESTING = DATA_STACK_NESTING - 1
    endfunction
endlibrary

Passing one integer is usually enough because you can/should create a struct instance, which you attach all the other data to, and use the struct as the entry point.
 
Last edited:
Level 2
Joined
Apr 7, 2008
Messages
19
Okay, I'll try this out, thanks. Would I apply a similar stack method to the condition function? What is the keyword thistype?
 
Level 14
Joined
Nov 18, 2007
Messages
816
Why do you want to use two timers per instance? One global timer (for all instances) should be enough for this type of spell.

The standard (ie. thats what everyone has always done) way to push information to GroupEnum... and ForGroup callbacks is via global variables. You can either use a single variable that holds a struct reference, or you use a variable for each bit you want to pass on.

Keep in mind that whatever people tell you, WC3 does not work with threads, not the real thing anyway. What it does is it uses lightweight threads that it switches between on a "real" thread (i think this is referred to as a fiber in some places). You never have parallel code exection for JASS code, so using global varibles to pass information indirectly to callbacks is completely fine.

Personally, i have never needed to nest ForGroup calls and i dont think youd need to for this spell, if im understanding it correctly. Use structs (think Java classes), use a struct instance for every instance of the spell; iterate over every instance of the spell every time a timer signals; do the work that needs to be done for every instance of the spell.



thistype doesnt work inside libraries (and if it does, id be very surprised), only within structs. It is a placeholder for the struct you are currently inside.
 
Level 26
Joined
Aug 18, 2009
Messages
4,097
thistype. was a remnant because I cut this snippet from my own map where I use a struct type for the group.

@Deaod: The nested ForGroups do not have to be directly visible. It can happen when you have an event cycle. It might be better to at least use private globals.
 
Level 2
Joined
Apr 7, 2008
Messages
19
Timer tTimers[n] is needed for the lifetime of the spell and does all of the cleaning at the end. Timer tIntervals[n] is needed to cause the actual effect at 16 FPS. You're right, I could probably use a global timer for the interval effect. However, if the spell is cast during the global timer's cooldown, I can lose an effect tick. Additionally, the timing of the effect will not be consistent, particularly for spells with a lower frequency. That's why I chose to use two timers per instance (and this is for most of my spells): if I want an effect to consistently behave, I need to use two timers for over-time spells.

I might change it to a global timer for counting the intervals in this case since the frequency is so high.

Thanks for your insight, I will simply use the global variable instead of the stack.

EDIT:

Yes, I always use private globals to prevent pollution, although I'm not certain if this matters given Deaod's post. I just don't know for certain if Wc3 actually operates consistently enough for my purposes and I'd rather not risking my spells bugging out and leaking or something.
 
Level 4
Joined
Jul 18, 2009
Messages
81
Here's a template I quickly whipped up. Could be rewritten to use 1 timer + stack + array struct to be more efficient but thought this would be easier for you to read.

JASS:
scope GravityWell initializer Init

globals
    //Spell Config Constants
    private constant integer ABILITY_ID     = 'A00E'
    private constant real    TIMEOUT        = 0.0625
    private constant real    RADIUS         = 300.
    private constant real    DURATION_BASE  = 4.
    private constant real    DURATION_LEVEL = 4.
    private constant real    FORCE_BASE     = 40.
    private constant real    FORCE_LEVEL    = 60.
    
    private group    G = CreateGroup()
    private boolexpr B = null
    private player   P
endglobals

private struct GravityWell
    unit    caster
    real    x
    real    y
    real    dur
    integer level
    
    static method Loop takes nothing returns nothing
        local timer       t    = GetExpiredTimer()
        local GravityWell this = GetTimerData(t)
        local unit        u
        
        set P = GetOwningPlayer(.caster)
        call GroupClear(G)
        call GroupEnumUnitsInRange(G, .x, .y, RADIUS, B)
        
        loop
            set u = FirstOfGroup(G)
          exitwhen u == null
            //Do movement stuff here
            call GroupRemoveUnit(G, u)
        endloop
        
        set .dur = .dur - TIMEOUT
        if .dur <= 0. then
            set .caster = null
            call ReleaseTimer(t)
            call .destroy()
        endif
        
        set t = null
        set u = null
    endmethod
    
    static method create takes unit c, real x, real y returns GravityWell
        local GravityWell this = GravityWell.allocate()
        
        set .caster = c
        set .x      = x
        set .y      = y
        set .level  = GetUnitAbilityLevel(c, ABILITY_ID)
        set .dur    = DURATION_BASE + (DURATION_LEVEL * .level)
        call TimerStart(NewTimerEx(this), TIMEOUT, true, function GravityWell.Loop)
        
        return this
    endmethod
endstruct

private function FilterConditions takes nothing returns boolean
    return IsUnitEnemy(GetFilterUnit(), P) and GetUnitTypeId(GetFilterUnit()) != 'u004' and IsUnitType(GetFilterUnit(), UNIT_TYPE_STRUCTURE) == false
endfunction

private function SpellEffect takes nothing returns boolean
    if GetSpellAbilityId() == ABILITY_ID then
        call GravityWell.create(GetTriggerUnit(), GetSpellTargetX(), GetSpellTargetY())
    endif
    
    return false
endfunction

private function Init takes nothing returns nothing
    local trigger t = CreateTrigger()
    call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
    call TriggerAddCondition(t, Condition(function SpellEffect))
    
    set B = Condition(function FilterConditions)
endfunction

endscope

EDIT: Whoops, wasn't destroying struct instances!
 
Last edited:
Level 2
Joined
Apr 7, 2008
Messages
19
Ohai Fledermaus!

I'm curious: how would you make it faster through 1 timer, stack, and array?

I have yet to use vJASS's structs, but I'm familiar with data structures.

EDIT: As an example, your suggestion makes me think of using one interval timer and an array of integers that count the number of intervals have passed.
 
Level 4
Joined
Jul 18, 2009
Messages
81
It wouldn't be faster (because of the overhead of putting it in the stack and looping through all instances each timeout) (actually, it probably would because it wouldn't have struct allocation calls) but it would run less timers, which is usually worth it for wc3 (not that it really matters these days). Something like this:
JASS:
scope GravityWell initializer Init

globals
    //Spell Config Constants
    private constant integer ABILITY_ID     = 'A00E'
    private constant real    TIMEOUT        = 0.0625
    private constant real    RADIUS         = 300.
    private constant real    DURATION_BASE  = 4.
    private constant real    DURATION_LEVEL = 4.
    private constant real    FORCE_BASE     = 40.
    private constant real    FORCE_LEVEL    = 60.
    
    private group          G     = CreateGroup()
    private boolexpr       B     = null
    private player         P
    //Stack variables
    private integer  array Array
    private integer        Index = 0
endglobals

private struct GravityWell extends array
    unit caster
    real x
    real y
    real dur
    real force
    
    static method Loop takes nothing returns nothing
        local GravityWell this
        local unit        u
        local integer     i    = 0
        
        loop
          exitwhen i >= Index
            //Get the current stack instance
            set this = Array[i]
            
            set P = GetOwningPlayer(.caster)
            call GroupClear(G)
            call GroupEnumUnitsInRange(G, .x, .y, RADIUS, B)
            
            loop
                set u = FirstOfGroup(G)
              exitwhen u == null
                //Do movement stuff here
                call GroupRemoveUnit(G, u)
            endloop
            
            set .dur = .dur - TIMEOUT
            if .dur <= 0. then
                set .caster = null
                
                //Remove from the stack
                set Index = Index - 1
                if Index > 0 then
                    set Array[i] = Array[Index]
                    set i = i - 1
                else
                    call ReleaseTimer(GetExpiredTimer())
                endif
            endif
            
            set i = i + 1
        endloop
        
        set u = null
    endmethod
    
    static method create takes unit c, real x, real y returns GravityWell
        local GravityWell this  = Index
        local integer     level = GetUnitAbilityLevel(c, ABILITY_ID)
        
        set .caster = c
        set .x      = x
        set .y      = y
        set .dur    = DURATION_BASE + (DURATION_LEVEL * level)
        set .force  = FORCE_BASE + (FORCE_LEVEL * level)
        
        //Add the new instance to the stack, if it's the first - start the timer
        if Index == 0 then
            call TimerStart(NewTimer(), TIMEOUT, true, function GravityWell.Loop)
        endif
        set Array[Index] = this
        set Index = Index + 1
        
        return this
    endmethod
endstruct

private function FilterConditions takes nothing returns boolean
    return IsUnitEnemy(GetFilterUnit(), P) and GetUnitTypeId(GetFilterUnit()) != 'u004' and IsUnitType(GetFilterUnit(), UNIT_TYPE_STRUCTURE) == false
endfunction

private function SpellEffect takes nothing returns boolean
    if GetSpellAbilityId() == ABILITY_ID then
        call GravityWell.create(GetTriggerUnit(), GetSpellTargetX(), GetSpellTargetY())
    endif
    
    return false
endfunction

private function Init takes nothing returns nothing
    local trigger t = CreateTrigger()
    call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
    call TriggerAddCondition(t, Condition(function SpellEffect))
    
    set B = Condition(function FilterConditions)
endfunction

endscope

struct extends array is essentially the same as creating a bunch of arrays yourself with the member names (caster, x, y, dur, force) but it gives a nicer syntax (more info incase you're interested).
 
Last edited:
Status
Not open for further replies.
Top