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

GUI Unit Event (now with a state-of-the-art Lua version)

Unit Event for GUI gives you access to all kinds of events which normal GUI events can't do:

FunctionLuavJass
Subject unit of the eventUnitEvent_unitUDexUnits[UDex]
Event index (for use within an array).UnitEvent_indexUDex
Data attachment to unitSet UnitEvent_setKey = unit
Set MyArray[UnitEvent_getKey] = data
Set MyArray2[UnitEvent_getKey] = data2
Set TempInt = (Custom value of unit)
Set MyArray[TempInt] = data
Set MyArray2[TempInt] = data2
Data retrieval from unitSet UnitEvent_setKey = unit
Set data = MyArray[UnitEvent_getKey]
Set data2 = MyArray2[UnitEvent_getKey]
Set TempInt = (Custom value of unit)
Set data = MyArray[TempInt]
Set data2 = MyArray2[TempInt]
Manually created in World Editor
Unit who summoned the subject unit
Know the previous type in a transform event
Is the unit reincarnating
Unit who is carrying the subject unit
All units carried by the unit
Detect the unit's killer in a death event
UnitEvent_preplaced
UnitEvent_summoner
UnitEvent_unitType
UnitEvent_reincarnating
UnitEvent_transporter
UnitEvent_cargo
(Killing unit)
IsUnitPreplaced
SummonerOfUnit
UnitTypeOf
IsUnitReincarnating
CargoTransportUnit
CargoTransportGroup
KillerOfUnit
Starts existing
Fully created
Stops existing
Starts reincarnating
Resurrects, reanimated or finishes reincarnating
Dies
Loaded into a transport
Unloaded from a transport
Transforms into a new unit
Is playable (alive/not loaded)
Is unplayable (dead/loaded)
OnUnitIndexed
OnUnitCreation
OnUnitRemoval
OnUnitReincarnating
OnUnitRevival
OnUnitDeath
OnUnitLoaded
OnUnitUnloaded
OnUnitTransform
OnUnitActive
OnUnitPassive
UnitIndexEvent == 1.00
UnitIndexEvent == 1.50
UnitIndexEvent == 2.00
DeathEvent == 0.50
DeathEvent == 2.00
DeathEvent == 1.00
CargoEvent == 1.00
CargoEvent == 2.00
UnitTypeEvent == 1.00
UnitInActionEvent == 1.00
UnitInActionEvent == 2.00

You can distinguish between types of unit revival with three conditions:
1) If it is summoned, that means it was reanimated (e.g. "Animate Dead (Death Knight Ultimate)").
2) If UnitEvent_reincarnating (Lua version) / IsUnitReincarnating (JASS version) is true, that means it finished reincarnating (e.g. "Reincarnation (Tauren Chieftan Ultimate)").
3) If the above are False, the unit was resurrected (e.g. "Ressurrection (Paladin Ultimate)").

