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

[vJASS] UnitEventEx

I enjoyed using GUI Unit Event for the longest time, but since I'm moving away from GUI systems and needed something similar, I figured I'd make my own custom unit event library. With the help of Bribe, I was able to reverse engineer his library and create UnitEventEx.

UnitEventEx will allow you to detect when a unit Reincarnates, is Reanimated via Animate Dead, Resurrected, Transforms (eg Metamorphosis), and is Loaded/Unloaded from transport.

UnitEventEx is modular, so for events you do not need, simply set the corresponding boolean under // CONFIGURATION to false.

NB: This library was renamed from UnitEventsEx to UnitEventEx because the naming convention on that type of stuff doesn't seem to have pluralisation, and the fact that UnitEventsEx makes me giggle like a ten-year-old and is a little bit distracting.

So I recently spent a few days trying to create a vJASS version of Bribe's excellent GUI Unit Event, but I'd like some feedback on the system in general. However, I do have some specific questions that would help me a great deal if knowledgeable people would enlighten me:

Groups vs lists
Does one run faster than the other? Is that even relevant? If I ever learn how to code a linked list, I'm going to try to make my own and make ListT optional.

Static Ifs
In trying to make this code modular, I ended up with a lot of static ifs everywhere and I'm not even sure that's necessary, given they're placed after if blocks. I suppose preventing an unnecessary TriggerEvaluate does save some performance, but do you think it's worth doing?

Getters
What other types of Get-type functions would this code benefit from?

If you think of anything else to comment on, please feel free.


JASS:
/*
    UnitEventEx v1.03.1
    by Spellbound
  
    Credits to Bribe for the excellent GUI Unit Event, of which this library is the vJASS
    version of. He was also helpful in understanding how his system works so that I may code this.
    
    Additional credits to Jesus4Lyf for Transport, Nestharus for UnitEvent and grim001 for AutoEvents.
*/
/*
    _________________________________________________________________________
    DESCRIPTION
    ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
    UnitEventEx provides you with extra events not available in the vanilla game. Specifically, 
    you will be able to detect when a unit transforms, loads and unloads from a transport unit, is
    reanimated by Reanimate Dead, reincarnates or is brought back to life.
  
    Additionally, each of those events can be toggled on or off depending on whether you need them.
    See // CONFIGURATION under the globals block below.
*/
/*
    _________________________________________________________________________
    INSTALLATION
    ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
    UnitEventEx requires:
  
        UnitDex by TriggerHappy (Make sure you have the Detect Leave (UnitDex) ability in your map)
        https://www.hiveworkshop.com/threads/system-unitdex-unit-indexer.248209/
      
        RegisterEvent Pack by Bannar (both RegisterNativeEvent and RegisterPlayerUnitEvent)
        https://www.hiveworkshop.com/threads/snippet-registerevent-pack.250266/
    
    Optionally uses:
    
        ListT by Bannar
        https://www.hiveworkshop.com/threads/containers-list-t.249011/
        
        WorldBounds by Nestharus
        https://github.com/nestharus/JASS/blob/master/jass/Systems/WorldBounds/script.j
  
    Once you have the required libraries in place, either copy the Detect Transform (UnitEventEx)
    ability to your map and set DETECT_TRANSFORM_ABILITY in the globals block below to the id of
    the spell.
  
    Alternatively, you can use the textmacro below. Import UnitEventEx, uncomment it, save your map,
    close it, reload and comment it out again. The ability will have been created in your Object Editor.
*/
    // //! external ObjectMerger w3a Adef EETD anam "Detect Transform" ansf "(UnitEventEx)" aart "" acat "" arac 0
/*
    _________________________________________________________________________
    CONFUGRATION
    ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
    UnitEventEx can be configured to exclude those events you do not wish to use. Define which
    events will be needed in the CONFIG block below.
*/
/*
    _________________________________________________________________________
    API
    ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
    
    Event Registration
    ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
    call RegisterNativeEvent(yourEvent, function yourFunction )
        This registers the event for all players.
    
    call RegisterIndexNativeEvent(playerIndex, yourEvent, function yourFunction )
        This registers the event for a specific player.
        NB: Player 1 starts on zero.
    
    call RegisterIndexNativeEvent(GetHandleId(event unit), yourEvent, function yourFunction )
        This registers the event for a specific unit.
        
    call GetIndexNativeEventTrigger(GetHandleId(event unit), yourEvent)
        This returns the trigger associated with that unit id and event id. Destroy that trigger
        to remove it. Useful for temporary events.
        
        Also works with playerIndex.
        
    The Events you can call are:
  
    EVENT_ON_TRANSFORM
    EVENT_ON_CARGO_LOAD
    EVENT_ON_CARGO_UNLOAD
    EVENT_ON_RESURRECTION
    EVENT_ON_ANIMATE_DEAD
    EVENT_ON_REINCARNATION_START
    EVENT_ON_REINCARNATION_FINISH
    EVENT_ON_UNIT_CREATED
    
    Event Getters
    ¯¯¯¯¯¯¯¯¯¯¯¯¯
    call GetEventUnit() returns unit
        returns the event unit, similar to GetTriggerUnit().
        MB: GetTriggerUnit() will NOT work with UnitEventEx.
      
    call GetCargoUnit() returns unit
        returns the transport unit.
        NB: Only works on EVENT_ON_CARGO_LOAD and EVENT_ON_CARGO_UNLOAD
      
    call GetCargoSize() returns integer
        returns the number of units loaded in a transport.
        NB: Only works on EVENT_ON_CARGO_LOAD and EVENT_ON_CARGO_UNLOAD

    Cargo Getters
    ¯¯¯¯¯¯¯¯¯¯¯¯¯
    call IsUnitInTransporter(whichUnit) returns boolean
        returns true/false on whether a unit is loaded into a transporter.
    call GetUnitTransporter(whichUnit) returns unit
        returns the transport unit that is currently carrying whichUnit.
        the value will be null if the unit isn't in any transport.
    call GetCargoTransportedUnitGroup(transporter) returns group
        returns a copy of the group of the units in the cargo of transporter.
        NB: you must destroy the group that is returned after you're done with it.
    **Only if you have ListT
    call GetCargoTransportedUnitList(transporter) returne UEExList
        returns a copy of the list of the units in the cargo of transporter.
        NB: you must destroy the list that is returned after you're done with it.
*/
library UnitEventEx requires UnitDex, RegisterPlayerUnitEvent, optional ListT, WorldBounds
globals
    
    // CONFIGURATION
    
    // the transform detection ability. If DETECT_TRANSFORM is false, this is not needed.
    private constant integer    DETECT_TRANSFORM_ABILITY = 'UEdt'
    
    // toggles the detection of transform events.
    private constant boolean    DETECT_TRANSFORM        = true
    
    // toggles the detection of load/unload of cargo units, except for dead units (eg Meat Wagon's Exhume Corpse)
    private constant boolean    DETECT_CARGO            = true
    
    // toggles the detection of unloading dead units in cargo (Exhume Corpse). Does nothing if DETECT_CARGO is false.
    private constant boolean    DETECT_CARGO_DEAD       = true
    
    // toggles the detection of when a unit begins and finishes reincarnating.
    private constant boolean    DETECT_REINCARNATION    = true
    
    // toggles the detection of when a unit is animated via animate dead.
    private constant boolean    DETECT_ANIMATE_DEAD     = true
    
    // toggles the detection of units that are brought back to life via resurrection.
    private constant boolean    DETECT_RESURRECTION     = true
    
    // this overrides reincarnation, animate dead and resurrection. Set to true if you want any of these events to work.
    // for some reason setting DETECT_REVIVES = (DETECT_REINCARNATION or DETECT_ANIMATE_DEAD or DETECT_RESURRECTION) does not work.
    private constant boolean    DETECT_REVIVES          = true
    // this particular event will run after a zero-second delay so that units are able to fully enter the creation scope.
    // it can be useful if you need something to run after both indexing and other events like EVENT_PLAYER_UNIT_CONSTRUCT_START
    private constant boolean    DETECT_CREATION         = true
    
    // END CONFIGURATION
    
    private unit eventUnit = null
    private unit eventOther = null
    private integer eventPreType = 0
    
    integer EVENT_ON_TRANSFORM
    integer EVENT_ON_CARGO_LOAD
    integer EVENT_ON_CARGO_UNLOAD
    integer EVENT_ON_RESURRECTION
    integer EVENT_ON_ANIMATE_DEAD
    integer EVENT_ON_REINCARNATION_START
    integer EVENT_ON_REINCARNATION_FINISH
    integer EVENT_ON_UNIT_CREATED
    
    private integer Stack = -1
    private unit array IndexedUnit
    private unit array CargoUnit
    private group array CargoGroup
    private unit array Transporter
    private integer array PreTransformType
    
    private real MaxX
    private real MaxY
    
    private boolean array IsNew
    private boolean array IsAlive
    private boolean array IsReincarnating
    private boolean array IsTransforming
    
    private timer AfterIndexTimer = CreateTimer()
    private boolean rezCheck = true
    
endglobals
native UnitAlive takes unit u returns boolean
//! runtextmacro optional DEFINE_LIST("", "UEExList", "unit")
private function FireEvent takes integer ev, unit u, unit other returns nothing
    local integer playerId = GetPlayerId(GetOwningPlayer(u))
    local integer handleId = GetHandleId(u)
    local integer id = GetUnitId(u)
    local unit prevUnit = eventUnit
    local unit prevOther = eventOther
    local integer prevType = eventPreType
    
    set eventUnit = u
    set eventOther = other
    set eventPreType = PreTransformType[id]
    
    call TriggerEvaluate(GetNativeEventTrigger(ev))
    if IsNativeEventRegistered(playerId, ev) then
        call TriggerEvaluate(GetIndexNativeEventTrigger(playerId, ev))
    elseif IsNativeEventRegistered(handleId, ev) then
        call TriggerEvaluate(GetIndexNativeEventTrigger(handleId, ev))
    endif
    
    set eventUnit = prevUnit
    set eventOther = prevOther
    set eventPreType = prevType
    
    set prevUnit = null
    set prevOther = null
