[Spell] Auto cast spell on valid targets JASS

Level 7
Joined
Nov 3, 2015
Messages
78
Hey. I'm trying to make a unit autocast Unholy Frenzy on valid targets. But it never casts anything. Can you help?

Valid targets are friendly ancients.
1764793812612.png


This is a valid target
1764793841992.png


vJASS:
scope Freakout

globals
    private constant integer ID     = 'A0GS'
    private constant integer BUFFID = 'B04B'
   
    private constant real INTERVAL  = 0.50
    private constant real AI_RANGE  = 600.00
   
    private constant integer ORDERID = OrderId("unholyfrenzy")
   
    private timer SpellTimer = CreateTimer()
   
    private group targets   = CreateGroup()
   
    private group gTempBuff  = CreateGroup()
    private group gTempUnits = CreateGroup()
    private group gTempAI    = CreateGroup()
   
    private real array damage
endglobals

//! runtextmacro PUI_PROPERTY("private", "integer", "Level", "0")

private function PERIODIC takes nothing returns nothing
    local unit u
    local unit caster
    local unit ally
    local integer lvl


    set gTempBuff = CopyGroup(targets)
    loop
        set u = FirstOfGroup(gTempBuff)
        exitwhen u == null
        set lvl = Level[u]
        if GetUnitAbilityLevel(u, BUFFID) > 0 then
            call UnitDamageTarget(u, u, damage[lvl] * INTERVAL, true, false, ATTACK_TYPE_HERO, DAMAGE_TYPE_UNIVERSAL, WEAPON_TYPE_WHOKNOWS)
        else
            call GroupRemoveUnit(targets, u)
        endif
        call GroupRemoveUnit(gTempBuff, u)
    endloop

    call GroupEnumUnitsInRect(gTempUnits, bj_mapInitialPlayableArea, null)
    loop
        set caster = FirstOfGroup(gTempUnits)
        exitwhen caster == null
        call GroupRemoveUnit(gTempUnits, caster)

        if GetUnitAbilityLevel(caster, ID) > 0 and GetUnitState(caster, UNIT_STATE_LIFE) > 0.405 then
            call GroupEnumUnitsInRange(gTempAI, GetUnitX(caster), GetUnitY(caster), AI_RANGE, null)
            loop
                set ally = FirstOfGroup(gTempAI)
                exitwhen ally == null
                call GroupRemoveUnit(gTempAI, ally)

                if IsUnitAlly(ally, GetOwningPlayer(caster)) and GetUnitState(ally, UNIT_STATE_LIFE) > 0.405 and IsUnitType(ally, UNIT_TYPE_ANCIENT) and GetUnitAbilityLevel(ally, BUFFID) == 0 and ally != caster then
                    call IssueTargetOrderById(caster, ORDERID, ally)
                    exitwhen true
                endif
            endloop
            // endif
        endif
    endloop

    set u = null
    set caster = null
    set ally = null
endfunction

private function Conditions takes nothing returns boolean
    if GetSpellAbilityId() == ID then
        call GroupAddUnit(targets, GetSpellTargetUnit())
        set Level[GetSpellTargetUnit()] = GetUnitAbilityLevel(GetTriggerUnit(), ID)
    endif
    return false
endfunction

function TriggerStartFreakOut takes nothing returns nothing
    local trigger t = CreateTrigger()

    call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
    call TriggerAddCondition(t, Condition(function Conditions))

    call TimerStart(SpellTimer, INTERVAL, true, function PERIODIC)

    set damage[1]  = 20
    set damage[2]  = 40
    set damage[3]  = 75
    set damage[4]  = 125
    set damage[5]  = 200
    set damage[6]  = 350
    set damage[7]  = 550
    set damage[8]  = 700
    set damage[9]  = 1200
    set damage[10] = 2000

    set t = null
endfunction

endscope
 
Last edited:
Oh I see, Unholy Frenzy isn't even an Autocast ability. You just want the AI to cast it.

Changing the Targets Allowed to only work on Ancients works fine, but that doesn't mean the AI will understand that.

Also, in your picture the Unit is under your control, meaning it's not owned by a Computer and therefore doesn't have any concept of AI.
 
Last edited:
Oh I see, Unholy Frenzy isn't even an Autocast ability. You just want the AI to cast it.

Changing the Targets Allowed to only work on Ancients works fine, but that doesn't mean the AI will understand that.

Also, in your picture the Unit is under your control, meaning it's not owned by a Computer and therefore doesn't have any concept of "AI".
Lol. So pretty much the whole trigger is wrong?
 
Lol. So pretty much the whole trigger is wrong?
No, actually I missed that you were trying to use PERIODIC to cast the ability as well. I was under the impression that either the Targets Allowed was wrong or that you wanted a Computer AI to cast it. Disregard most of what I said before.

Add some debug messages to see if the Conditions are even being met:
vJASS:
                call DisplayTextToPlayer(GetLocalPlayer(), 0, 0, "A")
                if IsUnitAlly(ally, GetOwningPlayer(caster)) and GetUnitState(ally, UNIT_STATE_LIFE) > 0.405 and IsUnitType(ally, UNIT_TYPE_ANCIENT) and GetUnitAbilityLevel(ally, BUFFID) == 0 and ally != caster then
                    call DisplayTextToPlayer(GetLocalPlayer(), 0, 0, "B")
                    call IssueTargetOrderById(caster, ORDERID, ally)
                    exitwhen true
                endif
Do that throughout all of the code, anywhere that things could fail.

Also, I highly recommend storing all units with the Freak Out ability in their own managed Unit Group rather than having to find them over and over again. But you can worry about that later, if at all.
 
I think the issue is your Conditions function never returns True.