Lua:
OnGlobalInit(function()
    
    Require "Timed"                         --https://www.hiveworkshop.com/threads/timed-call-and-echo.339222/
    Require "AddHook"                       --https://www.hiveworkshop.com/threads/hook.339153
    Require "GlobalRemap"                   --https://www.hiveworkshop.com/threads/global-variable-remapper.339308
    Require "RegisterAnyPlayerUnitEvent"    --https://www.hiveworkshop.com/threads/collection-gui-repair-kit.317084/
    Require "CreateEvent"                   --https://www.hiveworkshop.com/threads/event-gui-friendly.339451/
    Require "PreciseWait"                   --https://www.hiveworkshop.com/threads/precise-wait-gui-friendly.316960/
--[[
Lua Unit Event 1.2.0.2

In addition to the existing benefits enjoyed over the past years, this Lua version supports linked events that allow
your trigger to Wait until another event runs (meaning you can do attachment and cleanup from one trigger).

Variable names have been completely changed from all prior Unit Event incarnations.
> All real variable event names are now prefixed with OnUnit...
> All array references (unit properties) are now prefixed with UnitEvent_
> Lua users can access a unit's properties via UnitEvent[unit].property (e.g. reincarnating/cargo)
> Lua users can easily add a readonly GUI property to a unit via UnitEvent.addProperty("propertyName")
>>> GUI accesses it via UnitEvent_propertyName, the second is readable and writable within Lua via UnitEvent[unit].propertyName
> UnitUserData (custom value of unit) has been completely removed. This is the first unit event/indexer to not use UnitUserData nor hashtables.
>>> UnitEvent_unit is the subject unit of the event.
>>> UnitEvent_index is an integer in GUI, but points to a the unit.
>>> UnitEvent_setKey lets you assign a unit to the key.
>>> UnitEvent_getKey is an integer in GUI, but points to the unit you assigned as the key.
>>>>> Lua doesn't care about array max sizes, nor the type of information used as an index in that array (because it uses tables and not arrays).
>>>>> GUI is over 20 years old and can easily be fooled. As long as the variable is defined with the correct type, it doesn't care what happens to that variable behind the scenes.
--]]
    UnitEvent={}

    local _REMOVE_ABIL      = FourCC('A001')
    local _TRANSFORM_ABIL   = FourCC('A002') --be sure to assign these to their respective abilities if you prefer not to initialize via GUI
    
    --Un-comment this next line if you want to use the custom value of units functionality (for backwards compatibility):
    --GetUnitUserData = function(unit) return unit end

--[[
    Full list of GUI variables:
    real    udg_OnUnitIndexed
    real    udg_OnUnitCreation
    real    udg_OnUnitRemoval
    real    udg_OnUnitReincarnating
    real    udg_OnUnitRevival
    real    udg_OnUnitLoaded
    real    udg_OnUnitUnloaded
    real    udg_OnUnitTransform
    real    udg_OnUnitDeath
    real    udg_OnUnitActive
    real    udg_OnUnitPassive

    ability udg_DetectRemoveAbility
    ability udg_DetectTransformAbility

    unit    udg_UnitEvent_unit
    integer udg_UnitEvent_index

    unit    udg_UnitEvent_setKey
    integer udg_UnitEvent_getKey

    boolean   array udg_UnitEvent_preplaced
    unit      array udg_UnitEvent_summoner
    unittype  array udg_UnitEvent_unitType
    boolean   array udg_UnitEvent_reincarnating
    unit      array udg_UnitEvent_transporter
    unitgroup array udg_UnitEvent_cargo
--]]

    local eventList={}
    local udg_prefix="udg_OnUnit"
    local makeAPI = function(name)
        UnitEvent["on"..name],
        eventList["on"..name] = CreateEvent(udg_prefix..name, true)
    end
    local unitIndices={} ---@type UnitEventTable[]

    --onIndexed and onCreation occur at roughly the same time, but the unit's creation should be used instead as it will have more data.
    --Combined, they are the counterparts to onRemoval.
    makeAPI("Indexed")
    makeAPI("Creation")
    makeAPI("Removal")

    --counterparts (though revival doesn't only come from reincarnation):
    makeAPI("Reincarnating")
    makeAPI("Revival")
    
    --perfect counterparts:
    makeAPI("Loaded")
    makeAPI("Unloaded")
    
    --stand-alone events:
    makeAPI("Transform")
    makeAPI("Death")
    
    --perfect counterparts that generalize all but the "transform" event:
    makeAPI("Active")
    makeAPI("Passive")

    --Used to get the UnitEvent table from the unit to detect UnitEvent-specific properties.
    UnitEvent.__index = function(_, unit) return unitIndices[unit] end
    
    ---@param name string
    UnitEvent.addProperty = function(name)
        GlobalRemapArray("udg_UnitEvent_"..name, function(unit) return unitIndices[unit][name] end)
    end

    ---@class UnitEventTable : table
    ---@field unit          unit
    ---@field preplaced     boolean
    ---@field summoner      unit
    ---@field transporter   unit
    ---@field cargo         group
    ---@field reincarnating boolean
    ---@field private new boolean
    ---@field private alive boolean
    ---@field private unloading boolean
    
    --The below two variables are intended for GUI typecasting, because you can't use a unit as an array index.
    --What it does is bend the rules of GUI (which is still bound by strict JASS types) by transforming those
    --variables with Global Variable Remapper (which isn't restricted by any types).
    --"setKey" is write-only (assigns the key to a unit)
    --"getKey" is read-only (retrieves the key and tells GUI that it's an integer, allowing it to be used as an array index)
    local lastUnit
    GlobalRemap("udg_UnitEvent_setKey", nil, function(unit)lastUnit=unit end) --assign to a unit to unlock the getKey variable.
    GlobalRemap("udg_UnitEvent_getKey",      function() return lastUnit  end) --type is "integer" in GUI but remains a unit in Lua.

    local runEvent
    do
        local eventUnit
        local getEventUnit  = function() return eventUnit end
        runEvent            = function(event, unitTable)
            local cached    = eventUnit
            eventUnit       = unitTable.unit
            eventList[event](unitTable)
            eventUnit       = cached
        end
        GlobalRemap("udg_UnitEvent_unit",  getEventUnit) --the subject unit for the event.
        GlobalRemap("udg_UnitEvent_index", getEventUnit) --fools GUI into thinking unit is an integer
    end
    --add a bunch of read-only arrays to access GUI data. I've removed the "IsUnitAlive" array as the GUI living checks are fixed with the GUI Enhancer Colleciton.
    UnitEvent.addProperty("preplaced")
    UnitEvent.addProperty("unitType")
    UnitEvent.addProperty("reincarnating")
    UnitEvent.addProperty("transporter")
    UnitEvent.addProperty("summoner")
    
    if rawget(_G, "udg_UnitEvent_cargo") then
        UnitEvent.addProperty("cargo")
    end
    
    --Flag a unit as being able to move or attack on its own:
    local function setActive(unitTable)
        if unitTable and not unitTable.active and UnitAlive(unitTable.unit) then --be sure not to run the event when corpses are created/unloaded.
            unitTable.active = true
            runEvent("onActive", unitTable)
        end
    end
    ---Flag a unit as NOT being able to move or attack on its own:
    local function setPassive(unitTable)
        if unitTable and unitTable.active then
            unitTable.active = nil
            runEvent("onPassive", unitTable)
        end
    end
    local function getFunc(active)
        return function(unitTable) (active and setActive or setPassive)(unitTable) end
    end

    UnitEvent.onCreation(getFunc(true), 2, true)
    UnitEvent.onUnloaded(getFunc(true), 2, true)
    UnitEvent.onRevival(getFunc(true), 2, true)
    
    UnitEvent.onLoaded(getFunc(), 2, true)
    UnitEvent.onReincarnating(getFunc(), 2, true)
    UnitEvent.onDeath(getFunc(), 2, true)
    UnitEvent.onRemoval(getFunc(), 2, true)
    
    --UnitEvent.onIndex(function(unitTable) print(tostring(unitTable.unit).."/"..GetUnitName(unitTable.unit).." has been indexed.") end)
    
    setmetatable(UnitEvent, UnitEvent)

    --Wait until GUI triggers and events have been initialized. 
    OnTrigInit(function()
        if rawget(_G, "Trig_Unit_Event_Config_Actions") then
            Trig_Unit_Event_Config_Actions()
            _REMOVE_ABIL    = udg_DetectRemoveAbility    or _REMOVE_ABIL
            _TRANSFORM_ABIL = udg_DetectTransformAbility or _TRANSFORM_ABIL
        end
        local function checkAfter(unitTable)
            if not unitTable.checking then
                unitTable.checking              = true
                Timed.call(0, function()
                    unitTable.checking          = nil
                    if unitTable.new then
                        unitTable.new           = nil
                        runEvent("onCreation", unitTable) --thanks to Spellbound for the idea
                    elseif unitTable.transforming then
                        local unit = unitTable.unit
                        runEvent("onTransform", unitTable)
                        unitTable.unitType = GetUnitTypeId(unit) --Set this afterward to give the user extra reference

                        --Reset the transforming flags so that subsequent transformations can be detected.
                        unitTable.transforming  = nil
                        UnitAddAbility(unit, _TRANSFORM_ABIL)
                    elseif unitTable.alive then
                        unitTable.reincarnating = true
                        unitTable.alive         = false
                        runEvent("onReincarnating", unitTable)
                    elseif UnitAlive(unitTable.unit) then
                        unitTable.alive = true
                        runEvent("onRevival", unitTable)
                        unitTable.reincarnating = false
                    end
                end)
            end
        end
    
        local re = CreateRegion()
        local r = GetWorldBounds()
        local maxX, maxY = GetRectMaxX(r), GetRectMaxY(r)
        RegionAddRect(re, r); RemoveRect(r)
        
        local function unloadUnit(unitTable)
            local unit, transport       = unitTable.unit, unitTable.transporter
            GroupRemoveUnit(unitIndices[transport].cargo, unit)
            unitTable.unloading         = true
            runEvent("onUnloaded", unitTable)
            unitTable.unloading         = nil
            if not IsUnitLoaded(unit) or not UnitAlive(transport) or GetUnitTypeId(transport) == 0 then
                unitTable.transporter   = nil
            end
        end
        
        local preplaced = true
        local onEnter = Filter(
        function()
            local unit = GetFilterUnit()
            local unitTable = unitIndices[unit]
            if not unitTable then
                unitTable = {
                    unit    = unit,
                    new     = true,
                    alive   = true,
                    unitType= GetUnitTypeId(unit)
                }
                UnitAddAbility(unit, _REMOVE_ABIL)
                UnitMakeAbilityPermanent(unit, true, _REMOVE_ABIL)
                UnitAddAbility(unit, _TRANSFORM_ABIL)

                unitIndices[unit] = unitTable

                unitTable.preplaced = preplaced
                runEvent("onIndexed", unitTable)
                
                checkAfter(unitTable)
            elseif unitTable.transporter and not IsUnitLoaded(unit) then
                --the unit was dead, but has re-entered the map (e.g. unloaded from meat wagon)
                unloadUnit(unitTable)
            end
        end)
        TriggerRegisterEnterRegion(CreateTrigger(), re, onEnter)
        
        RegisterAnyPlayerUnitEvent(EVENT_PLAYER_UNIT_LOADED,
        function()
            local unit = GetTriggerUnit()
            local unitTable = unitIndices[unit]
            if unitTable then
                if unitTable.transporter then
                    unloadUnit(unitTable)
                end
                --Loaded corpses do not issue an order when unloaded, therefore must
                --use the enter-region event method taken from Jesus4Lyf's Transport: https://www.thehelper.net/threads/transport-enter-leave-detection.126051/
                if not unitTable.alive then
                    SetUnitX(unit, maxX)
                    SetUnitY(unit, maxY)
                end
                local transporter = GetTransportUnit()
                unitTable.transporter = transporter
                local g = unitIndices[transporter].cargo
                if not g then
                    g=CreateGroup()
                    unitIndices[transporter].cargo = g
                end
                GroupAddUnit(g, unit)
                
                runEvent("onLoaded", unitTable)
            end
        end)
        
        RegisterAnyPlayerUnitEvent(EVENT_PLAYER_UNIT_DEATH,
        function()
            local unitTable = unitIndices[GetTriggerUnit()]
            if unitTable then
                unitTable.alive = false
                runEvent("onDeath", unitTable)
                if unitTable.transporter then
                    unloadUnit(unitTable)
                end
            end
        end)
        
        RegisterAnyPlayerUnitEvent(EVENT_PLAYER_UNIT_SUMMON,
        function()
            local unitTable = unitIndices[GetTriggerUnit()]
            if unitTable.new then
                unitTable.summoner = GetSummoningUnit()
            end
        end)
        
        local orderB = Filter(
        function()
            local unit = GetFilterUnit()
            local unitTable = unitIndices[unit]
            if unitTable then
                if GetUnitAbilityLevel(unit, _REMOVE_ABIL) == 0 then

                    runEvent("onRemoval", unitTable)
                    unitIndices[unit] = nil
                    unitTable.cargo = nil
                elseif not unitTable.alive then
                    if UnitAlive(unit) then
                        checkAfter(unitTable)
                    end
                elseif not UnitAlive(unit) then
                    if unitTable.new then
                        --This unit was created as a corpse.
                        unitTable.alive = false
                        runEvent("onDeath", unitTable)

                    elseif not unitTable.transporter or not IsUnitType(unit, UNIT_TYPE_HERO) then
                        --The unit may have just started reincarnating.
                        checkAfter(unitTable)
                    end
                elseif GetUnitAbilityLevel(unit, _TRANSFORM_ABIL) == 0 and not unitTable.transforming then
                    unitTable.transforming = true
                    checkAfter(unitTable)
                end
                if unitTable.transporter and not unitTable.unloading and not (IsUnitLoaded(unit) and UnitAlive(unit)) then
                    unloadUnit(unitTable)
                end
            end
        end)
        
        local p
        local order = CreateTrigger()
        for i = 0, bj_MAX_PLAYER_SLOTS - 1 do
            p = Player(i)
            GroupEnumUnitsOfPlayer(bj_lastCreatedGroup, p, onEnter)
            SetPlayerAbilityAvailable(p, _REMOVE_ABIL, false)
            SetPlayerAbilityAvailable(p, _TRANSFORM_ABIL, false)
            TriggerRegisterPlayerUnitEvent(order, p, EVENT_PLAYER_UNIT_ISSUED_ORDER, orderB)
        end
        preplaced = false
    end)
end)

How to install:

- Delete Unit Indexer from your map (if you already have it)
- Copy the "Unit Event Ability" ability from Object Editor
- Copy the Unit Event trigger category
- Paste the Unit Event Ability from object editor twice
- Set the DetectRemoveAbility variable in the Unit Event Config trigger to one of the abilities.
- Set the DetectTransformAbility variable in the same trigger to the other of the two.


Inspiration:

- AutoEvents by grim001 (was hosted on the now-defunct wc3c.net)
- UnitEvent by Nestharus: https://github.com/nestharus/JASS/blob/master/jass/Systems/Unit%20Event/script.j
- Transport by Jesus4Lyf: https://www.thehelper.net/threads/transport-enter-leave-detection.126051/

nestharus, grim001, unitevent, autoevents, unit indexer, reincarnate, reincarnation, reanimation, resurrect, resurrection, detect, death, removal, load, unload, transport, transform, unit type
Contents

Lua Unit Event Test Map (Map)

Unit Event 2.5.3.1 (Map)

Reviews
Approved. Makes easy to detect many events that do not exist in the editor. Very useful system.
Level 8
Joined
May 12, 2018
Messages
106
I had found only one SetUnitUserData, it is from GUI UNIT EVENT

As thy explained I checked Object manager too, I find some strange things.
1697464919540.png

ArcaneVaultElf 'h01Q' that Copied and pasted from Original ArcaneVault is printed as Unknown.

I've seen this error in the item section before. Items that linked with Item and Shops Sell lists in campaign object editors are normal, but the map does not recognize objects with that ID. these errors also likely to cause indexing errors?
 

Uncle

Warcraft Moderator
Level 64
Joined
Aug 10, 2018
Messages
6,517
Issues that can occur when using a Unit Indexer:

- Recycled custom values contain previous unit's data - You've forgotten to reset the data during the de-indexing event. This could cause your triggers that rely on this data to have unexpected results.

- Mirror Image simply breaks the system - Recent bug, I wish they'd stop trying to "fix" things already.

But if the system works for some time before breaking then it could have something to do with specific Events reaching a registration limit.
 
Last edited:

Uncle

Warcraft Moderator
Level 64
Joined
Aug 10, 2018
Messages
6,517
Uncle pointed at "forgetting to reset the data" in the system. Is this still the case in that system? Would it lead to any serious issues?
The non-Lua version uses his Unit Indexer which does still recycle custom values. This recycling means that a new unit can get the same custom value as a previously removed unit. Therefore, any data that hasn't been "reset" will retain it's previous values. This is more of an edge case concern, because it's only in some situations that you rely on the default value. In a lot of cases you simply overwrite the data before using it so it's not an issue.

Example of when you should reset your Array:
  • Events
    • Unit - A unit dies
  • Conditions
  • Actions
    • Set Variable CV = (Custom value of (Killing unit))
    • Set Variable Kill_Counter[CV] = (Kill_Counter[CV] + 1)
Kill_Counter defaults at 0. I increase it by 1 whenever a unit gets a kill in order to track their total number of kills. If the unit using that custom value is removed from the game and it's custom value gets recycled and given to a new unit, then that new unit's Kill_Counter will still be using the previous value. UNLESS, you reset the variable back to it's default value before that happens. Bribe gives you two Events, one for when a custom value gets recycled and one for when a unit is given a new custom value, either of these could be used to Reset the data but it makes the most sense to use the Recycled event.


Note that the Lua version doesn't use a Unit Indexer and instead relies on Unit handles, so you don't have to worry about resetting anything since these are guaranteed to be different for each Unit. But as far as I know this version is unsafe to use since it relies on unit handles as indexes. Hopefully Bribe knows something I don't know and can speak on this, but I've read in other threads that this can cause desyncs due to mismatched handle id's between clients. I've experienced desyncs myself which I've attributed to this but I'm not 100% certain.
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
The non-Lua version uses his Unit Indexer which does still recycle custom values. This recycling means that a new unit can get the same custom value as a previously removed unit. Therefore, any data that hasn't been "reset" will retain it's previous values. This is more of an edge case concern, because it's only in some situations that you rely on the default value. In a lot of cases you simply overwrite the data before using it so it's not an issue.

Example of when you should reset your Array:
  • Events
    • Unit - A unit dies
  • Conditions
  • Actions
    • Set Variable CV = (Custom value of (Killing unit))
    • Set Variable Kill_Counter[CV] = (Kill_Counter[CV] + 1)
Kill_Counter defaults at 0. I increase it by 1 whenever a unit gets a kill in order to track their total number of kills. If the unit using that custom value is removed from the game and it's custom value gets recycled and given to a new unit, then that new unit's Kill_Counter will still be using the previous value. UNLESS, you reset the variable back to it's default value before that happens. Bribe gives you two Events, one for when a custom value gets recycled and one for when a unit is given a new custom value, either of these could work but it makes the most sense to use the Recycled event to avoid any issues.


Note that the Lua version doesn't use a Unit Indexer, so you don't have to worry about that. But as far as I know it's unsafe to use for other reasons since it relies on unit handles as indexes. Hopefully Bribe knows something I don't know and can speak on this, but I've read in other threads and have experienced desyncs myself (although I'm not 100% sure if this was the cause) due to mismatched handle id's between clients.
that mismatch of ids is more of a "pairs" syncing issue I thought. And your death event method is unsafe in maps with revival/heroes. Better to use OnUnitRemoved
 

Uncle

Warcraft Moderator
Level 64
Joined
Aug 10, 2018
Messages
6,517
that mismatch of ids is more of a "pairs" syncing issue I thought. And your death event method is unsafe in maps with revival/heroes. Better to use OnUnitRemoved
Yeah, I saw that pairs thread as well, I can't say for certain because 1) Warcraft 3 seems to have a million problems these days regardless of what you do, and 2) I'm far from an expert. I just know that my maps that use this method do indeed desync from time to time, although, there were times when I tried to remove all cases of it and the desyncs still happened so... I guess it's something to keep an eye on is all.

About the death event, I have like 1000 example maps using your Unit Indexer spread around Hive that I need to go revise... I was under the impression that Heroes didn't recycle custom values for some reason.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Yeah, I saw that pairs thread as well, I can't say for certain because 1) Warcraft 3 seems to have a million problems these days regardless of what you do, and 2) I'm far from an expert. I just know that my maps that use this method do indeed desync from time to time, although, there were times when I tried to remove all cases of it and the desyncs still happened so... I guess it's something to keep an eye on is all.

About the death event, I have like 1000 example maps using your Unit Indexer spread around Hive that I need to go revise... I was under the impression that Heroes didn't recycle custom values for some reason.
Heroes will trigger the death event throughout the game. So if you clear data that should be permanently attached to them, wait until the "remove" event is called instead. If it is just debuff data, it is fine.

But most of all, with JASS, make sure nothing BUT Unit Event/Unit Indexer sets that custom value.
 
Top