endfunction
private struct Cargo
    
    static if LIBRARY_ListT then
        static UEExList array CargoList
    endif
    
    static method delete takes integer transport_id returns nothing
        static if LIBRARY_ListT then
            call CargoList[transport_id].destroy()
        else
            call DestroyGroup(CargoGroup[transport_id])
        endif
    endmethod
    
    static method remove takes unit u, unit transport returns nothing
        local integer transport_id = GetUnitId(transport)
        static if LIBRARY_ListT then
            call CargoList[transport_id].removeElem(u)
        else
            call GroupRemoveUnit(CargoGroup[transport_id], u)
        endif
        set Transporter[GetUnitId(u)] = null
        call FireEvent(EVENT_ON_CARGO_UNLOAD, u, transport)
    endmethod
    
    static method add takes unit u, unit transport returns nothing
        local integer transport_id = GetUnitId(transport)
        static if LIBRARY_ListT then
            call CargoList[transport_id].push(u)
        else
            call GroupAddUnit(CargoGroup[transport_id], u)
        endif
        set Transporter[GetUnitId(u)] = transport
        call FireEvent(EVENT_ON_CARGO_LOAD, u, transport)
    endmethod
    
    static method size takes integer transport_id returns integer
        static if LIBRARY_ListT then
            return CargoList[transport_id].size()
        else
            return CountUnitsInGroup(CargoGroup[transport_id])
        endif
    endmethod
    
    static method exists takes integer transport_id returns boolean
        static if LIBRARY_ListT then
            return (CargoList[transport_id] != 0)
        else
            return (CargoGroup[transport_id] != null)
        endif
    endmethod
    static method copyGroup takes integer transport_id returns group
        local group g = CreateGroup()
        static if LIBRARY_ListT then
            local UEExListItem node = CargoList[transport_id].first
            loop
                exitwhen node == 0
                call GroupAddUnit(g, node.data)
                set node = node.next
            endloop
        else
            call BlzGroupAddGroupFast(CargoGroup[transport_id], g)
        endif
        return g
    endmethod
    static if LIBRARY_ListT then
        static method copyList takes integer transport_id returns UEExList
            local UEExListItem node = CargoList[transport_id].first
            local UEExList list = UEExList.create()
            loop
                exitwhen node == 0
                call list.push(node.data)
                set node = node.next
            endloop
            return list
        endmethod
    endif
    
    static method create takes integer transport_id returns thistype
        static if LIBRARY_ListT then
            set CargoList[transport_id] = UEExList.create()
        else
            set CargoGroup[transport_id] = CreateGroup()
        endif
        return 0
    endmethod
    
endstruct
// these functions are here so that they don't call functions below them.
function GetEventUnit takes nothing returns unit
    return eventUnit
endfunction
function GetCargoUnit takes nothing returns unit
    return eventOther
endfunction
function GetCargoSize takes unit transport returns integer
    return Cargo.size(GetUnitId(transport))
endfunction
function IsUnitInTransporter takes unit whichUnit returns boolean
    return Transporter[GetUnitId(whichUnit)] != null
endfunction
function GetUnitTransporter takes unit whichUnit returns unit
    return Transporter[GetUnitId(whichUnit)]
endfunction
function GetCargoTransportedUnitGroup takes unit transporter returns group
    return Cargo.copyGroup(GetUnitId(transporter))
endfunction
static if LIBRARY_ListT then
    function GetCargoTransportedUnitList takes unit transporter returns UEExList
        return Cargo.copyList(GetUnitId(transporter))
    endfunction
endif
private module UnitEventExCore
    
    /*/* afterIndex */*/
    private static method afterIndex takes nothing returns nothing
        local integer i = Stack
        local integer id
        local unit u
        
        loop
            exitwhen i < 0
            set u = IndexedUnit[i]
            set id = GetUnitId(u)
            
            if IsNew[id] then
                set IsNew[id] = false
                static if DETECT_CREATION then
                    call FireEvent(EVENT_ON_UNIT_CREATED, u, null)
                endif
                
            elseif IsTransforming[id] then
                
                static if DETECT_TRANSFORM then
                    call FireEvent(EVENT_ON_TRANSFORM, u, null)
                    set IsTransforming[id] = false
                    // The unit has finished transforming. Store it's new type id.
                    set PreTransformType[id] = GetUnitTypeId(u)
                    call UnitAddAbility(u, DETECT_TRANSFORM_ABILITY)
                endif
                
            elseif IsAlive[id] then
            
                static if DETECT_REINCARNATION then
                    set IsReincarnating[id] = true
                    set IsAlive[id] = false
                    call FireEvent(EVENT_ON_REINCARNATION_START, u, null)
                endif
                
            endif
            
            set IndexedUnit[i] = null
            set i = i - 1
        endloop
        set Stack = -1
            
        set u = null
    endmethod
    
    
    /*/* timerCheck */*/
    private static method timerCheck takes unit u returns nothing
        set Stack = Stack + 1
        set IndexedUnit[Stack] = u
        call TimerStart(AfterIndexTimer, 0., false, function thistype.afterIndex)
    endmethod
    
    
    /*/* unload */*/
    private static method unload takes unit u returns nothing
        local integer id = GetUnitId(u)
        local integer cargo_id = GetUnitId(CargoUnit[id])
        
        call Cargo.remove(u, CargoUnit[id])
        
        if not IsUnitLoaded(u) or not UnitAlive(CargoUnit[id]) then
            set CargoUnit[id] = null
        endif
    endmethod
    
    
    /*/* onOrder */*/
    private static method onOrder takes nothing returns nothing
        local unit u = GetTriggerUnit()
        local integer id = GetUnitId(u)
        
            // onOrder occurs after onEnter
            
        /*
            NB: when units are unloaded from a transport, they fire a stop order (see below)
            When units are removed from the game or die, they fire an undefend order.
        */
        
        // If id is not zero then the unit has been indexed.
        if id > 0 then
            
            // Detect Cargo
            static if DETECT_CARGO then
                
                if GetIssuedOrderId() == 851972 then // order stop
                    
                    // This does not detect unloaded corpses.
                    if CargoUnit[id] != null and not IsUnitLoaded(u) or UnitAlive(u) then
                        call thistype.unload(u)
                    endif
                    
                    set u = null
                    return
                endif
                
            endif
            
            // Detect Morph
            static if DETECT_TRANSFORM then
                
                if GetUnitAbilityLevel(u, DETECT_TRANSFORM_ABILITY) == 0 and not IsTransforming[id] then
                    // re-adding DETECT_TRANSFORM_ABILITY immediately doesn't work, so it has to be
                    // done after a zero-second timer.
                    set IsTransforming[id] = true
                    call thistype.timerCheck(u)
                    
                    set u = null
                    return
                endif
                
            endif
            
            // Detect Revives
            static if DETECT_REVIVES then
            
                // If unit was not previously alive and ...
                if not IsAlive[id] and UnitAlive(u) then
                    
                    set IsAlive[id] = true
                        
                    // ... it is also a summoned unit then it has been Reanimated.
                    if IsUnitType(u, UNIT_TYPE_SUMMONED) then
                    
                        static if DETECT_ANIMATE_DEAD then
                            call FireEvent(EVENT_ON_ANIMATE_DEAD, u, null)
                        endif
                        
                    // ... IsReincarnating[id] is true then it is done reincarnating.
                    elseif IsReincarnating[id] then
                    
                        static if DETECT_REINCARNATION then
                            call FireEvent(EVENT_ON_REINCARNATION_FINISH, u, null)
                        endif
                        
                    // ... neither of the above two conditions have been met, it has been resurrected.
                    elseif not IsNew[id] and not rezCheck then
                        
                        static if DETECT_RESURRECTION then
                            call FireEvent(EVENT_ON_RESURRECTION, u, null)
                        endif
                    
                    endif
                
                // Else if the unit is dead then ...
                elseif not UnitAlive(u) then
                    
                    // if unit was just indexed then it was created as a corpse.
                    if IsNew[id] then
                        set IsAlive[id] = false
                        
                    // else if either of these parameters are true then ...
                    elseif CargoUnit[id] == null or not IsUnitType(u, UNIT_TYPE_HERO) then
                    
                        /* order undefend fires before a UNIT_DEATH event, so if the unit is not
                        alive, yet IsAlive[id] is true, that means the unit is potentially reincar-
                        nating. Fire a zero-second timer to give time for the UNIT_DEATH event to
                        trigger. If IsAlive[id] is still true, then the unit is reincarnating. */
                        if IsAlive[id] then
                            call thistype.timerCheck(u)
                        endif
                        
                    endif
                
                endif
                
            endif // static if DETECT_REVIVES then
            
        endif
        
        set u = null
    endmethod
    
    
    /*/* onDeath */*/
    private static method onDeath takes nothing returns nothing
        local unit u = GetTriggerUnit()
        local integer id = GetUnitId(u)
        
        // This checks if the unit has been indexed.
        if id > 0 then
            set IsAlive[id] = false
        endif
        
        static if DETECT_CARGO then
            if CargoUnit[id] != null then
                call FireEvent(EVENT_ON_CARGO_UNLOAD, u, CargoUnit[id])
                set CargoUnit[id] = null
            endif
        endif
        
        set u = null
    endmethod
    
    
    /*/* onLoad */*/
    private static method onLoad takes nothing returns nothing
        local unit u = GetTriggerUnit()
        local integer id = GetUnitId(u)
        local integer cargo_id
        
        // if unit somehow loaded into a transport while being inside another, unload it
        if CargoUnit[id] != null then
            call thistype.unload(u)
        endif
        
        static if DETECT_CARGO_DEAD then
            // if a Meat Wagon loads up a corpse either by grabbing one on the ground or via Exhume
            // Corpse, that corpse is sent to the edge of the map by this system. This way, when it
            // is unloaded and placed back at the Meat Wagon's location, it fires an onEnter event,
            // which allows the system to detect when a corpse was unloaded from a transport.
            if not UnitAlive(u) then
                if LIBRARY_WorldBounds then
                    call SetUnitX(u, WorldBounds.maxX)
                    call SetUnitY(u, WorldBounds.maxY)
                else
                    call SetUnitX(u, MaxX)
                    call SetUnitY(u, MaxY)
                endif
            endif
        endif
        
        set CargoUnit[id] = GetTransportUnit()
        set cargo_id = GetUnitId(CargoUnit[id])
        
        if not Cargo.exists(cargo_id) then
            call Cargo.create(cargo_id)
        endif
        call Cargo.add(u, CargoUnit[id])
        
        set u = null
    endmethod
    
    
    /*/* onEnter */*/
    private static method onEnter takes nothing returns nothing
        local unit u = GetFilterUnit()
        local integer id = GetUnitId(u)
        local integer cargo_id = GetUnitId(CargoUnit[id])
        
            // onEnter occurs AFTER onIndex
            
        // The unit was dead, but has re-entered the map. Used to detect when a Meat Wagon unloads a corpse.
        if id > 0 then
            if not IsUnitLoaded(u) and CargoUnit[id] != null then
                call thistype.unload(u)
            endif
        endif
        
        set u = null
    endmethod
    
    
    /*/* onIndex */*/
    private static method onIndex takes nothing returns nothing
        local unit u = GetIndexedUnit()
        local integer id = GetUnitId(u)
        
            // onIndex occurs BEFORE onEnter
        
        set IsNew[id] = true
        
        static if DETECT_TRANSFORM then
            set IsTransforming[id] = false
            set PreTransformType[id] = GetUnitTypeId(u)
            call UnitAddAbility(u, DETECT_TRANSFORM_ABILITY)
        endif
        
        static if DETECT_REINCARNATION and DETECT_REVIVES then
            set IsReincarnating[id] = false
        endif
        
        if UnitAlive(u) then
            set IsAlive[id] = true
        else
            set IsAlive[id] = false
        endif
        
        // This is called here so as to set the variable IsNew[] to false after 0. seconds.
        call thistype.timerCheck(u)
        
        set u = null
    endmethod
    
    
    /*/* onDeindex */*/
    private static method onDeindex takes nothing returns nothing
        local unit u = GetIndexedUnit()
        local integer id = GetUnitId(u)
        
        if Cargo.exists(id) then
            call Cargo.delete(id)
        endif
        
        set u = null
    endmethod
    
    
    private static method onInit takes nothing returns nothing
        local integer i
        
        static if DETECT_CARGO and DETECT_CARGO_DEAD then
            static if (not LIBRARY_WorldBounds) then
                local rect world = GetWorldBounds()
                local region reg = CreateRegion()
                set MaxX = GetRectMaxX(world)
                set MaxY = GetRectMaxY(world)
            endif
        endif
        
        set EVENT_ON_CARGO_LOAD             = CreateNativeEvent()
        set EVENT_ON_CARGO_UNLOAD           = CreateNativeEvent()
        set EVENT_ON_TRANSFORM              = CreateNativeEvent()
        set EVENT_ON_ANIMATE_DEAD           = CreateNativeEvent()
        set EVENT_ON_RESURRECTION           = CreateNativeEvent()
        set EVENT_ON_REINCARNATION_START    = CreateNativeEvent()
        set EVENT_ON_REINCARNATION_FINISH   = CreateNativeEvent()
        set EVENT_ON_UNIT_CREATED           = CreateNativeEvent()
        
        call RegisterAnyPlayerUnitEvent(EVENT_PLAYER_UNIT_ISSUED_ORDER, function thistype.onOrder)
        call RegisterAnyPlayerUnitEvent(EVENT_PLAYER_UNIT_DEATH, function thistype.onDeath)
        
        static if DETECT_CARGO then
            
            static if DETECT_CARGO_DEAD then
            
                static if (not LIBRARY_WorldBounds) then
                    call RegionAddRect(reg, world)
                    call TriggerRegisterEnterRegion(CreateTrigger(), reg, function thistype.onEnter)
                    call RemoveRect(world)
                    set world = null
                    set reg = null
                else
                    call TriggerRegisterEnterRegion(CreateTrigger(), WorldBounds.worldRegion, function thistype.onEnter)
                endif
                call RegisterAnyPlayerUnitEvent(EVENT_PLAYER_UNIT_LOADED, function thistype.onLoad)
            
            endif
            call RegisterUnitIndexEvent(Condition(function thistype.onDeindex), EVENT_UNIT_DEINDEX)
            
        endif
        
        call RegisterUnitIndexEvent(Condition(function thistype.onIndex), EVENT_UNIT_INDEX)
        
        static if DETECT_TRANSFORM then
            set i = 0
            loop
                call SetPlayerAbilityAvailable(Player(i), DETECT_TRANSFORM_ABILITY, false)
                set i = i + 1
                exitwhen i == bj_MAX_PLAYER_SLOTS
            endloop
        endif
        
        // see resurrectionTimer below.
        call TimerStart(CreateTimer(), 0., false, function thistype.resurrectionTimer)
        
    endmethod
    
    // for some reason dummy recyclers creating dummies to store fires off a resurrection event, so
    // this boolean rezCheck is set to false after a 0. second timer to prevent this from happening.
    // rezCheck must be false for a resurrection event to happen.
    private static method resurrectionTimer takes nothing returns nothing
        set rezCheck = false
        call DestroyTimer(GetExpiredTimer())
    endmethod
    