I'm also unfamiliar with how Level is being used, never messed with text macros.

Anyway, you don't need to use this code, but here's how I would do it (requires a Unit Indexer):
vJASS:
scope Freakout
    globals
        private constant integer ID      = 'A0GS'
        private constant integer BUFFID  = 'B04B'
        private constant real INTERVAL   = 0.50
        private constant real AI_RANGE   = 600.00
        private constant integer ORDERID = OrderId("unholyfrenzy")

        // persistent groups
        private group gCasters = null
        private group gBuffed  = null

        // temporary group used for EnumUnitsInRange
        private group gTempAI  = null

        // spell data
        private timer spellTimer = null
        private real array spellDamage
        private integer array spellLevel // Currently relies on a Unit Indexer
    endglobals

    private function IsValidTarget takes unit caster, unit ally returns boolean
        if IsUnitType(ally, UNIT_TYPE_ANCIENT) and IsUnitAlly(ally, GetOwningPlayer(caster)) and GetUnitState(ally, UNIT_STATE_LIFE) > 0.405 and GetUnitAbilityLevel(ally, BUFFID) == 0 and ally != caster then
            return true
        endif
        return false
    endfunction

    private function CasterCallback takes nothing returns nothing
        local unit caster = GetEnumUnit()
        local unit ally = null

        call GroupEnumUnitsInRange(gTempAI, GetUnitX(caster), GetUnitY(caster), AI_RANGE, null)
        loop
            set ally = FirstOfGroup(gTempAI)
            exitwhen ally == null
            call GroupRemoveUnit(gTempAI, ally)

            // Only works on allied ancients
            if IsValidTarget(caster, ally) then
                call IssueTargetOrderById(caster, ORDERID, ally)
                exitwhen true
            endif
        endloop

        set caster = null
    endfunction

    private function BuffCallback takes nothing returns nothing
        local unit target = GetEnumUnit()
        local integer lvl

        // damage them or remove them
        if GetUnitAbilityLevel(target, BUFFID) > 0 then
            set lvl = spellLevel[GetUnitUserData(target)]
            call UnitDamageTarget(target, target, spellDamage[lvl] * INTERVAL, true, false, ATTACK_TYPE_HERO, DAMAGE_TYPE_UNIVERSAL, WEAPON_TYPE_WHOKNOWS)
        else
            call GroupRemoveUnit(gBuffed, target)
        endif
      
        set target = null
    endfunction

    private function TimerCallback takes nothing returns nothing
        call ForGroup(gBuffed, function BuffCallback)
        call ForGroup(gCasters, function CasterCallback)
    endfunction

    private function Conditions takes nothing returns boolean
        local unit caster = null
        local unit target = null

        if GetSpellAbilityId() == ID then
            set caster = GetTriggerUnit()
            set target = GetSpellTargetUnit()
            call GroupAddUnit(gCasters, caster)
            call GroupAddUnit(gBuffed, target)
            set spellLevel[GetUnitUserData(target)] = GetUnitAbilityLevel(caster, ID)
            set caster = null
            set target = null
            return true
        endif

        return false
    endfunction

    function TriggerStartFreakOut takes nothing returns nothing
        local trigger t = CreateTrigger()

        // create timer and groups
        set spellTimer = CreateTimer()
        set gCasters   = CreateGroup()
        set gBuffed    = CreateGroup()
        set gTempAI    = CreateGroup()

        call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
        call TriggerAddCondition(t, Condition(function Conditions))

        call TimerStart(spellTimer, INTERVAL, true, function TimerCallback)

        // initialize damage table
        set spellDamage[1]  = 20
        set spellDamage[2]  = 40
        set spellDamage[3]  = 75
        set spellDamage[4]  = 125
        set spellDamage[5]  = 200
        set spellDamage[6]  = 350
        set spellDamage[7]  = 550
        set spellDamage[8]  = 700
        set spellDamage[9]  = 1200
        set spellDamage[10] = 2000

        set t = null
    endfunction