endmodule
private struct UnitEventEx
    implement UnitEventExCore
endstruct
endlibrary

Demo:
JASS:
scope onUnitEventsEx

private function onLoad takes nothing returns nothing
    local unit transport = GetCargoUnit()
    call BJDebugMsg(GetUnitName(GetEventUnit()) + " was loaded into " + GetUnitName(transport))
    set transport = null
endfunction

private function onUnload takes nothing returns nothing
    local unit transport = GetCargoUnit()
    call BJDebugMsg(GetUnitName(GetEventUnit()) + " has disembarked from " + GetUnitName(transport))
    set transport = null
endfunction

private function onReincarnateStart takes nothing returns nothing
    call BJDebugMsg(GetUnitName(GetEventUnit()) + " has begun reincarnating.")
endfunction

private function onReincarnateFinish takes nothing returns nothing
    call BJDebugMsg(GetUnitName(GetEventUnit()) + " has reincarnated.")
endfunction

private function onAnimate takes nothing returns nothing
    call BJDebugMsg(GetUnitName(GetEventUnit()) + " has been reanimated.")
endfunction

private function onResurrect takes nothing returns nothing
    call BJDebugMsg(GetUnitName(GetEventUnit()) + " has been resurrected.")
endfunction

private function onMorph takes nothing returns nothing
    call BJDebugMsg(GetUnitName(GetEventUnit()) + " has finished morphing.")
endfunction

private function onCreated takes nothing returns nothing
    call BJDebugMsg(GetUnitName(GetEventUnit()) + " has finished morphing.")
endfunction

struct onUnitEventsEx
    private static method onInit takes nothing returns nothing
 
        call RegisterNativeEvent(EVENT_ON_CARGO_LOAD, function onLoad)
        call RegisterNativeEvent(EVENT_ON_CARGO_UNLOAD, function onUnload)
        call RegisterNativeEvent(EVENT_ON_REINCARNATION_START, function onReincarnateStart)
        call RegisterNativeEvent(EVENT_ON_REINCARNATION_FINISH, function onReincarnateFinish)
        call RegisterNativeEvent(EVENT_ON_ANIMATE_DEAD, function onAnimate)
        call RegisterNativeEvent(EVENT_ON_RESURRECTION, function onResurrect)
        call RegisterNativeEvent(EVENT_ON_TRANSFORM, function onMorph)
        call RegisterNativeEvent(EVENT_ON_UNIT_CREATED, function onCreated)
 
    endmethod
endstruct
 
endscope
 

Attachments

  • UnitEventEx v1.03.1.w3x
    68.9 KB · Views: 7
Last edited:
Groups vs lists
Does one run faster than the other? Is that even relevant? If I ever learn how to code a linked list, I'm going to try to make my own and make ListT optional.
At mission 12/13 here, I tried to explain a bit when a (doubly-) linked list can be used. In short I would say, if you need data you're required to loop over and you want ensure correct order in the data structure, then you can use it. Else, other data structures, or simply data attaching to a timer is usually more straight forward.
Groups I use, too, when I just need some container for units and/or want to simply loop through it at some time, or group loops for temp enums are for sure good, too. If locals are needed then such tequnique is mostly used for group enumerations: vJass Optimization: Using a First of Group Loop for Enumeration

Static Ifs
In trying to make this code modular, I ended up with a lot of static ifs everywhere and I'm not even sure that's necessary, given they're placed after if blocks. I suppose preventing an unnecessary TriggerEvaluate does save some performance, but do you think it's worth doing?
In general I would drop static ifs as much as possible, and would try to shorten such optional stuff that requires configuration if not really needed. Only if very needed, like maybe one user should carefully choose which feature to use, to fight performance issue (what I don't think is the issue here), I would make things optional. Else, just providing is simpler --> better.

Getters
What other types of Get-type functions would this code benefit from?
Can/do you provide the unit type id the unit had before transformation?

If you think of anything else to comment on, please feel free.
Dunno if it's needed, but maybe also some other events that fall into mind that could be useful could be added if it makes sense to group them together, like: HeroReviveCancelEvent v1.1. I also like if such resources are seperated naturally, but bundling such extra unit events does make good sense, too.
 
+1 to everything IcemanBo said.

In general, don't focus too much on micro-optimizations. Unit groups work well for storing and iterating units, so you should use them unless there is a noticeable bottleneck. I don't think you should manage your own linked list for units unless you need to maintain an order for iteration.

As for static ifs, I think you should only use them if they: (1) make sense for the system, or (2) are simple to implement. If it ends up making your code branch off in two different directions, then you should definitely pick one and ditch the other. In the past, we used static ifs in our JASS section to:
  1. Have optional requirements
  2. Remove unnecessary code
  3. Create "flavors" for our systems (e.g. a "light" version)
I used to support them, but man they really make code hard to maintain and hard to read. #1 can be justified, but only if it is a simple static if. If you have to end up scattering it throughout your code, just make the requirement a requirement. #2 can be done by optimizers. #3 makes it hard to maintain a system, since you have to support all the "flavors" you implement. And then if a library uses your system, it may have to be conscious of all those flavors... It is an anti-pattern.

My recommendation to you: try removing the static ifs and see if the performance takes a big hit. Try making a test map where units are being ordered stuff every 1 sec or so. And occasionally kill them. You can try a stress test too. Compare it with and without the static ifs--if the performance is noticeably worse, you should consider keeping the static ifs so the person using your system has finite control over what is happening. If it is roughly the same, you might as well just include all the code. A lot of the code is guarded by if statements, so even if you have all the detections enabled, it won't really be doing much work most of the time.
 
I was wondering if I should post that in The Lab... so if you think it would be better suited there, I'm cool with it. I've been discussing this with Bribe in PMs so I'm assuming he's ok with it as well?

@IcemanBo I'll be sure to read the mission when I get a moment.

Concerning the static ifs, I might do a stress test eventually, but considering they're already there... I might as well leave them in. The reasoning I'm following is that, say you don't need to detect transform events. You set DETECT_TRANSFORM to false so this way there isn't a needless TriggerEvaluate called whenever a unit morphs.

That being said, I do understand the micro-optimisation vs more readable code... I've always been very concerned about performance, so I guess it can't be helped? It's the reason I moved away from GUI in the first place.

I'll think about it. It's also entirely possible that I would rewrite the library if I find a way to make each event contained in its own function and more readable in itself.
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Mind if this gets moved to the Lab, and if it's okay with @Bribe?
Most of my stuff is just other peoples' stuff as a foundation and expanding on it. Damage Engine and Spell System have a lot of unique ideas that weren't from the systems they were inspired by, but in Unit Event the only ideas that were mine were Transform detection and the unload-detect-by-stop-order method.

Other resources like Table I just made more functional. Then there's a wall of GUI resources I made just so that GUI users won't keep getting shafted by insufficient quality resources. See: Unit Event, Damage Engine, Spell System, Item Cleanup, probably all of them in fact (though Spell System, Unit Event and Damage Engine are better than any vJass solution due to no one having had done it better).

Unless Spellbound adds more features (which he already has via static ifs) then it wouldn't be worth the port. However, as I've explained in PMs, Spellbound should just integrate the indexer (he can keep the API the same) so that he has more control over events and there is less repeated code. This is why later versions of Unit Event actually replace GUI Unit Indexer.
 
So as per Bribe's recommendation, I've integrated a unit indexer in ExtraEvents, which means it doesn't need UnitDex anymore. I have no idea how to code a linked list so I copied the one in GUI Unit Event directly. I hope you don't mind, Bribe :\

You can now also catch the unit type of a unit on transform events. Since transform events fire after the morph, the unit even will always return the resulting morph. With GetTransformPreType() it will return, for example, a Druid of the Claw (Night Elf Form) after it has morphed into a bear.

JASS:
/*
    ExtraEvents v1.01
    by Spellbound
   
    Credits to Bribe for the excellent GUI Unit Event, of which this library is the vJASS
    version of. He was also helpful in understanding how his system works so that I may code this.
*/
/*
    _________________________________________________________________________
    DESCRIPTION
    ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
    ExtraEvents provides you with extra events not available in the vanilla game. Specifically, you
    will be able to detect when a unit transforms, loads and unloads from a transport unit, is
    reanimated by Reanimate Dead, reincarnates or is brought back to life.
   
    Additionally, each of those events can be toggled on or off depending on whether you need them.
    See // CONFIGURATION under the globals block below.
*/
/*
    _________________________________________________________________________
    INSTALLATION
    ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
    ExtraEvents requires:
   
    RegisterEvent Pack by Bannar (both RegisterNativeEvent and RegisterPlayerUnitEvent)
    https://www.hiveworkshop.com/threads/snippet-registerevent-pack.250266/
   
    ListT by Bannar
    https://www.hiveworkshop.com/threads/containers-list-t.249011/
   
    Once you have the required libraries in place, either copy the Detect Transform (ExtraEvents)
    ability to your map and set DETECT_TRANSFORM_ABILITY in the globals block below to the id of 
    the spell.
   
    Alternatively, you can use the textmacro below. Import ExtraEvents, uncomment it, save your map, 
    close it, reload and comment it out again. The ability will have been created in your Object Editor.
*/
    // Detect Remove ability
    // //! external ObjectMerger w3a Adef EEdr anam "Detect Remove" ansf "(ExtraEvents)" aart "" acat "" arac 0
   
    // Detect Transform ability
    // //! external ObjectMerger w3a Adef EEdt anam "Detect Transform" ansf "(ExtraEvents)" aart "" acat "" arac 0
/*
    _________________________________________________________________________
    CONFUGRATION
    ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
    ExtraEvents can be configured to exclude those events you do not wish to use. Define which
    events will be needed in the CONFIG block below. 
*/
/*
    _________________________________________________________________________
    API
    ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
    call RegisterUnitEvent(yourEvent, function yourFunction )
   
   
    UNIT INDEXER
    ¯¯¯¯¯¯¯¯¯¯¯¯
    call GetUnitId()
        returns the index of the unit. 
   
    call GetIndexedUnit()
        returns the unit that was indexed/deindexed during EVENT_UNIT_INDEX or EVENT_UNIT_DEINDEX.
        NB: GetEventUnit() also returns the same unit during that event.

    call EnableIndexer( boolean flag )
        toggles the unit indexer on or off. True will enable, false will disable.

    call GetUnitById( integer index )
        returns the unit based on the index.

    call GetIndexedUnitId()
        returns the index of a unit during EVENT_UNIT_INDEX or EVENT_UNIT_DEINDEX

    call IsUnitIndexed( unit yourUnit )
        returns true if a unit is indexed.
   
    You indexing events are:
   
    EVENT_UNIT_INDEX
    EVENT_UNIT_DEINDEX
   
   
    EXTRA EVENTS
    ¯¯¯¯¯¯¯¯¯¯¯¯
    call GetEventUnit()
        returns the event unit, similar to GetTriggerUnit().
        MB: GetTriggerUnit() will NOT work with ExtraEvents.
       
    call GetCargoUnit()
        returns the transport unit. 
        NB: Only works on EVENT_ON_CARGO_LOAD and EVENT_ON_CARGO_UNLOAD
       
    call GetCargoSize( unit yourTransport )
        returns the number of units loaded in a transport.
        NB: Only works on EVENT_ON_CARGO_LOAD and EVENT_ON_CARGO_UNLOAD
       
    call GetTransformPreType( unit yourUnit )
        returns the previous unit type id of a unit after a transform event.
        NB: Transform events fire AFTER the morph has ended, so the PreType of Storm Crow form
        would be the Druid of the Talon.
       
    The Events you can call are:
   
    EVENT_ON_TRANSFORM
    EVENT_ON_CARGO_LOAD
    EVENT_ON_CARGO_UNLOAD
    EVENT_ON_RESURRECTION
    EVENT_ON_ANIMATE_DEAD
    EVENT_ON_REINCARNATION_START
    EVENT_ON_REINCARNATION_FINISH
*/

library ExtraEvents requires RegisterPlayerUnitEvent, ListT optional WorldBounds

globals
   
    // CONFIGURATION
   
    // if you do not want the indexer to overwrite custom values, set this to true
    // a bit slower than array lookups.
    private constant boolean    INDEX_WITH_HASHTABLE    = false
   
    // the transform detection ability. If DETECT_TRANSFORM is false, this is not needed.
    private constant integer    DETECT_REMOVE_ABILITY   = 'EEdr'
   
    // the transform detection ability. If DETECT_TRANSFORM is false, this is not needed.
    private constant integer    DETECT_TRANSFORM_ABILITY= 'EEdt'
   
    // toggles the detection of transform events.
    private constant boolean    DETECT_TRANSFORM        = true
   
    // toggles the detection of load/unload of cargo units (including dead units from Meat Wagons).
    private constant boolean    DETECT_CARGO            = true
   
    // toggles the detection of when a unit begins and finishes reincarnating.
    private constant boolean    DETECT_REINCARNATION    = true
   
    // toggles the detection of when a unit is animated via animate dead.
    private constant boolean    DETECT_ANIMATE_DEAD     = true
   
    // toggles the detection of units that are brought back to life via resurrection.
    private constant boolean    DETECT_RESURRECTION     = true
   
    // this overrides reincarnation, animate dead and resurrection. Set to true if you want any of these events to work.
    // for some reason setting DETECT_REVIVES = (DETECT_REINCARNATION or DETECT_ANIMATE_DEAD or DETECT_RESURRECTION) does not work.
    private constant boolean    DETECT_REVIVES          = true
   
    // END CONFIGURATION
   
    private unit eventUnit = null
    private unit eventOther = null
    private integer eventPreType = 0
   
    integer EVENT_ON_TRANSFORM
    integer EVENT_ON_CARGO_LOAD
    integer EVENT_ON_CARGO_UNLOAD
    integer EVENT_ON_RESURRECTION
    integer EVENT_ON_ANIMATE_DEAD
    integer EVENT_ON_REINCARNATION_START
    integer EVENT_ON_REINCARNATION_FINISH
   
    integer EVENT_UNIT_INDEX
    integer EVENT_UNIT_DEINDEX
   
    private boolean IsIndexerEnabled = true
    private integer LastIndex = 0
    private integer MaxIndex = 0
    private integer array PrevIndex
    private integer array NextIndex
    private unit array IndexedUnits
   
    private integer Stack = -1
    private unit array IndexedUnit
    private unit array CargoUnit
    private UnitList array CargoList
    private integer array PreTransformType
   
    private real MaxX
    private real MaxY
   
    private boolean array IsNew
    private boolean array IsAlive
    private boolean array IsReincarnating
    private boolean array IsTransforming
   
    private timer AfterIndexTimer = CreateTimer()
    private boolean rezCheck = true
    private hashtable IndexHash
   
endglobals

native UnitAlive takes unit u returns boolean

//! runtextmacro DEFINE_LIST("", "UnitList", "unit")

// Indexer

function GetUnitId takes unit u returns integer
    static if INDEX_WITH_HASHTABLE then
        return LoadInteger(IndexHash, GetHandleId(u), 0)
    else
        return GetUnitUserData(u)
    endif
endfunction

function GetIndexedUnit takes nothing returns unit
    return eventUnit
endfunction

function EnableIndexer takes boolean flag returns nothing
    set IsIndexerEnabled = flag
endfunction

function GetUnitById takes integer index returns unit
    return IndexedUnits[index]
endfunction

function GetIndexedUnitId takes nothing returns integer
    return GetUnitId(GetIndexedUnit())
endfunction

function IsUnitIndexed takes unit u returns boolean
    return (GetUnitById(GetUnitId(u)) != null)
endfunction

// Extra Events

function GetEventUnit takes nothing returns unit
    return eventUnit
endfunction

function GetTransformPreType takes unit u returns integer
    return eventPreType
endfunction

function GetCargoUnit takes nothing returns unit
    return eventOther
endfunction

function GetCargoSize takes unit cargo returns integer
    return CargoList[GetUnitId(cargo)].size()
endfunction

private function FireEvent takes integer ev, unit u, unit other returns nothing
    local integer playerId = GetPlayerId(GetOwningPlayer(u))
    local integer id = GetUnitId(u)
    local unit prevUnit = eventUnit
    local unit prevOther = eventOther
    local integer prevType = eventPreType
   
    set eventUnit = u
    set eventOther = other
    set eventPreType = PreTransformType[id]
   
    call TriggerEvaluate(GetNativeEventTrigger(ev))
    if IsNativeEventRegistered(playerId, ev) then
        call TriggerEvaluate(GetIndexNativeEventTrigger(playerId, ev))
    endif
   
    set eventUnit = prevUnit
    set eventOther = prevOther
    set eventPreType = prevType
   
    set prevUnit = null
    set prevOther = null
endfunction

private module ExtraEventsCore
   
    /*/* afterIndex */*/
    private static method afterIndex takes nothing returns nothing
        local integer i = Stack
        local integer id
        local unit u
       
        loop
            exitwhen i < 0
            set u = IndexedUnit[i]
            set id = GetUnitId(u)
           
            if IsNew[id] then
                set IsNew[id] = false
               
            elseif IsTransforming[id] then
               
                static if DETECT_TRANSFORM then
                    call FireEvent(EVENT_ON_TRANSFORM, u, null)
                    set IsTransforming[id] = false
                    // The unit has finished transforming. Store it's new type id.
                    set PreTransformType[id] = GetUnitTypeId(u)
                    call UnitAddAbility(u, DETECT_TRANSFORM_ABILITY)
                endif
               
            elseif IsAlive[id] then
           
                static if DETECT_REINCARNATION then
                    set IsReincarnating[id] = true
                    set IsAlive[id] = false
                    call FireEvent(EVENT_ON_REINCARNATION_START, u, null)
                endif
               
            endif
           
            set IndexedUnit[i] = null
            set i = i - 1
        endloop
        set Stack = -1
           
        set u = null
    endmethod
   
   
    /*/* timerCheck */*/
    private static method timerCheck takes unit u returns nothing
        set Stack = Stack + 1
        set IndexedUnit[Stack] = u
        call TimerStart(AfterIndexTimer, 0., false, function thistype.afterIndex)
    endmethod
   
   
    /*/* unload */*/
    private static method unload takes unit u returns nothing
        local integer id = GetUnitId(u)
        local integer cargo_id = GetUnitId(CargoUnit[id])
       
        call CargoList[cargo_id].removeElem(u)
        call FireEvent(EVENT_ON_CARGO_UNLOAD, u, CargoUnit[id])
       
        if not IsUnitLoaded(u) or not UnitAlive(CargoUnit[id]) then
            set CargoUnit[id] = null
        endif
    endmethod
   
   
    /*/* onOrder */*/
    private static method onOrder takes nothing returns nothing
        local unit u = GetTriggerUnit()
        local integer id = GetUnitId(u)
       
            // onOrder occurs after onEnter
           
        /*
            NB: When units are removed from the game or die, they fire an undefend order.
            When units are unloaded from a transport, they fire a stop order (see Detect Cargo)
        */
       
        if id > 0 then
       
            // If DETECT_REMOVE_ABILITY returns zero then it has been removed from the game.
            // Deindex it.
            if GetUnitAbilityLevel(u, DETECT_REMOVE_ABILITY) == 0 then
               
                set IsAlive[id] = false
           
                set NextIndex[PrevIndex[id]] = NextIndex[id]
                set PrevIndex[NextIndex[id]] = PrevIndex[id]
           
                // Recycle the index for later use
                set IndexedUnits[id] = null
                set PrevIndex[id] = LastIndex
                set LastIndex = id
               
                static if INDEX_WITH_HASHTABLE then
                    call RemoveSavedInteger(IndexHash, GetHandleId(u), 0
                else
                    call SetUnitUserData(u, 0)
                endif
               
                call FireEvent(EVENT_UNIT_DEINDEX, u, null)
               
                if CargoList[id] != 0 then
                    call CargoList[id].destroy()
                endif
               
            else
                // Detect Cargo
                static if DETECT_CARGO then
                   
                    if GetIssuedOrderId() == 851972 then // order stop
                       
                        // This does not detect unloaded corpses.
                        if CargoUnit[id] != null and not IsUnitLoaded(u) or UnitAlive(u) then
                            call thistype.unload(u)
                        endif
                       
                        set u = null
                        return
                    endif
                   
                endif
               
                // Detect Morph
                static if DETECT_TRANSFORM then
                   
                    if GetUnitAbilityLevel(u, DETECT_TRANSFORM_ABILITY) == 0 and not IsTransforming[id] then
                        // re-adding DETECT_TRANSFORM_ABILITY immediately doesn't work, so it has to be
                        // done after a zero-second timer.
                        set IsTransforming[id] = true
                        call thistype.timerCheck(u)
                       
                        set u = null
                        return
                    endif
                   
                endif
               
                // Detect Revives
                static if DETECT_REVIVES then
               
                    // If unit was not previously alive and ... 
                    if not IsAlive[id] and UnitAlive(u) then
                       
                        set IsAlive[id] = true
                           
                        // ... it is also a summoned unit then it has been Reanimated.
                        if IsUnitType(u, UNIT_TYPE_SUMMONED) then
                       
                            static if DETECT_ANIMATE_DEAD then
                                call FireEvent(EVENT_ON_ANIMATE_DEAD, u, null)
                            endif
                           
                        // ... IsReincarnating[id] is true then it is done reincarnating.
                        elseif IsReincarnating[id] then
                       
                            static if DETECT_REINCARNATION then
                                call FireEvent(EVENT_ON_REINCARNATION_FINISH, u, null)
                            endif
                           
                        // ... neither of the above two conditions have been met, it has been resurrected.
                        elseif not IsNew[id] and not rezCheck then
                           
                            static if DETECT_RESURRECTION then
                                call FireEvent(EVENT_ON_RESURRECTION, u, null)
                            endif
                       
                        endif
                   
                    // Else if the unit is dead then ...
                    elseif not UnitAlive(u) then
                       
                        // if unit was just indexed then it was created as a corpse.
                        if IsNew[id] then
                            set IsAlive[id] = false
                           
                        // else if either of these parameters are true then ...
                        elseif CargoUnit[id] == null or not IsUnitType(u, UNIT_TYPE_HERO) then
                       
                            /* order undefend fires before a UNIT_DEATH event, so if the unit is not
                            alive, yet IsAlive[id] is true, that means the unit is potentially reincar-
                            nating. Fire a zero-second timer to give time for the UNIT_DEATH event to 
                            trigger. If IsAlive[id] is still true, then the unit is reincarnating. */
                            if IsAlive[id] then
                                call thistype.timerCheck(u)
                            endif
                           
                        endif
                   
                    endif
                   
                endif // static if DETECT_REVIVES then
               
            endif //
       
        endif // id > 0
       
        set u = null
    endmethod
   
   
    /*/* onDeath */*/
    private static method onDeath takes nothing returns nothing
        local unit u = GetTriggerUnit()
        local integer id = GetUnitId(u)
       
        // This checks if the unit has been indexed.
        if id > 0 then
            set IsAlive[id] = false
        endif
       
        static if DETECT_CARGO then
            if CargoUnit[id] != null then
                call FireEvent(EVENT_ON_CARGO_UNLOAD, u, CargoUnit[id])
                set CargoUnit[id] = null
            endif
        endif
       
        set u = null
    endmethod
   
   
    /*/* onLoad */*/
    private static method onLoad takes nothing returns nothing
        local unit u = GetTriggerUnit()
        local integer id = GetUnitId(u)
        local integer cargo_id
       
        // if unit somehow loaded into a transport while being inside another, unload it
        if CargoUnit[id] != null then
            call thistype.unload(u)
        endif
       
        static if DETECT_CARGO then
            // if a Meat Wagon loads up a corpse either by grabbing one on the ground of via Exhume
            // Corpse, that corpse is sent to the edge of the map by this system. This way, when it
            // is unloaded and placed back at the Meat Wagon's location, it fires an onEnter event, 
            // which allows the system to detect when a corpse was unloaded from a transport.
            if not UnitAlive(u) then
                if LIBRARY_WorldBounds then
                    call SetUnitX(u, WorldBounds.maxX)
                    call SetUnitY(u, WorldBounds.maxY)
                else
                    call SetUnitX(u, MaxX)
                    call SetUnitY(u, MaxY)
                endif
            endif
        endif
       
        set CargoUnit[id] = GetTransportUnit()
        set cargo_id = GetUnitId(CargoUnit[id])
        if CargoList[cargo_id] == null then
            set CargoList[cargo_id] = UnitList.create()
        endif
        call CargoList[cargo_id].push(u)
        call FireEvent(EVENT_ON_CARGO_LOAD, u, CargoUnit[id])
       
        set u = null
    endmethod
   
   
    /*/* onEnter */*/
    private static method onEnter takes nothing returns nothing
        local unit u = GetFilterUnit()
        local integer id = GetUnitId(u)
        local integer i
       
            // onEnter occurs AFTER onIndex
       
        // if id == 0 then the unit has not been indexed.
        if IsIndexerEnabled and id == 0 then
           
            // INDEXING
           
            set i = LastIndex
           
            //Generate a unique integer index for this unit
            if i == 0 then
                set i = MaxIndex + 1
                set MaxIndex = i
            else
                set LastIndex = PrevIndex[i]
            endif
           
            //Link index to unit, unit to index
            set IndexedUnits[i] = u
           
            static if INDEX_WITH_HASHTABLE then
                call SaveInteger(IndexHash, GetHandleId(u), 0, i)
            else
                call SetUnitUserData(u, i)
            endif
           
            // add the unit to a linked list
            set NextIndex[i] = NextIndex[0]
            set PrevIndex[NextIndex[0]] = i
            set NextIndex[0] = i
            set PrevIndex[i] = 0
           
            call UnitAddAbility(u, DETECT_REMOVE_ABILITY)
            call UnitMakeAbilityPermanent(u, true, DETECT_REMOVE_ABILITY)
           
            call FireEvent(EVENT_UNIT_INDEX, u, null)
           
            // END INDEXING
       
            set IsNew[i] = true
       
            static if DETECT_TRANSFORM then
                set PreTransformType[i] = GetUnitTypeId(u)
                set IsTransforming[i] = false
                call UnitAddAbility(u, DETECT_TRANSFORM_ABILITY)
            endif
           
            static if DETECT_REINCARNATION and DETECT_REVIVES then
                set IsReincarnating[i] = false
            endif
           
            if UnitAlive(u) then
                set IsAlive[i] = true
            else
                set IsAlive[i] = false
            endif
           
            // This is called here so as to set the variable IsNew[] to false after 0. seconds.
            call thistype.timerCheck(u)
           
        // The unit was dead but has re-entered the map. Used to detect when a Meat Wagon unloads
        // a corpse.
        else
            if not IsUnitLoaded(u) and CargoUnit[id] != null then
                call thistype.unload(u)
            endif
        endif
       
        set u = null
    endmethod
   
   
    static method onInit takes nothing returns nothing
        local integer i
        local group g = CreateGroup()
        local player p
        local boolexpr enter = Filter(function thistype.onEnter)
       
        static if (not LIBRARY_WorldBounds) then
            local rect world = GetWorldBounds()
            local region reg = CreateRegion()
            set MaxX = GetRectMaxX(world)
            set MaxY = GetRectMaxY(world)
        endif
       
        static if INDEX_WITH_HASHTABLE then
            set IndexHash = InitHashtable()
        endif
       
        set EVENT_ON_CARGO_LOAD             = CreateNativeEvent()
        set EVENT_ON_CARGO_UNLOAD           = CreateNativeEvent()
        set EVENT_ON_TRANSFORM              = CreateNativeEvent()
        set EVENT_ON_ANIMATE_DEAD           = CreateNativeEvent()
        set EVENT_ON_RESURRECTION           = CreateNativeEvent()
        set EVENT_ON_REINCARNATION_START    = CreateNativeEvent()
        set EVENT_ON_REINCARNATION_FINISH   = CreateNativeEvent()
       
        set EVENT_UNIT_INDEX                = CreateNativeEvent()
        set EVENT_UNIT_DEINDEX              = CreateNativeEvent()
       
        call RegisterAnyPlayerUnitEvent(EVENT_PLAYER_UNIT_ISSUED_ORDER, function thistype.onOrder)
        call RegisterAnyPlayerUnitEvent(EVENT_PLAYER_UNIT_DEATH, function thistype.onDeath)
       
        // if you do not need to detect load/unload then there's no point in creating that event.
        static if DETECT_CARGO then
            call RegisterAnyPlayerUnitEvent(EVENT_PLAYER_UNIT_LOADED, function thistype.onLoad)
        endif
       
        static if (not LIBRARY_WorldBounds) then
            call RegionAddRect(reg, world)
            call TriggerRegisterEnterRegion(CreateTrigger(), reg, enter)
            call RemoveRect(world)
            set world = null
            set reg = null
        else
            call TriggerRegisterEnterRegion(CreateTrigger(), WorldBounds.worldRegion, enter)
        endif
       
        set i = 0
        loop
            set p = Player(i)
           
            // Hide the detect remove ability from players
            call SetPlayerAbilityAvailable(p, DETECT_REMOVE_ABILITY, false)
           
            // Hide the detect transform ability from players
            static if DETECT_TRANSFORM then
                call SetPlayerAbilityAvailable(p, DETECT_TRANSFORM_ABILITY, false)
            endif
           
            call GroupEnumUnitsOfPlayer(g, p,enter)
           
            set i = i + 1
            exitwhen i == bj_MAX_PLAYER_SLOTS
        endloop
       
        set p = null
       
        // see resurrectionTimer below
        call TimerStart(CreateTimer(), 0., false, function thistype.resurrectionTimer)
       
    endmethod
   
    // for some reason dummy recyclers creating dummies to store fires off a resurrection event, so
    // this boolean rezCheck is set to false after a 0. second timer to prevent this from happening.
    // rezCheck must be false for a resurrection event to happen.
    static method resurrectionTimer takes nothing returns nothing
        set rezCheck = false
        call DestroyTimer(GetExpiredTimer())
    endmethod
   
endmodule

struct ExtraEvents
    implement ExtraEventsCore
endstruct

endlibrary
 

Bannar

Code Reviewer
Level 26
Joined
Mar 19, 2008
Messages
3,140
Please, do not mix unit indexer with other unit events. This is plain wrong. We do not need another indexer, Bribe you know it. UnitDex has been accepted as a simple indexer resource anyone would need. It contains GetUnitId and unit index/deindex events - no less, no more. Same as Wurst UnitIndexer.
Abyss stands between vJass (especially when using VSCode), Wurst and GUI.
Don't mix this together, don't use GUI logic for vJass code.

1 library - 1 functionality.

@Spellbound you need to include credits for all of the original authors. Not just Bribe, but also grim001, Jesus4Lyf and Nestrarus. This is because you are not creating a new resource, instead you provide a bridge to functionality that has been removed from hive by mistakes of the past. Example: Bitwise, StackNSplit.

Rewrite your comments. Guesses, unfinished sentences and such are of no importance to us. As long as you provide readable var and func names, most of your code should be informative without any additions. Cluttering it with newline/ comment spam gives the exact opposite effect.
 
I'll add the credits, but what did grim001 do?

I'm not sure what's wrong with my comments (also guesses? unfinished sentences?) but I have them there so I can read my own code if I get back to it in the future. Imo it also facilitates reading for people who are learning because there are some connections between the various methods that you wouldn't know unless told. Like, onIndex, there's this comment:
[kass]
// This is called here so as to set the variable IsNew[] to false after 0. seconds.
call thistype.timerCheck(u)[/code]
Sure, you find out after looking at timerCheck, but isn't it nice (especially for someone who's not great at reading code like I am) to be told directly?

Anyway, here's the updated version below. I've removed a few lines of comments but I feel that the ones that exist are quite helpful. At least, they would have been for me when trying to understand GUI Unit Event. ;)