endscope
^ You would want to Remove casters when they die. (If they're revivable Heroes then Add/Remove as needed).
 
Last edited:
I think the issue is your Conditions function never returns True.

I'm also unfamiliar with how Level is being used, never messed with text macros.

Anyway, you don't need to use this code, but here's how I would do it (requires a Unit Indexer):
vJASS:
scope Freakout
    globals
        private constant integer ID      = 'A0GS'
        private constant integer BUFFID  = 'B04B'
        private constant real INTERVAL   = 0.50
        private constant real AI_RANGE   = 600.00
        private constant integer ORDERID = OrderId("unholyfrenzy")

        // persistent groups
        private group gCasters = null
        private group gBuffed  = null

        // temporary group used for EnumUnitsInRange
        private group gTempAI  = null

        // spell data
        private timer spellTimer = null
        private real array spellDamage
        private integer array spellLevel // Currently relies on a Unit Indexer
    endglobals

    private function IsValidTarget takes unit caster, unit ally returns boolean
        if IsUnitType(ally, UNIT_TYPE_ANCIENT) and IsUnitAlly(ally, GetOwningPlayer(caster)) and GetUnitState(ally, UNIT_STATE_LIFE) > 0.405 and GetUnitAbilityLevel(ally, BUFFID) == 0 and ally != caster then
            return true
        endif
        return false
    endfunction

    private function CasterCallback takes nothing returns nothing
        local unit caster = GetEnumUnit()
        local unit ally = null

        call GroupEnumUnitsInRange(gTempAI, GetUnitX(caster), GetUnitY(caster), AI_RANGE, null)
        loop
            set ally = FirstOfGroup(gTempAI)
            exitwhen ally == null
            call GroupRemoveUnit(gTempAI, ally)

            // Only works on allied ancients
            if IsValidTarget(caster, ally) then
                call IssueTargetOrderById(caster, ORDERID, ally)
                exitwhen true
            endif
        endloop

        set caster = null
    endfunction

    private function BuffCallback takes nothing returns nothing
        local unit target = GetEnumUnit()
        local integer lvl

        // damage them or remove them
        if GetUnitAbilityLevel(target, BUFFID) > 0 then
            set lvl = spellLevel[GetUnitUserData(target)]
            call UnitDamageTarget(target, target, spellDamage[lvl] * INTERVAL, true, false, ATTACK_TYPE_HERO, DAMAGE_TYPE_UNIVERSAL, WEAPON_TYPE_WHOKNOWS)
        else
            call GroupRemoveUnitSimple(target, gBuffed)
        endif
      
        set target = null
    endfunction

    private function TimerCallback takes nothing returns nothing
        call ForGroup(gBuffed, function BuffCallback)
        call ForGroup(gCasters, function CasterCallback)
    endfunction

    private function Conditions takes nothing returns boolean
        local unit caster = null
        local unit target = null

        if GetSpellAbilityId() == ID then
            set caster = GetTriggerUnit()
            set target = GetSpellTargetUnit()
            call GroupAddUnit(gCasters, caster)
            call GroupAddUnit(gBuffed, target)
            set spellLevel[GetUnitUserData(target)] = GetUnitAbilityLevel(caster, ID)
            set caster = null
            set target = null
            return true
        endif

        return false
    endfunction

    function TriggerStartFreakOut takes nothing returns nothing
        local trigger t = CreateTrigger()

        // create timer and groups
        set spellTimer = CreateTimer()
        set gCasters   = CreateGroup()
        set gBuffed    = CreateGroup()
        set gTempAI    = CreateGroup()

        call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
        call TriggerAddCondition(t, Condition(function Conditions))

        call TimerStart(spellTimer, INTERVAL, true, function TimerCallback)

        // initialize damage table
        set spellDamage[1]  = 20
        set spellDamage[2]  = 40
        set spellDamage[3]  = 75
        set spellDamage[4]  = 125
        set spellDamage[5]  = 200
        set spellDamage[6]  = 350
        set spellDamage[7]  = 550
        set spellDamage[8]  = 700
        set spellDamage[9]  = 1200
        set spellDamage[10] = 2000

        set t = null
    endfunction

endscope
^ You would want to Remove casters when they die. (If they're heroes then Add/Remove as needed).
Thank you so much for writing this! I appreciate you taking your time.

I tried your code and I don't really understand unit indexers. So I just gave it to ChatGPT (I know this makes me look dumb) and asked for a PUI friendly version. But it still doesn't cast the spell.
vJASS:
scope Freakout
    globals
        private constant integer ID      = 'A0GS'
        private constant integer BUFFID  = 'B04B'
        private constant real INTERVAL   = 0.50
        private constant real AI_RANGE   = 600.00
        private constant integer ORDERID = OrderId("unholyfrenzy")

        private group gCasters = null
        private group gBuffed  = null
        private group gTempAI  = null

        private timer spellTimer = null
        private real array spellDamage
    endglobals

    //! runtextmacro PUI_PROPERTY("private", "integer", "Level", "0")

    // Adjust this to match Freak Out's Targets Allowed
    private function IsValidTarget takes unit caster, unit ally returns boolean
        if IsUnitAlly(ally, GetOwningPlayer(caster)) and GetUnitState(ally, UNIT_STATE_LIFE) > 0.405 and GetUnitAbilityLevel(ally, BUFFID) == 0 and ally != caster and IsUnitType(ally, UNIT_TYPE_ANCIENT) then
            return true
        endif
        return false
    endfunction

    private function CasterCallback takes nothing returns nothing
        local unit caster = GetEnumUnit()
        local unit ally = null

        // Clean out dead casters / casters that lost the ability
        if GetUnitState(caster, UNIT_STATE_LIFE) <= 0.405 or GetUnitAbilityLevel(caster, ID) == 0 then
            call GroupRemoveUnitSimple(caster, gCasters)
            set caster = null
            set ally = null
            return
        endif

        call GroupEnumUnitsInRange(gTempAI, GetUnitX(caster), GetUnitY(caster), AI_RANGE, null)
        loop
            set ally = FirstOfGroup(gTempAI)
            exitwhen ally == null
            call GroupRemoveUnit(gTempAI, ally)

            if IsValidTarget(caster, ally) then
                call IssueTargetOrderById(caster, ORDERID, ally)
                exitwhen true
            endif
        endloop

        set caster = null
        set ally = null
    endfunction

    private function BuffCallback takes nothing returns nothing
        local unit target = GetEnumUnit()
        local integer lvl

        if GetUnitAbilityLevel(target, BUFFID) > 0 then
            set lvl = Level[target] // PUI: no Unit Indexer needed
            call UnitDamageTarget(target, target, spellDamage[lvl] * INTERVAL, true, false, ATTACK_TYPE_HERO, DAMAGE_TYPE_UNIVERSAL, WEAPON_TYPE_WHOKNOWS)
        else
            call GroupRemoveUnitSimple(target, gBuffed)
        endif

        set target = null
    endfunction

    private function TimerCallback takes nothing returns nothing
        call ForGroup(gBuffed, function BuffCallback)
        call ForGroup(gCasters, function CasterCallback)
    endfunction

    private function Conditions takes nothing returns boolean
        local unit caster = null
        local unit target = null

        if GetSpellAbilityId() == ID then
            set caster = GetTriggerUnit()
            set target = GetSpellTargetUnit()
            call GroupAddUnit(gCasters, caster)
            call GroupAddUnit(gBuffed, target)
            set Level[target] = GetUnitAbilityLevel(caster, ID) // store level via PUI
            set caster = null
            set target = null
            return true
        endif

        return false
    endfunction

    function TriggerStartFreakOut takes nothing returns nothing
        local trigger t = CreateTrigger()

        set spellTimer = CreateTimer()
        set gCasters   = CreateGroup()
        set gBuffed    = CreateGroup()
        set gTempAI    = CreateGroup()

        call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
        call TriggerAddCondition(t, Condition(function Conditions))

        call TimerStart(spellTimer, INTERVAL, true, function TimerCallback)

        set spellDamage[1]  = 20
        set spellDamage[2]  = 40
        set spellDamage[3]  = 75
        set spellDamage[4]  = 125
        set spellDamage[5]  = 200
        set spellDamage[6]  = 350
        set spellDamage[7]  = 550
        set spellDamage[8]  = 700
        set spellDamage[9]  = 1200
        set spellDamage[10] = 2000

        set t = null
    endfunction
endscope
 
You would import a Unit Indexer system into your map, there's a bunch of them on Hive. Assuming the system is designed well, it should automatically work in the background without any interference.

All it does is assign new unit's a unique Custom Value as they come into play, which is referred to as "User Data" in code form.
So GetUnitUserData() is the same as getting the unit's Custom Value.

This one works well:
 
I've already got Auto Indexer in the map. I didn't put it there originally.
But I don't reallu understand how to utilize it in this trigger.

You would import a Unit Indexer system into your map, there's a bunch of them on Hive. Assuming the system is designed well, it should automatically work in the background without any interference.

All it does is assign new unit's a unique Custom Value as they come into play, which is referred to as "User Data" in code form.
So GetUnitUserData() is the same as getting the unit's Custom Value.

This one works well:

And this trigger is also active. I don't know anything about any of this. How to use it, or to avoid messing things up.

vJASS:
library AutoIndex
//===========================================================================
// Information:
//==============
//
//     AutoIndex is a very simple script to utilize. Just call GetUnitId(unit) to
// get get the unique value assigned to a particular unit. AutoIndex differs from
// other unit indexing libraries because it automatically assigns an ID to each
// unit as it enters the map, and automatically frees that ID as the unit leaves
// the map. This gives you several advantages as the user:
//   -The GetUnitId function inlines directly to a GetUnitUserData call (or a
//    LoadInteger call if the UseUnitUserData constant is set to false.)
//   -You don't need to manually free IDs as units leave the map.
//   -Detecting removing units to free their indexes is O(1), and less costly
//    performance-wise than a timer scanning the map for removed units.
//
//  If you turn on debug mode, AutoIndex will become slower, but it will be able
//  to display several helpful error messages. It can detect the following issues:
//   -Passing a removed or decayed unit to GetUnitId
//   -Code outside of AutoIndex has overwritten a unit's UserData value.
//   -GetUnitId was used on a filtered unit (a unit you don't want indexed).
//
//     AutoIndex uses UnitUserData by default. If something else in your map
// would conflict with that, you can set the UseUnitUserData configuration
// constant to false, and a hashtable will be used instead. Note that hash-
// tables are about 60% slower.
//
//     AutoIndex provides events upon indexing or deindexing units. This
// effectively allows you to notice when units enter or leave the game, and
// handle the creation or destruction of attached data or other things. Also
// included is the AutoStruct module, which automatically creates and destroys
// struct instances associated with units as they enter and leave the game.
//
//===========================================================================
// How to install AutoIndex:
//===========================
//
// 1.) Copy and paste this script into your map.
// 2.) Save it to allow the ObjectMerger macro to generate the "Leave Detect"
//     ability for you. Close and re-open the map. After that, disable the macro
//     to prevent the save delay.
//
//===========================================================================
// How to use AutoIndex:
//=======================
//
//     So you can get a unique integer for each unit, but how do you use that to
// attach data to a unit? GetUnitId will always return a number in the range of
// 1-8190. This means it can be used as an array index, as demonstrated below:
//
// globals
//     integer array IntegerData
//     real array RealData
//     SomeStruct array SomeStructData
// englobals
//
// function Example takes nothing returns nothing
//     local unit u = CreateUnit(...)
//     local integer id = GetUnitId(u)
//         //You now have a unique index for the unit, so you can
//         //attach or retrieve data about the unit using arrays.
//         set IntegerData[id] = 5
//         set RealData[id] = 25.0
//         set SomeStructData[id] = SomeStruct.create()
//         //If you have access to the same unit in another function, you can
//         //retrieve the data by using GetUnitId() and reading the arrays.
// endfunction
//
//     The UnitFilter function in the configuration section is provided so that
// you can make AutoIndex completely ignore certain unit-types. Ignored units
// won't be indexed or fire indexed/deindexed events. You may want to filter out
// dummy casters or system-private units, especially ones that use UnitUserData
// internally. xe dummy units are automatically filtered.
//
//===========================================================================
// How to use OnUnitIndexed / OnUnitDeindexed:
//=============================================
//
//     AutoIndex will fire the OnUnitIndexed event when a unit enters the map,
// and the OnUnitDeindexed event when a unit leaves the map. Functions used
// as events must take a unit and return nothing. An example is given below:
//
// function UnitEntersMap takes unit u returns nothing
//     call BJDebugMsg(GetUnitName(u)+" was indexed with the ID "+I2S(GetUnitId(u)))
// endfunction
//
// function UnitLeavesMap takes unit u returns nothing
//     call BJDebugMsg(GetUnitName(u)+" was deindexed with the ID "+I2S(GetUnitId(u)))
// endfunction
//
// function Init takes nothing returns nothing
//     call OnUnitIndexed(UnitEntersMap)
//     call OnUnitDeindexed(UnitLeavesMap)
// endfunction
//
//     As you can see, it works perfectly fine to call GetUnitId() on a unit
// during either of these events.
//
//     If you call OnUnitIndexed during map initialization, every existing
// unit will be considered as entering the map. This saves you from needing
// to manually enumerate preplaced units (or units created by initialization
// code that ran before OnUnitIndexed was called).
//
//     OnUnitDeindexed runs while a unit still exists, which means you can
// still do things such as destroy special effects attached to the unit.
// The unit will cease to exist immediately after the event is over.
//
//===========================================================================
// AutoIndex API:
//================
//
// GetUnitId(unit) -> integer
//   This function returns a unique ID in the range of 1-8190 for the
//   specified unit. Ruturns 0 if a null unit was passed. This function
//   inlines directly to GetUnitUserData or LoadInteger if debug mode
//   is disabled. If debug mode is enabled, this function will print
//   an error message when passed a decayed or filtered unit.
//
// OnUnitIndexed(IndexFunc)
//   This function accepts an IndexFunc, which must take a unit and
//   return nothing. The IndexFunc will be fired instantly whenever
//   a unit enters the map. You may use GetUnitId on the unit. When
//   you call this function during map initialization, every existing
//   unit will be considered as entering the map.
//
// OnUnitDeindexed(IndexFunc)
//   Same as above, but runs whenever a unit is leaving the map. When
//   this event runs, the unit still exists, but it will cease to exist
//   as soon as the event ends. You may use GetUnitId on the unit.
//
//===========================================================================
// How to use the AutoStruct module:
//===================================
//
//     The AutoStruct module allows you to automatically create and destroy
// struct instances as units enter and leave the game. An instance of the
// implementing struct will be created each time a unit enters the game,
// and destroyed when that unit leaves the game. (You cannot create or dest-
// roy instances manually.) This means that you should consider each inst-
// ance of an AutoStruct to be "owned by" a specific unit.
//
//   AutoStruct restrictions:
//   -You may not implement AutoStruct in any struct that declares its
//    own create, destroy, or operator[] methods.
//
//   -You may not manually create or destroy structs implementing AutoStruct.
//    Creation and destruction are handled automatically as units enter/leave.
//
//   -AutoStruct may be implemented in structs that extend interfaces or
//    other structs, but only if the allocate() method takes no parameters.
//    (The restriction of allocate() taking no parameters may be changed soon.)
//
//   -AutoStruct will not work with structs that extend array, since they
//    can't be created or destroyed.
//
//  AutoStruct features:
//   -An instance of the implementing struct will be created each time a
//    unit enters the game. That instance will be destroyed when that unit
//    leaves the game. The struct will always exist while the unit does.
//
//   -You can retrieve an AutoStruct from a unit by using the syntax:
//      local StructName mystruct = StructName[unit]
//      //This inlines to a GetUnitId() call + array lookup.
//
//   -You can refer to the unit that owns the instance by the member "me":
//      //Outside of the struct:  call BJDebugMsg(GetUnitName(mystruct.me))
//      //From within the struct: call BJDebugMsg(GetUnitName(me))
//
//   -You can use the optional methods onCreate and onDestroy to notice when
//    instances of a struct implementing AutoStruct are created and destroyed.
//    This is equivalent to detecting a unit entering and leaving the game.
//      struct Example
//          private method onCreate takes nothing returns nothing
//              call BJDebugMsg(GetUnitName(me)+" has entered the game!")
//          endmethod
//          private method onDestroy takes nothing returns nothing
//              call BJDebugMsg(GetUnitName(me)+" has left the game!")
//          endmethod
//          implement AutoStruct
//      endstruct
//
//   -You can filter which units will recieve an AutoStruct by using the
//    optional static method createFilter. The createFiler method must take
//    a unit parameter and return a boolean (true if created, false if not).
//      struct Example
//          private static method createFiler takes unit u returns boolean
//              return GetUnitTypeId(u) == 'hfoo' //Only Footmen will recieve this AutoStruct.
//          endmethod
//          implement AutoStruct
//      endstruct
//
//===========================================================================
// Configuration:
//================

// external ObjectMerger w3a Adef lvdt anam "Leave Detect" aart "" arac 0
//Save your map with this Object Merger call enabled, then close and reopen your
//map. Disable it by removing the exclamation to remove the delay while saving.

globals
    private constant integer LeaveDetectAbilityID = 'lvdt'
    //This rawcode must match the parameter after "Adef" in the
    //ObjectMergermacro above. You can change both if you want.
   
    private constant boolean UseUnitUserData = true
    //If this is set to true, UnitUserData will be used. You should only set
    //this to false if something else in your map already uses UnitUserData.
    //A hashtable will be used instead, but it is about 60% slower.
endglobals

private function UnitFilter takes unit u returns boolean
    return true
endfunction
//Make this function return false for any unit-types you wish to be ignored.
//Ignored units won't be indexed or fire OnUnitIndexed/OnUnitDeindexed
//events. Use the unit u parameter to refer to the unit being filtered.
//You do not need to filter out xe dummy units; they are already filtered.

//===========================================================================
// AutoStruct module:
//====================

function interface AutoStructCreator takes unit u returns integer
function interface AutoStructDestroyer takes unit u returns nothing

module AutoStruct
    private static thistype array data
    unit me //The unit that "owns" this struct instance is referred to as "me".
   
    static method operator [] takes unit u returns thistype
        return data[GetUnitId(u)] //Return the struct instance associated with the unit.
    endmethod
   
    private static method create takes unit u returns thistype
        local thistype this
            static if thistype.createFilter.exists then //If the createFiler exists...
                if not createFilter(u) then //If the unit fails the createFilter...
                    return 0 //Don't allocate a struct for this unit.
                endif
            endif
            set this = allocate() //Allocate the struct.
            set me = u //Assign the "me" unit of this struct to the entering unit.
            set data[GetUnitId(u)] = this //Attach this instance to the unit.
            static if thistype.onCreate.exists then //If onCreate exists...
                call onCreate() //Call the onCreate() method for this struct.
            endif
        return this
    endmethod
    //create is private; the user doesn't create AutoStructs. They are created
    //automatically when a unit enters the game and passes the createFilter.
   
    private method destroy takes nothing returns nothing
    endmethod
    //destroy is private; the user doesn't destroy AutoStructs. They are destroyed
    //automatically when their corresponding units leave the game.
   
    private static method destroyer takes unit u returns nothing
        local thistype this = thistype[u] //Get the instance of the struct for this unit.
            if this != 0 then //If it has an instance...
                call deallocate() //Deallocate it, calling onDestroy.
                set .data[GetUnitId(me)] = 0 //Null the unit's associated struct instance.
                set me = null //Null the unit reference.
            endif
    endmethod
   
    private static method onInit takes nothing returns nothing
        call AutoIndex.addAutoStruct(thistype.create, thistype.destroyer)
        //Pass pointers to the create and destroyer functions to AutoIndex, so
        //that it can create/destroy instances as units are indexed/deindexed.
    endmethod
endmodule

//===========================================================================
// AutoIndex struct:
//===================

hook RemoveUnit AutoIndex.hook_RemoveUnit
hook ReplaceUnitBJ AutoIndex.hook_ReplaceUnitBJ
debug hook SetUnitUserData AutoIndex.hook_SetUnitUserData

struct AutoIndex
    private static trigger   enter      = CreateTrigger()
    private static trigger   order      = CreateTrigger()
    private static trigger   creepdeath = CreateTrigger()
    private static group     preplaced  = CreateGroup()
    private static timer     allowdecay = CreateTimer()
    private static hashtable ht

    private static boolean array dead
    private static boolean array summoned
    private static boolean array animated
    private static boolean array nodecay
    private static boolean array removing
   
    private static IndexFunc array indexfuncs
    private static integer indexfuncs_n = -1
    private static IndexFunc array deindexfuncs
    private static integer deindexfuncs_n = -1
    private static IndexFunc indexfunc
   
    private static AutoStructCreator array creators
    private static AutoStructDestroyer array destroyers
    private static integer autostructs_n = -1
   
    private static unit array allowdecayunit
    private static integer allowdecay_n = -1
   
    private static boolean duringinit = true
    private static boolean array altered
    private static unit array idunit
   
    //===========================================================================

    static method getIndex takes unit u returns integer
        static if UseUnitUserData then
            return GetUnitUserData(u)
        else
            return LoadInteger(ht, 0, GetHandleId(u))
        endif
    endmethod
    //Resolves to an inlinable one-liner after the static if.
   
    static method getIndexDebug takes unit u returns integer
            if GetUnitTypeId(u) == 0 then
                call BJDebugMsg("AutoIndex error: Removed or decayed unit passed to GetUnitId.")
            elseif idunit[getIndex(u)] != u and GetIssuedOrderId() != 852056 then
                call BJDebugMsg("AutoIndex error: "+GetUnitName(u)+" is a filtered unit.")
            endif
        return getIndex(u)
    endmethod
    //If debug mode is enabled, use the getIndex method that shows errors.
   
    static method setIndex takes unit u, integer index returns nothing
        static if UseUnitUserData then
            call SetUnitUserData(u, index)
        else
            call SaveInteger(ht, 0, GetHandleId(u), index)
        endif
    endmethod
    //Resolves to an inlinable one-liner after the static if.
   
    static method isUnitAnimateDead takes unit u returns boolean
        return animated[getIndex(u)]
    endmethod
    //Don't use this; use IsUnitAnimateDead from AutoEvents instead.
   
    //===========================================================================
   
    private static method onUnitIndexed_sub takes nothing returns nothing
        call indexfunc.evaluate(GetEnumUnit())
    endmethod
    static method onUnitIndexed takes IndexFunc func returns nothing
        set indexfuncs_n = indexfuncs_n + 1
        set indexfuncs[indexfuncs_n] = func
        if duringinit then
            set indexfunc = func
            //During initialization, evaluate the indexfunc for every preplaced unit.
            call ForGroup(preplaced, function AutoIndex.onUnitIndexed_sub)
        endif
    endmethod
   
    static method onUnitDeindexed takes IndexFunc func returns nothing
        set deindexfuncs_n = deindexfuncs_n + 1
        set deindexfuncs[deindexfuncs_n] = func
    endmethod
   
    static method addAutoStruct takes AutoStructCreator creator, AutoStructDestroyer destroyer returns nothing
        set autostructs_n = autostructs_n + 1
        set creators[autostructs_n] = creator
        set destroyers[autostructs_n] = destroyer
    endmethod
   
    //===========================================================================
   
    private static method hook_RemoveUnit takes unit whichUnit returns nothing
        set removing[getIndex(whichUnit)] = true
    endmethod
    private static method hook_ReplaceUnitBJ takes unit whichUnit, integer newUnitId, integer unitStateMethod returns nothing
        set removing[getIndex(whichUnit)] = true
    endmethod
    //Intercepts whenever RemoveUnit or ReplaceUnitBJ is called and sets a flag.
   
    private static method hook_SetUnitUserData takes unit whichUnit, integer data returns nothing
        static if UseUnitUserData then
            if idunit[getIndex(whichUnit)] == whichUnit then
                if getIndex(whichUnit) == data then
                    call BJDebugMsg("AutoIndex error: Code outside AutoIndex attempted to alter "+GetUnitName(whichUnit)+"'s index.")
                else
                    call BJDebugMsg("AutoIndex error: Code outside AutoIndex altered "+GetUnitName(whichUnit)+"'s index.")
                    if idunit[data] != null then
                        call BJDebugMsg("AutoIndex error: "+GetUnitName(whichUnit)+" and "+GetUnitName(idunit[data])+" now have the same index.")
                    endif
                    set altered[data] = true
                endif
            endif
        endif
    endmethod
    //In debug mode, intercepts whenever SetUnitUserData is used on an indexed unit.
    //Displays an error message if outside code tries to alter a unit's index.
   
    //===========================================================================
   
    private static method allowDecay takes nothing returns nothing
        local integer n = allowdecay_n
            loop
                exitwhen n < 0
                set nodecay[getIndex(allowdecayunit[n])] = false
                set allowdecayunit[n] = null
                set n = n - 1
            endloop
            set allowdecay_n = -1
    endmethod
    //Iterate through all the units in the stack and allow them to decay again.
   
    private static method detectStatus takes nothing returns boolean
        local unit u = GetTriggerUnit()
        local integer index = getIndex(u)
        local integer n
           
            if idunit[index] == u then //Ignore non-indexed units.
                if not IsUnitType(u, UNIT_TYPE_DEAD) then
               
                    if dead[index] then //The unit was dead, but now it's alive.
                        set dead[index] = false //The unit has been resurrected.
                       
                        //! runtextmacro optional RunAutoEvent("Resurrect")
                        //If AutoEvents is in the map, run the resurrection events.
                           
                        if IsUnitType(u, UNIT_TYPE_SUMMONED) and not summoned[index] then
                            set summoned[index] = true //If the unit gained the summoned flag,
                            set animated[index] = true //it's been raised with Animate Dead.
                               
                            //! runtextmacro optional RunAutoEvent("AnimateDead")
                            //If AutoEvents is in the map, run the Animate Dead events.
                        endif
                    endif
                else
               
                    if not removing[index] and not dead[index] and not animated[index] then
                        set dead[index] = true    //The unit was alive, but now it's dead.
                        set nodecay[index] = true //A dead unit can't decay for at least 0. seconds.
                        set allowdecay_n = allowdecay_n + 1  //Add the unit to a stack. After the timer
                        set allowdecayunit[allowdecay_n] = u //expires, allow the unit to decay again.
                        call TimerStart(allowdecay, 0., false, function AutoIndex.allowDecay)
                       
                        //! runtextmacro optional RunAutoEvent("Death")
                        //If AutoEvents is in the map, run the Death events.
                        //! runtextmacro optional TransportUnload()
                        //If TransportEvents is in the map, remove the dead unit from whatever transport it's in.
                       
                    elseif removing[index] or (dead[index] and not nodecay[index]) or (not dead[index] and animated[index]) then
                        //If .nodecay was false and the unit is dead and was previously dead, the unit decayed.
                        //If .animated was true and the unit is dead, the unit died and exploded.
                        //If .removing was true, the unit is being removed or replaced.
                       
                        //! runtextmacro optional TransportUnload()
                        //If TransportEvents is in the map, remove the leaving unit from whatever transport it's in.
                       
                        set n = deindexfuncs_n
                        loop //Run the OnUnitDeindexed events.
                            exitwhen n < 0
                            call deindexfuncs[n].evaluate(u)
                            set n = n - 1
                        endloop
                        //! runtextmacro optional DestroyUnitLists()
                        //If UnitListModule is in the map, destroy all of the UnitLists associated with the leaving unit.
                        set n = autostructs_n
                        loop //Destroy AutoStructs for the leaving unit.
                            exitwhen n < 0
                            call destroyers[n].evaluate(u)
                            set n = n - 1
                        endloop
                       
                        //! runtextmacro optional TransportClean()
                        //If TransportEvents is in the map, and the leaving unit is a
                        //transport, clean the transport- related data from the unit.
                       
                        call AutoIndex(index).destroy() //Free the index by destroying the AutoIndex struct.
                        set idunit[index] = null //Null this unit reference to prevent a leak.
                    endif
                endif
            endif
           
        set u = null
        return false
    endmethod

    //===========================================================================
   
    private static method unitEntersMap takes unit u returns nothing
        local integer index
        local integer n = 0
            if getIndex(u) != 0 then
                return //Don't index a unit that already has an ID.
            endif
            static if false then//LIBRARY_xebasic then
                if GetUnitTypeId(u) == XE_DUMMY_UNITID then
                    return //Don't index xe dummy units.
                endif
            endif
            if not UnitFilter(u) then
                return //Don't index units that fail the unit filter.
            endif
           
            set index = create()
            call setIndex(u, index) //Assign an index to the entering unit.
           
            call UnitAddAbility(u, LeaveDetectAbilityID) //Add the leave detect ability to the entering unit.
            call UnitMakeAbilityPermanent(u, true, LeaveDetectAbilityID) //Prevent it from disappearing on morph.
           
            set dead[index] = IsUnitType(u, UNIT_TYPE_DEAD)         //Reset all of the flags for the entering
            set summoned[index] = IsUnitType(u, UNIT_TYPE_SUMMONED) //unit. These flags are necessary to detect
            set animated[index] = false                             //when the unit leaves the map.
            set nodecay[index] = false
            set removing[index] = false
            debug set altered[index] = false //In debug mode, this flag tracks wheter a unit's index was altered.
            set idunit[index] = u            //Attach the unit that is supposed to have this index to the index.
           
            if duringinit then
                call GroupAddUnit(preplaced, u) //Add units that are created during initialization to the preplaced
                //units group. This ensures that all units are noticed by OnUnitIndexed during initialization.
            endif
           
            loop //Create AutoStructs for the entering unit.
                exitwhen n > autostructs_n
                call creators[n].evaluate(u)
                set n = n + 1
            endloop
            set n = 0
            loop //Run the OnUnitIndexed events.
                exitwhen n > indexfuncs_n
                call indexfuncs[n].evaluate(u)
                set n = n + 1
            endloop
    endmethod
   
    private static method onIssuedOrder takes nothing returns boolean
            if getIndex(GetTriggerUnit()) == 0 then
                call unitEntersMap(GetTriggerUnit())
            endif //If the unit doesn't have an index at this point, assign it one.
            //This is necessary to catch units with default-on autocast abilities
            //when using GetUnitId on a newly created unit within an order event.
            //! runtextmacro optional TransportUnloadCheck()
            //If TransportEvents is in the map, check whether a unit is unloading.
        return GetIssuedOrderId() == 852056 //If the order is Undefend, allow detectStatus to run.
    endmethod
   
    private static method initEnteringUnit takes nothing returns boolean
            call unitEntersMap(GetFilterUnit())
        return false
    endmethod
   
    //===========================================================================
   
    private static method afterInit takes nothing returns nothing
        set duringinit = false //Initialization is over; set a flag.
        call DestroyTimer(GetExpiredTimer()) //Destroy the timer.
        call GroupClear(preplaced)   //The preplaced units group is
        call DestroyGroup(preplaced) //no longer needed, so clean it.
        set preplaced = null
    endmethod
   
    private static method onInit takes nothing returns nothing
        local region maparea = CreateRegion()
        local rect bounds = GetWorldBounds()
        local group g = CreateGroup()
        local integer i = 15
            static if not UseUnitUserData then
                set ht = InitHashtable() //Only create a hashtable if it will be used.
            endif
            loop
                exitwhen i < 0
                call SetPlayerAbilityAvailable(Player(i), LeaveDetectAbilityID, false)
                //Make the LeaveDetect ability unavailable so that it doesn't show up on the command card of every unit.
                call TriggerRegisterPlayerUnitEvent(order, Player(i), EVENT_PLAYER_UNIT_ISSUED_ORDER, null)
                //Register the "EVENT_PLAYER_UNIT_ISSUED_ORDER" event for each player.
                call GroupEnumUnitsOfPlayer(g, Player(i), function AutoIndex.initEnteringUnit)
                //Enum every non-filtered unit on the map during initialization and assign it a unique
                //index. By using GroupEnumUnitsOfPlayer, even units with Locust can be detected.
                set i = i - 1
            endloop
            call TriggerAddCondition(order, And(function AutoIndex.onIssuedOrder, function AutoIndex.detectStatus))
            //The detectStatus method will fire every time a non-filtered unit recieves an undefend order.
            //And() is used here to avoid using a trigger action, which starts a new thread and is slower.
            call TriggerRegisterPlayerUnitEvent(creepdeath, Player(12), EVENT_PLAYER_UNIT_DEATH, null)
            call TriggerAddCondition(creepdeath, function AutoIndex.detectStatus)
            //The detectStatus method must also fire when a neutral hostile creep dies, in case it was
            //sleeping. Sleeping creeps don't fire undefend orders on non-damaging deaths.
            call RegionAddRect(maparea, bounds) //GetWorldBounds() includes the shaded boundry areas.
            call TriggerRegisterEnterRegion(enter, maparea, function AutoIndex.initEnteringUnit)
            //The filter function of an EnterRegion trigger runs instantly when a unit is created.
            call TimerStart(CreateTimer(), 0., false, function AutoIndex.afterInit)
            //After any time elapses, perform after-initialization actions.
        call GroupClear(g)
        call DestroyGroup(g)
        call RemoveRect(bounds)
        set g = null
        set bounds = null
    endmethod
   
endstruct

//===========================================================================
// User functions:
//=================

function GetUnitId takes unit u returns integer
    static if DEBUG_MODE then
        return AutoIndex.getIndexDebug(u)
    else
        return AutoIndex.getIndex(u)
    endif
endfunction

function interface IndexFunc takes unit u returns nothing

function OnUnitIndexed takes IndexFunc func returns nothing
    call AutoIndex.onUnitIndexed(func)
endfunction

function OnUnitDeindexed takes IndexFunc func returns nothing
    call AutoIndex.onUnitDeindexed(func)
endfunction

endlibrary

At this point I feel there must be an error in the Object Editor that doesn't allow this.
 
Last edited:
If you already have a Unit Indexer then you're good to go. The only thing left to do is ensure that the code works.

I assumed you had done this already, but are you actually calling this?
vJASS:
call TriggerStartFreakOut()
That's what initializes everything and begins the timer. That won't happen automatically, you have to manually do it.

Also, these globals need to be defined properly:
vJASS:
private constant integer ID      = 'A0GS'
private constant integer BUFFID  = 'B04B'
private constant real INTERVAL   = 0.50
private constant real AI_RANGE   = 600.00
private constant integer ORDERID = OrderId("unholyfrenzy")
The ID's need to match what you have in your map, which could easily be different.

Lastly, when debugging code you want to add text messages throughout your functions. Then you can see exactly what code is and isn't running when you test things in-game. I showed how to do this before.
 
Last edited:
Back
Top