This one does not contain an indexer.

JASS:
/*
    ExtraEvents v1.02
    by Spellbound
 
    Credits to Bribe for the excellent GUI Unit Event, of which this library is the vJASS
    version of. He was also helpful in understanding how his system works so that I may code this.
  
    Additional credits to Jesus4Lyf, Nestharus and grim001
*/
/*
    _________________________________________________________________________
    DESCRIPTION
    ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
    ExtraEvents provides you with extra events not available in the vanilla game. Specifically, you
    will be able to detect when a unit transforms, loads and unloads from a transport unit, is
    reanimated by Reanimate Dead, reincarnates or is brought back to life.
 
    Additionally, each of those events can be toggled on or off depending on whether you need them.
    See // CONFIGURATION under the globals block below.
*/
/*
    _________________________________________________________________________
    INSTALLATION
    ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
    ExtraEvents requires:
 
    UnitDex by TriggerHappy (Make sure you have the Detect Leave (UnitDex) ability in your map)
    https://www.hiveworkshop.com/threads/system-unitdex-unit-indexer.248209/
 
    RegisterEvent Pack by Bannar (both RegisterNativeEvent and RegisterPlayerUnitEvent)
    https://www.hiveworkshop.com/threads/snippet-registerevent-pack.250266/
 
    ListT by Bannar
    https://www.hiveworkshop.com/threads/containers-list-t.249011/
  
    PS: Add the following textmacro to ListT if it wasn't taken from the ExtraEvents demo map:
    //! runtextmacro DEFINE_LIST("", "UnitList", "unit")
  
    If you are unsure where to add that line, search for this comment in ListT and paste the
    textmacro above it:
    "// Run here any global list types you want to be defined."
 
    Once you have the required libraries in place, either copy the Detect Transform (ExtraEvents)
    ability to your map and set DETECT_TRANSFORM_ABILITY in the globals block below to the id of
    the spell.
 
    Alternatively, you can use the textmacro below. Import ExtraEvents, uncomment it, save your map,
    close it, reload and comment it out again. The ability will have been created in your Object Editor.
*/
    // //! external ObjectMerger w3a Adef EETD anam "Detect Transform" ansf "(ExtraEvents)" aart "" acat "" arac 0
/*
    _________________________________________________________________________
    CONFUGRATION
    ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
    ExtraEvents can be configured to exclude those events you do not wish to use. Define which
    events will be needed in the CONFIG block below.
*/
/*
    _________________________________________________________________________
    API
    ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
  
    Event Registration
    ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
    call RegisterNativeEvent(yourEvent, function yourFunction )
        This registers the event for all players.
  
    call RegisterIndexNativeEvent(playerIndex, yourEvent, function yourFunction )
        This registers the event for a specific player.
        NB: Player 1 starts on zero.
  
    call RegisterIndexNativeEvent(GetHandleId(event unit), yourEvent, function yourFunction )
        This registers the event for a specific unit.
      
    call GetIndexNativeEventTrigger(GetHandleId(event unit), yourEvent)
        This returns the trigger associated with that unit id and event id. Destroy that trigger
        to remove it. Useful for temporary events.
      
        Also works with playerIndex.
      
    The Events you can call are:
 
    EVENT_ON_TRANSFORM
    EVENT_ON_CARGO_LOAD
    EVENT_ON_CARGO_UNLOAD
    EVENT_ON_RESURRECTION
    EVENT_ON_ANIMATE_DEAD
    EVENT_ON_REINCARNATION_START
    EVENT_ON_REINCARNATION_FINISH
  
    Event Getters
    ¯¯¯¯¯¯¯¯¯¯¯¯¯
    call GetEventUnit()
        returns the event unit, similar to GetTriggerUnit().
        MB: GetTriggerUnit() will NOT work with ExtraEvents.
    
    call GetCargoUnit()
        returns the transport unit.
        NB: Only works on EVENT_ON_CARGO_LOAD and EVENT_ON_CARGO_UNLOAD
    
    call GetCargoSize()
        returns the number of units loaded in a transport.
        NB: Only works on EVENT_ON_CARGO_LOAD and EVENT_ON_CARGO_UNLOAD
 
*/

library ExtraEvents requires UnitDex, RegisterPlayerUnitEvent, optional ListT, WorldBounds

globals
 
    // CONFIGURATION
 
    // the transform detection ability. If DETECT_TRANSFORM is false, this is not needed.
    private constant integer    DETECT_TRANSFORM_ABILITY = 'EEdt'
 
    // toggles the detection of transform events.
    private constant boolean    DETECT_TRANSFORM        = true
 
    // toggles the detection of load/unload of cargo units, except for dead units (eg Meat Wagon's Exhume Corpse)
    private constant boolean    DETECT_CARGO            = true
 
    // toggles the detection of unloading dead units in cargo (Exhume Corpse). Does nothing if DETECT_CARGO is false.
    private constant boolean    DETECT_CARGO_DEAD       = true
 
    // toggles the detection of when a unit begins and finishes reincarnating.
    private constant boolean    DETECT_REINCARNATION    = true
 
    // toggles the detection of when a unit is animated via animate dead.
    private constant boolean    DETECT_ANIMATE_DEAD     = true
 
    // toggles the detection of units that are brought back to life via resurrection.
    private constant boolean    DETECT_RESURRECTION     = true
 
    // this overrides reincarnation, animate dead and resurrection. Set to true if you want any of these events to work.
    // for some reason setting DETECT_REVIVES = (DETECT_REINCARNATION or DETECT_ANIMATE_DEAD or DETECT_RESURRECTION) does not work.
    private constant boolean    DETECT_REVIVES          = true
 
    // END CONFIGURATION
 
    private unit eventUnit = null
    private unit eventOther = null
    private integer eventPreType = 0
 
    integer EVENT_ON_TRANSFORM
    integer EVENT_ON_CARGO_LOAD
    integer EVENT_ON_CARGO_UNLOAD
    integer EVENT_ON_RESURRECTION
    integer EVENT_ON_ANIMATE_DEAD
    integer EVENT_ON_REINCARNATION_START
    integer EVENT_ON_REINCARNATION_FINISH
 
    private integer Stack = -1
    private unit array IndexedUnit
    private unit array CargoUnit
    private group array CargoGroup
    private integer array PreTransformType
 
    private real MaxX
    private real MaxY
 
    private boolean array IsNew
    private boolean array IsAlive
    private boolean array IsReincarnating
    private boolean array IsTransforming
 
    private timer AfterIndexTimer = CreateTimer()
    private boolean rezCheck = true
  
endglobals

native UnitAlive takes unit u returns boolean

private keyword Cargo

function GetEventUnit takes nothing returns unit
    return eventUnit
endfunction

function GetCargoUnit takes nothing returns unit
    return eventOther
endfunction

function GetCargoSize takes unit transport returns integer
    return Cargo.size(GetUnitId(transport))
endfunction

private function FireEvent takes integer ev, unit u, unit other returns nothing
    local integer playerId = GetPlayerId(GetOwningPlayer(u))
    local integer handleId = GetHandleId(u)
    local integer id = GetUnitId(u)
    local unit prevUnit = eventUnit
    local unit prevOther = eventOther
    local integer prevType = eventPreType
  
    set eventUnit = u
    set eventOther = other
    set eventPreType = PreTransformType[id]
  
    call TriggerEvaluate(GetNativeEventTrigger(ev))
    if IsNativeEventRegistered(playerId, ev) then
        call TriggerEvaluate(GetIndexNativeEventTrigger(playerId, ev))
    elseif IsNativeEventRegistered(handleId, ev) then
        call TriggerEvaluate(GetIndexNativeEventTrigger(handleId, ev))
    endif
  
    set eventUnit = prevUnit
    set eventOther = prevOther
    set eventPreType = prevType
 
    set prevUnit = null
    set prevOther = null
endfunction

private struct Cargo
  
    static if LIBRARY_ListT then
        private static UnitList array CargoList
    endif
  
    static method delete takes integer transport_id returns nothing
        static if LIBRARY_ListT then
            call CargoList[transport_id].destroy()
        else
            call DestroyGroup(CargoGroup[transport_id])
        endif
    endmethod
  
    static method remove takes unit u, unit transport returns nothing
        local integer transport_id = GetUnitId(transport)
        static if LIBRARY_ListT then
            call CargoList[transport_id].removeElem(u)
        else
            call GroupRemoveUnit(CargoGroup[transport_id], u)
        endif
        call FireEvent(EVENT_ON_CARGO_UNLOAD, u, transport)
    endmethod
  
    static method add takes unit u, unit transport returns nothing
        local integer transport_id = GetUnitId(transport)
        static if LIBRARY_ListT then
            call CargoList[transport_id].push(u)
        else
            call GroupAddUnit(CargoGroup[transport_id], u)
        endif
        call FireEvent(EVENT_ON_CARGO_LOAD, u, transport)
    endmethod
  
    static method size takes integer transport_id returns integer
        static if LIBRARY_ListT then
            return CargoList[transport_id].size()
        else
            return CountUnitsInGroup(CargoGroup[transport_id])
        endif
    endmethod
  
    static method exists takes integer transport_id returns boolean
        static if LIBRARY_ListT then
            return (CargoList[transport_id] != 0)
        else
            return (CargoGroup[transport_id] != null)
        endif
    endmethod
  
    static method create takes integer transport_id returns thistype
        static if LIBRARY_ListT then
            set CargoList[transport_id] = UnitList.create()
        else
            set CargoGroup[transport_id] = CreateGroup()
        endif
        return 0
    endmethod
  
endstruct

private module ExtraEventsCore
 
    /*/* afterIndex */*/
    private static method afterIndex takes nothing returns nothing
        local integer i = Stack
        local integer id
        local unit u
    
        loop
            exitwhen i < 0
            set u = IndexedUnit[i]
            set id = GetUnitId(u)
        
            if IsNew[id] then
                set IsNew[id] = false
            
            elseif IsTransforming[id] then
            
                static if DETECT_TRANSFORM then
                    call FireEvent(EVENT_ON_TRANSFORM, u, null)
                    set IsTransforming[id] = false
                    // The unit has finished transforming. Store it's new type id.
                    set PreTransformType[id] = GetUnitTypeId(u)
                    call UnitAddAbility(u, DETECT_TRANSFORM_ABILITY)
                endif
            
            elseif IsAlive[id] then
        
                static if DETECT_REINCARNATION then
                    set IsReincarnating[id] = true
                    set IsAlive[id] = false
                    call FireEvent(EVENT_ON_REINCARNATION_START, u, null)
                endif
            
            endif
        
            set IndexedUnit[i] = null
            set i = i - 1
        endloop
        set Stack = -1
        
        set u = null
    endmethod
 
 
    /*/* timerCheck */*/
    private static method timerCheck takes unit u returns nothing
        set Stack = Stack + 1
        set IndexedUnit[Stack] = u
        call TimerStart(AfterIndexTimer, 0., false, function thistype.afterIndex)
    endmethod
 
 
    /*/* unload */*/
    private static method unload takes unit u returns nothing
        local integer id = GetUnitId(u)
        local integer cargo_id = GetUnitId(CargoUnit[id])
      
        call Cargo.remove(u, CargoUnit[id])
      
        if not IsUnitLoaded(u) or not UnitAlive(CargoUnit[id]) then
            set CargoUnit[id] = null
        endif
    endmethod
 
 
    /*/* onOrder */*/
    private static method onOrder takes nothing returns nothing
        local unit u = GetTriggerUnit()
        local integer id = GetUnitId(u)
    
            // onOrder occurs after onEnter
        
        /*
            NB: when units are unloaded from a transport, they fire a stop order (see below)
            When units are removed from the game or die, they fire an undefend order.
        */
    
        // If id is not zero then the unit has been indexed.
        if id > 0 then
        
            // Detect Cargo
            static if DETECT_CARGO then
            
                if GetIssuedOrderId() == 851972 then // order stop
                
                    // This does not detect unloaded corpses.
                    if CargoUnit[id] != null and not IsUnitLoaded(u) or UnitAlive(u) then
                        call thistype.unload(u)
                    endif
                
                    set u = null
                    return
                endif
            
            endif
        
            // Detect Morph
            static if DETECT_TRANSFORM then
            
                if GetUnitAbilityLevel(u, DETECT_TRANSFORM_ABILITY) == 0 and not IsTransforming[id] then
                    // re-adding DETECT_TRANSFORM_ABILITY immediately doesn't work, so it has to be
                    // done after a zero-second timer.
                    set IsTransforming[id] = true
                    call thistype.timerCheck(u)
                
                    set u = null
                    return
                endif
            
            endif
        
            // Detect Revives
            static if DETECT_REVIVES then
        
                // If unit was not previously alive and ...
                if not IsAlive[id] and UnitAlive(u) then
                
                    set IsAlive[id] = true
                    
                    // ... it is also a summoned unit then it has been Reanimated.
                    if IsUnitType(u, UNIT_TYPE_SUMMONED) then
                
                        static if DETECT_ANIMATE_DEAD then
                            call FireEvent(EVENT_ON_ANIMATE_DEAD, u, null)
                        endif
                    
                    // ... IsReincarnating[id] is true then it is done reincarnating.
                    elseif IsReincarnating[id] then
                
                        static if DETECT_REINCARNATION then
                            call FireEvent(EVENT_ON_REINCARNATION_FINISH, u, null)
                        endif
                    
                    // ... neither of the above two conditions have been met, it has been resurrected.
                    elseif not IsNew[id] and not rezCheck then
                    
                        static if DETECT_RESURRECTION then
                            call FireEvent(EVENT_ON_RESURRECTION, u, null)
                        endif
                
                    endif
            
                // Else if the unit is dead then ...
                elseif not UnitAlive(u) then
                
                    // if unit was just indexed then it was created as a corpse.
                    if IsNew[id] then
                        set IsAlive[id] = false
                    
                    // else if either of these parameters are true then ...
                    elseif CargoUnit[id] == null or not IsUnitType(u, UNIT_TYPE_HERO) then
                
                        /* order undefend fires before a UNIT_DEATH event, so if the unit is not
                        alive, yet IsAlive[id] is true, that means the unit is potentially reincar-
                        nating. Fire a zero-second timer to give time for the UNIT_DEATH event to
                        trigger. If IsAlive[id] is still true, then the unit is reincarnating. */
                        if IsAlive[id] then
                            call thistype.timerCheck(u)
                        endif
                    
                    endif
            
                endif
            
            endif // static if DETECT_REVIVES then
        
        endif
    
        set u = null
    endmethod
 
 
    /*/* onDeath */*/
    private static method onDeath takes nothing returns nothing
        local unit u = GetTriggerUnit()
        local integer id = GetUnitId(u)
    
        // This checks if the unit has been indexed.
        if id > 0 then
            set IsAlive[id] = false
        endif
    
        static if DETECT_CARGO then
            if CargoUnit[id] != null then
                call FireEvent(EVENT_ON_CARGO_UNLOAD, u, CargoUnit[id])
                set CargoUnit[id] = null
            endif
        endif
    
        set u = null
    endmethod
 
 
    /*/* onLoad */*/
    private static method onLoad takes nothing returns nothing
        local unit u = GetTriggerUnit()
        local integer id = GetUnitId(u)
        local integer cargo_id
    
        // if unit somehow loaded into a transport while being inside another, unload it
        if CargoUnit[id] != null then
            call thistype.unload(u)
        endif
    
        static if DETECT_CARGO_DEAD then
            // if a Meat Wagon loads up a corpse either by grabbing one on the ground of via Exhume
            // Corpse, that corpse is sent to the edge of the map by this system. This way, when it
            // is unloaded and placed back at the Meat Wagon's location, it fires an onEnter event,
            // which allows the system to detect when a corpse was unloaded from a transport.
            if not UnitAlive(u) then
                if LIBRARY_WorldBounds then
                    call SetUnitX(u, WorldBounds.maxX)
                    call SetUnitY(u, WorldBounds.maxY)
                else
                    call SetUnitX(u, MaxX)
                    call SetUnitY(u, MaxY)
                endif
            endif
        endif
    
        set CargoUnit[id] = GetTransportUnit()
        set cargo_id = GetUnitId(CargoUnit[id])
      
        if not Cargo.exists(cargo_id) then
            call Cargo.create(cargo_id)
        endif
        call Cargo.add(u, CargoUnit[id])
      
        set u = null
    endmethod
 
 
    /*/* onEnter */*/
    private static method onEnter takes nothing returns nothing
        local unit u = GetFilterUnit()
        local integer id = GetUnitId(u)
        local integer cargo_id = GetUnitId(CargoUnit[id])
    
            // onEnter occurs AFTER onIndex
        
        // The unit was dead, but has re-entered the map. Used to detect when a Meat Wagon unloads a corpse.
        if id > 0 then
            if not IsUnitLoaded(u) and CargoUnit[id] != null then
                call thistype.unload(u)
            endif
        endif
    
        set u = null
    endmethod
 
 
    /*/* onIndex */*/
    private static method onIndex takes nothing returns nothing
        local unit u = GetIndexedUnit()
        local integer id = GetUnitId(u)
    
            // onIndex occurs BEFORE onEnter
    
        set IsNew[id] = true
    
        static if DETECT_TRANSFORM then
            set IsTransforming[id] = false
            set PreTransformType[id] = GetUnitTypeId(u)
            call UnitAddAbility(u, DETECT_TRANSFORM_ABILITY)
        endif
    
        static if DETECT_REINCARNATION and DETECT_REVIVES then
            set IsReincarnating[id] = false
        endif
    
        if UnitAlive(u) then
            set IsAlive[id] = true
        else
            set IsAlive[id] = false
        endif
    
        // This is called here so as to set the variable IsNew[] to false after 0. seconds.
        call thistype.timerCheck(u)
    
        set u = null
    endmethod
 
 
    /*/* onDeindex */*/
    private static method onDeindex takes nothing returns nothing
        local unit u = GetIndexedUnit()
        local integer id = GetUnitId(u)
      
        if Cargo.exists(id) then
            call Cargo.delete(id)
        endif
    
        set u = null
    endmethod
 
 
    static method onInit takes nothing returns nothing
        local integer i
    
        static if DETECT_CARGO and DETECT_CARGO_DEAD then
            static if (not LIBRARY_WorldBounds) then
                local rect world = GetWorldBounds()
                local region reg = CreateRegion()
                set MaxX = GetRectMaxX(world)
                set MaxY = GetRectMaxY(world)
            endif
        endif
    
        set EVENT_ON_CARGO_LOAD             = CreateNativeEvent()
        set EVENT_ON_CARGO_UNLOAD           = CreateNativeEvent()
        set EVENT_ON_TRANSFORM              = CreateNativeEvent()
        set EVENT_ON_ANIMATE_DEAD           = CreateNativeEvent()
        set EVENT_ON_RESURRECTION           = CreateNativeEvent()
        set EVENT_ON_REINCARNATION_START    = CreateNativeEvent()
        set EVENT_ON_REINCARNATION_FINISH   = CreateNativeEvent()
    
        call RegisterAnyPlayerUnitEvent(EVENT_PLAYER_UNIT_ISSUED_ORDER, function thistype.onOrder)
        call RegisterAnyPlayerUnitEvent(EVENT_PLAYER_UNIT_DEATH, function thistype.onDeath)
    
        static if DETECT_CARGO then
          
            static if DETECT_CARGO_DEAD then
        
                static if (not LIBRARY_WorldBounds) then
                    call RegionAddRect(reg, world)
                    call TriggerRegisterEnterRegion(CreateTrigger(), reg, function thistype.onEnter)
                    call RemoveRect(world)
                    set world = null
                    set reg = null
                else
                    call TriggerRegisterEnterRegion(CreateTrigger(), WorldBounds.worldRegion, function thistype.onEnter)
                endif
                call RegisterAnyPlayerUnitEvent(EVENT_PLAYER_UNIT_LOADED, function thistype.onLoad)
        
            endif
            call RegisterUnitIndexEvent(Condition(function thistype.onDeindex), EVENT_UNIT_DEINDEX)
        
        endif
    
        call RegisterUnitIndexEvent(Condition(function thistype.onIndex), EVENT_UNIT_INDEX)
    
        static if DETECT_TRANSFORM then
            set i = 0
            loop
                call SetPlayerAbilityAvailable(Player(i), DETECT_TRANSFORM_ABILITY, false)
                set i = i + 1
                exitwhen i == bj_MAX_PLAYER_SLOTS
            endloop
        endif
    
        // see resurrectionTimer below.
        call TimerStart(CreateTimer(), 0., false, function thistype.resurrectionTimer)
    
    endmethod
 
    // for some reason dummy recyclers creating dummies to store fires off a resurrection event, so
    // this boolean rezCheck is set to false after a 0. second timer to prevent this from happening.
    // rezCheck must be false for a resurrection event to happen.
    static method resurrectionTimer takes nothing returns nothing
        set rezCheck = false
        call DestroyTimer(GetExpiredTimer())
    endmethod
 
endmodule

struct ExtraEvents
    implement ExtraEventsCore
endstruct

endlibrary
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Ooooh, that was him. Right. I'll update the credits. I guess I'll go ahead an post this in the JASS section, though I am quite curious about indexing integration vs not integrating indexing, if anyone else has an opinion on the matter.
If you keep the API the same as an existing Indexer, then you just include in the install instructions to delete UnitDex as your system covers it.
 

Bannar

Code Reviewer
Level 26
Joined
Mar 19, 2008
Messages
3,140
I'd like @TriggerHappy opinion on this. @Jampion too, since he is an active jass moderator.
Bribe, you know that this is the wrong move. You mix GUI logic with vJass code. Why did you even create GUI Unit Indexer if GUI Unit Event replaces it?
You should even know the slang for such things in professional environment. Precisely, this is called code smell.

In 99% of cases that require UnitIndexer (most of the time we need nothing more than GetHandleId anyway), user does NOT need any other custom unit events. Anything that comes after this crucial feature is a unnecessary weight. It's not an eye candy spell system of some kind or mappers sugar like my recent item libraries. Indexer is close to core libraries domain.
A professional approach is to keep core packages modular. Unit index feature is no exception.

It's even easier to "install" (copy, instead of copy + delete) or clone (hey, VSCode + git works with vJass nicely too) when UnitIndexer and UnitEvent are separated. No instructions will help you with that.

By continuing this path, you make sure that:
a) it's less modular, thus harder to "install"
b) let's cut the crap.. none will use it
 
There were even ideas that it was good in past if unit index events and indexing per se are seperated modules/libs

When looking at the dependencies in Wurst, there was an onEnter package that only handles detection of unit creation and removal. The Unit Index package was separate, since all it had to do was index or deindex.

Back to the main topic, Unit Event was created in GUI, as the evolution of Unit Indexer. Since it catered to the majority of those who use GUI, some of the standards in coding would not be applicable to it, one of which is modularity.

Now, in recreating that resource in vJASS, it would be better to observe modularity, since those standards, which are not applicable to GUI, apply here.

With that said, a conflict of two perspectives comes into play, with one envisioning a solid rewrite of a pertinent resource and another envisioning it as an entity separate from its' base (resource).
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
True true, best coding practice and all would require the two to be separate and interchangable resources.

However: SpellBound's resource has the problem of not being written by the same person who designed the indexer. It would be easier to integrate the two via optional textmacro, but that would require TriggerHappy to get on board and for everyone using UnitDex to update their version for it to work.

On one hand you have a one-resource solution, on the other you have "if this had been planned out in advance, here's how pretty we could make it". This is why grim's Auto Events and Nestharus' Unit Event worked so well.
 
Just some additional heads-up:

JASS:
    static method onInit takes nothing returns nothing
        ...
    endmethod

That onInit method could be made private to make it behave truly as a module initializer.
The reason is that onInits in modules that are public are as good as struct initializers, which could break public resources.

I believe Bannar already explained this kind of behavior.

EDIT:

JASS:
//! runtextmacro DEFINE_LIST("", "UnitList", "unit")

I think that should be provided for by default. No need for the user to test the script out and encountering the lack of a certain function exception.
 
Last edited:
Ah, right, I must have missed this. Updated (kept it v1.00).

EDIT: Updated to v1.01

Added the following to UnitEventsEx to simplify installation when using ListT:
JASS:
static if LIBRARY_ListT then
    //! runtextmacro DEFINE_LIST("", "UEExList", "unit")
endif
 
Last edited:
Hey Spellbound, i would love to use this library(specifically the Detect Transforming event), but I cannot use UnitDex since I already have a unit indexer (AIDS) nor the RegisterEvent Pack by Bannar since i already use the RegisterPlayerUnitEvent by Magtheridon, which Bannar refuses to make it compatible with (by using different API). Is it possible to make it work with any unit indexer and with Magtheridon's RegisterPlayerUnitEvent?

Or could you teach me to be able to detect transformation? Since that's all I really need your library for. (Can't use Bribe's GUI version since my map doesn't support GUI).

Much appreciated!
 
I am unfamiliar with AIDS or Magtheridon's RegisterPlayerUnitEvent, but essentially, you'll want to change the indexing/deindexing functions to use AIDS' API, and the custom events to use RegisterPlayerUnitEvent's API. There are a few functions inside the onIndex, onDeindex and onInit methods that should be switched out.

If you want to know how the transformation detection is achieved, have a read through the library. The steps are documented. Look at the static ifs with regards to DETECT_TRANSFORM and trace back the function calls and variabled. As an overview, the detection works by checking if a unit has an ability after an order event (iirc a stop order is given when a transformation ends). If the ability is absent, that means it was removed after a transformation, by which it's re-added so that a second transformation can detect it.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Approved as a strictly-vJass (therefore, with less dependency on the GUI interface if it is not desired) version of GUI Unit Event that compliments UnitDex. However, @Spellbound, if you wish to continue development of this, I would recommend adding that "OnCreate" function that I included in the recent UnitEvent update (runs during that 0.00 second post-timer thread).

Lua UnitEvent, once formally published (right now, only a beta) still focuses mainly on GUI. However, it is nevertheless able to run without any GUI variables, so I would say it is an overall superior system than either of the vJass alternatives.
 
Level 25
Joined
Feb 2, 2006
Messages
1,686
This system is really useful. Could you add stun and ensnare events? Is it even possible? A simple ON_BUFF event would be great, not sure if it already exists?

edit:
If you already keep the cargo information stored in your code (not sure), you could prove a simple cargo API:

JASS:
function IsUnitInTransporter takes unit whichUnit returns boolean
function GetUnitTransporter takes unit whichUnit returns unit
function GetCargoTransportedUnits takes unit transporter returns group

Otherwise, a small snippet for such a system would be useful since most people need would probably need those functions.
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
This system is really useful. Could you add stun and ensnare events? Is it even possible? A simple ON_BUFF event would be great, not sure if it already exists?

edit:
If you already keep the cargo information stored in your code (not sure), you could prove a simple cargo API:

JASS:
function IsUnitInTransporter takes unit whichUnit returns boolean
function GetUnitTransporter takes unit whichUnit returns unit
function GetCargoTransportedUnits takes unit transporter returns group

Otherwise, a small snippet for such a system would be useful since most people need would probably need those functions.
Buff events are hit-and-miss. @Tasyen made a Spell Steal Event, and catching a debuff would be possible immediately on a zero damage event, but positive buffs would require a timer (which would have to do a manual scan, similar to what I did in Heal Event).

That doesn't belong in this system. Such things need a separate system.
 
This system is really useful. Could you add stun and ensnare events? Is it even possible? A simple ON_BUFF event would be great, not sure if it already exists?

edit:
If you already keep the cargo information stored in your code (not sure), you could prove a simple cargo API:

JASS:
function IsUnitInTransporter takes unit whichUnit returns boolean
function GetUnitTransporter takes unit whichUnit returns unit
function GetCargoTransportedUnits takes unit transporter returns group

Otherwise, a small snippet for such a system would be useful since most people need would probably need those functions.
I've updated the library with your suggestions. Cheers!
 
Level 25
Joined
Feb 2, 2006
Messages
1,686
Thx. Now I can remove my own code to store it which was based on this system and a hashtable. There is some copy&paste error?

JASS:
native BlzGroupAddGroupFast takes group whichGroup, group addGroup returns integer

returns an integer not a group and I think the paramaters should be switched?

syntax.jpg
 
Last edited:
Level 25
Joined
Feb 2, 2006
Messages
1,686
GetCargoTransportedUnitGroup returns an empty group although GetCargoSize returns > 0. I think
JASS:
BlzGroupAddGroupFast
is broken. It works when I replace it with
JASS:
GroupAddGroup
! I have created a thread for it: [JASS] - BlzGroupAddGroupFast seems broken

About the RegisterNativeEvent: I guess I meant after the requires keyword in vJass.
 
Last edited:
Oh, did I get the parameters backwards? I always make that mistake with this function 🤦‍♂️

EDIT: Actually, you might be on to something. In my notes for a previous usage of that function I wrote that the second parameter is the group that things get added to, which is what I'm doing at the moment. Could an update have broken this function?
 
Top