• 🏆 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] Generic Unit Event

A simple unit event generalizing library that uses UnitDex (by TriggerHappy) for leak clean-up. It can be used for creating generic versions of unit events which do not have a corresponding player unit event and either perfecting the behavior of the event to be detectable or be used for further insight on certain unit events.

This utilizes a naive bucket method, wherein the distribution of units to their own buckets is done procedurally, analogous to filling up 10 buckets with water, one bucket at a time.

JASS:
library GenericUnitEvent /*
    --------------
    */ requires /*
    --------------
   
        --------------
        */ UnitDex  /*
        --------------
            #? GroupUtils
            #? WorldBounds
       
            By TriggerHappy:
            link: https://www.hiveworkshop.com/threads/system-unitdex-unit-indexer.248209/
           
        --------------
        */ Table    /*
        --------------
            By Bribe:
            link: https://www.hiveworkshop.com/threads/snippet-new-table.188084/
           
        --------------
        */ ListT    /*
        --------------
            ## Table
            #  Alloc
           
            By Bannar:
            link: https://www.hiveworkshop.com/threads/containers-list-t.249011/page-2#post-3271599
           
         ----------------------------------------------------
        |                                                    |
        |       GenericUnitEvent                             |
        |           - MyPad                                  |
        |                                                    |
         ----------------------------------------------------
        
         -------------------------------------------------------------------------------
        |
        |   Restrictions:
        |
        |       - Units cannot be manually deindexed, as the system treats such an event
        |       as a unit removal event.
        |
        |-------------------------------------------------------------------------------
        |
        |   Description:
        |  
        |       - A library which makes unitevent exclusive events easy to register and
        |       listen to. Essentially extends unitevent to pseudo-playerunitevent.
        |
        |       - Makes damage detection a breeze, as well as target acquisition
        |       and target in range events.
        |
        |
        |--------------------------------------------------------------------------------
        |
        |   API:
        |
        |   struct GenericUnitEvent:
        |       static method listen(unitevent whichEvent) -> StubUnitEvent
        |           - Enables the system to listen to that specified unitevent.
        |
        |       static method registerUnit(unit whichUnit) -> boolean
        |           - Registers a unit to the list of listened unitevents.
        |             This is automatically called on unit creation
        |             if REGISTER_ON_STARTUP is set to true. (Default)
        |
        |   struct StubUnitEvent:
        |       method addHandler(code callback) -> triggercondition
        |           - Adds a callback function to be invoked upon the execution
        |             of a certain event.
        |
        |       method removeHandler(triggercondition cond)
        |           - Removes a callback function from a certain event.
        |
        |   Functions:
        |       function RegisterUnitEvent(unitevent whichEvent, code callback) -> triggercondition
        |           - Executes a condensed version of the code, without having to deal too much
        |             with structs.
        |
        |       function RegisterAnyUnitEvent(unitevent uv, code c) -> triggercondition
        |           - A deprecated function that invokes RegisterUnitEvent.
        |
        |       function RegisterUnitEventById(integer id, code callback) -> trggercondition
        |           - Internally calls RegisterUnitEvent
        |
        |       function GetUnitEventId() -> unitevent
        |           - Returns the triggering event in parallel with GetTriggerEventId()
        |             Can also persist where GetTriggerEventId might fail.
        |
        |--------------------------------------------------------------------------------
        |
        |   Changelogs:
        |
        |       v.1.1 - Rewritten the entire library.
        |             - Removed SetUnitBucketSize.
        |
        |       v.1.0 - Release
        |
        |---------------------------------------------------------------------------------
        |
        |   Programmer notes:
        |
        |       In the previous version, the following would compile:
        |           local integer i
        |           call GenericUnitEvent(i).addHandler(code callback)
        |
        |       Now, the lines above would no longer compile. Instead, try this:
        |
        |           call GenericUnitEvent.listen(yourUnitEvent).addHandler(code callback)
        |
         -----------------------------------------------------------------------------------
        
         -----------------------
        |
        |   Credits:
        |
        |       - AGD for the inputs on this system.
        |       - Nestharus for the ideal management
        |           of the linked list structure
        |           (Which the rewrite was guided on)
        |       - Bribe for Table (The HIVE one)
        |       - Bannar for ListT
        |
         -----------------------
    */

native UnitAlive takes unit id returns Boolean

globals
    private constant boolean REGISTER_ON_STARTUP    = true
    private constant boolean ADVANCED_DEBUG         = false
    private constant integer MAX_BUCKET_SIZE        = 5
    private constant integer REFRESH_AMOUNT         = 3
endglobals

private function DuplicateList takes IntegerList whichList returns IntegerList
    local IntegerList newList   = IntegerList.create()
    local IntegerListItem iter  = whichList.first
    loop
        exitwhen iter == 0
        call newList.push(iter.data)
        set iter = iter.next
    endloop
    return newList
endfunction

private module Initializer
    private static method onInit takes nothing returns nothing
        call thistype.init()
    endmethod
endmodule

private struct StubUnitEvent extends array
    implement Alloc
   
    private static Table eventMap                   = 0
   
    readonly unitevent event
    readonly trigger handlerTrig
   
    method removeHandler takes triggercondition cond returns nothing
        call TriggerRemoveCondition(this.handlerTrig, cond)
    endmethod
   
    method addHandler takes code callback returns triggercondition
        return TriggerAddCondition(this.handlerTrig, Condition(callback))
    endmethod
   
    static method peek takes eventid whichId returns thistype
        return eventMap[GetHandleId(whichId)]
    endmethod
   
    static method request takes unitevent whichEvent returns thistype
        local thistype this         = eventMap[GetHandleId(whichEvent)]
        if this == 0 then
            set this                = .allocate()
            set this.event          = whichEvent
            set this.handlerTrig    = CreateTrigger()
            set eventMap[GetHandleId(whichEvent)] = this
        endif
        return this
    endmethod
   
    private static method init takes nothing returns nothing
        set thistype.eventMap       = Table.create()
    endmethod
   
    implement Initializer
endstruct

struct GenericUnitEvent extends array
    implement Alloc
   
    private static code  registerPointer    = null
   
    private static group swap           = null
    private static group container      = null
    private static group tempRegContain = null
   
    private static Table stubMap        = 0
    private static IntegerList stubList = 0
   
    private static IntegerList array reqList
    private static integer removeCount  = 0
   
    //  For GenericUnitEvent members
    private trigger detector
    private group   groupHolder
    private integer unitsRegistered
    private integer unitsRemoved
   
    private IntegerList reqListPointer
    private IntegerListItem reqListItem
   
    //  For UnitDex members only
    private static boolean array existsForIndex
    private static thistype array keyGroup
   
    //  Iterate over global group
    private static StubUnitEvent tempStub   = 0
   
static if ADVANCED_DEBUG then
    debug private static boolean stackTrace     = false
    debug private static thistype stackObject   = 0
endif
    readonly static unitevent execUnitEvent     = null
    
    private static method onEventExecution takes nothing returns nothing
        local StubUnitEvent object  = StubUnitEvent.peek(GetTriggerEventId())
        local unitevent lastEv
        if object != 0 then
            set lastEv          = .execUnitEvent
            set .execUnitEvent  = object.event
            
            if IsTriggerEnabled(object.handlerTrig) then
                call TriggerEvaluate(object.handlerTrig)
            endif
            
            set .execUnitEvent  = lastEv
            set lastEv          = null
        endif
    endmethod
   
    private method destruct takes nothing returns nothing
        local unit u
        local integer id
       
        call DestroyTrigger(this.detector)
        loop
            set u   = FirstOfGroup(this.groupHolder)
            set id  = GetUnitId(u)
           
            exitwhen u == null
           
            set keyGroup[id]        = 0
            set existsForIndex[id]  = false
           
            call GroupRemoveUnit(.container, u)
            call GroupRemoveUnit(this.groupHolder, u)
            call GroupAddUnit(.tempRegContain, u)
        endloop
        call DestroyGroup(this.groupHolder)
        call this.reqListPointer.erase(this.reqListItem)
       
    static if ADVANCED_DEBUG then
        debug set .stackTrace   = true
        debug set .stackObject  = this
    endif
   
        call ForForce(bj_FORCE_PLAYER[0], .registerPointer)
   
    static if ADVANCED_DEBUG then
        debug set .stackTrace   = false
    endif
   
        set .removeCount         = .removeCount - this.unitsRemoved
               
        set this.unitsRegistered = 0
        set this.unitsRemoved    = 0
        set this.reqListPointer  = 0
        set this.reqListItem     = 0
        set this.groupHolder     = null
        set this.detector        = null
       
        call this.deallocate()
    endmethod
   
    private static method construct takes nothing returns thistype
        local thistype this     = .allocate()
        set this.detector       = CreateTrigger()
        set this.groupHolder    = CreateGroup()
        call TriggerAddCondition(this.detector, Condition(function thistype.onEventExecution))
        return this
    endmethod
   
    private static method onNewStubUnitEvent takes StubUnitEvent temp returns nothing
        local unit u            = FirstOfGroup(.container)
        local group tempSwap    = .swap
        local integer id        = GetUnitId(u)
        loop
            exitwhen u == null
           
            call GroupRemoveUnit(.container, u)
            call GroupAddUnit(.swap, u)
           
            call TriggerRegisterUnitEvent(keyGroup[id].detector, u, temp.event)
           
            set u   = FirstOfGroup(.container)
            set id  = GetUnitId(u)
        endloop
       
        set .swap           = .container
        set .container      = tempSwap
        set tempSwap        = null
        set u               = null
    endmethod
   
    private static method onListenCallback takes nothing returns nothing
        call thistype.onNewStubUnitEvent(.tempStub)
    endmethod
   
    static method registerUnit takes unit whichUnit returns boolean
        local integer id = GetUnitId(whichUnit)
        local thistype this
        local IntegerListItem iter
       
        if (id == 0) or existsForIndex[id] then
            return false
        endif
        // Find out if there are any instances that need to be filled up.
        if .reqList[1].size() != 0 then
            set this = .reqList[1].first.data
            static if ADVANCED_DEBUG then
                debug if .stackTrace then
                    debug call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 50000, "GenericUnitEvent::registerUnit -> Bucket retrieval type:: Node reference")
                debug endif
            endif
        else
            set this = thistype.construct()
            static if ADVANCED_DEBUG then
                debug if .stackTrace then
                    debug call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 50000, "GenericUnitEvent::registerUnit -> Bucket retrieval type:: Node creation")
                debug endif
            endif
           
            call .reqList[0].push(this)
           
            set this.reqListPointer = .reqList[1]
            set this.reqListItem    = .reqList[1].push(this).last
        endif
        static if ADVANCED_DEBUG then
            debug if .stackTrace then
                debug call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 50000, "GenericUnitEvent::registerUnit -> Bucket instance identified {" + I2S(this) + "}")
            debug endif
        endif
        call GroupAddUnit(.container, whichUnit)
       
        set this.unitsRegistered    = this.unitsRegistered + 1
        call GroupAddUnit(this.groupHolder, whichUnit)
        set existsForIndex[id]      = true
        set keyGroup[id]            = this
       
        //  Register all StubUnitEvents for the unit
        set iter = .stubList.first
        loop
            exitwhen iter == 0
            call TriggerRegisterUnitEvent(this.detector, whichUnit, StubUnitEvent(iter.data).event)
            set iter = iter.next
        endloop
        if this.unitsRegistered >= MAX_BUCKET_SIZE then
            call this.reqListPointer.erase(this.reqListItem)
           
            set this.reqListPointer = .reqList[2]
            set this.reqListItem    = .reqList[2].push(this).last
        endif
        return true
    endmethod
   
    private static method deregisterUnit takes unit whichUnit returns boolean
        local integer id = GetUnitId(whichUnit)
        local thistype this
       
        if (id == 0) or (not existsForIndex[id]) or UnitAlive(whichUnit) then
            return false
        endif
        
        set this = keyGroup[id]
        //  If previously belonging to a full list, move it to list that requests filling up
        if this.reqListPointer == .reqList[2] then
            call .reqList[2].erase(this.reqListItem)
           
            set this.reqListPointer = .reqList[1]
            set this.reqListItem    = .reqList[1].unshift(this).first
        endif
        call GroupRemoveUnit(.container, whichUnit)
       
        call GroupRemoveUnit(this.groupHolder, whichUnit)
        set keyGroup[id]        = 0
        set existsForIndex[id]  = false
       
        set this.unitsRegistered = this.unitsRegistered - 1
        set this.unitsRemoved    = this.unitsRemoved + 1
        set .removeCount         = .removeCount + 1
       
        if (this.unitsRemoved >= REFRESH_AMOUNT) then
            static if ADVANCED_DEBUG then
                debug if .stackTrace then
                    debug call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 50000, "GenericUnitEvent::deregisterUnit -> calling .destruct() {Attribute: " + I2S(this) + "}")
                debug endif
            endif
            call this.destruct()
            static if ADVANCED_DEBUG then
                debug if .stackTrace then
                    debug call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 50000, "GenericUnitEvent::deregisterUnit -> .destruct() procedure finished!")
                debug endif
            endif           
        elseif ((.reqList[0].size() > 2) and (.removeCount >= REFRESH_AMOUNT*.reqList[0].size()/2)) then
            set .reqList[3]     = DuplicateList(.reqList[1])
            loop
                exitwhen .reqList[3].first == 0
               
                call thistype(.reqList[3].first.data).destruct()
                call .reqList[3].shift()
            endloop
            call .reqList[3].destroy()
            set .reqList[3]     = 0
        endif
        return true
    endmethod
   
    static method listen takes unitevent whichEvent returns StubUnitEvent
        local StubUnitEvent object  = StubUnitEvent.request(whichEvent)
        if not .stubMap.has(object) then
            set .stubMap[object]     = stubList.push(object).last
            set .tempStub            = object
           
            call ForForce(bj_FORCE_PLAYER[0], function thistype.onListenCallback)
        endif
        return object
    endmethod
    private static method onReRegister takes nothing returns nothing
        local unit u
        local integer id
        loop
            set u   = FirstOfGroup(.tempRegContain)
            set id  = GetUnitId(u)
           
            exitwhen u == null
           
            call thistype.registerUnit(u)
            call GroupRemoveUnit(.tempRegContain, u)
        endloop
    endmethod
   
    private static method onUnitExit takes nothing returns nothing
        call thistype.deregisterUnit(GetIndexedUnit())
    endmethod
static if REGISTER_ON_STARTUP then
    private static method onUnitEnter takes nothing returns nothing
        call thistype.registerUnit(GetIndexedUnit())
    endmethod
endif
   
    private static method initListener takes nothing returns nothing
    static if REGISTER_ON_STARTUP then
        call OnUnitIndex(function thistype.onUnitEnter)
    endif
        call OnUnitDeindex(function thistype.onUnitExit)
    endmethod
    private static method init takes nothing returns nothing
        set thistype.swap               = CreateGroup()
        set thistype.container          = CreateGroup()
        set thistype.tempRegContain     = CreateGroup()
       
        set thistype.registerPointer    = function thistype.onReRegister
        set thistype.stubMap            = Table.create()
        set thistype.stubList           = IntegerList.create()
       
        //  Holds all instances
        set thistype.reqList[0]         = IntegerList.create()
       
        //  Holds instances that are in need of filling up
        set thistype.reqList[1]         = IntegerList.create()
        //  Holds instances that are already filled up.
        set thistype.reqList[2]         = IntegerList.create()
       
        call thistype.initListener()
    endmethod
   
    implement Initializer
endstruct

function RegisterUnitEvent takes unitevent whichEvent, code callback returns triggercondition
    return GenericUnitEvent.listen(whichEvent).addHandler(callback)
endfunction

function RegisterAnyUnitEvent takes unitevent uv, code c returns triggercondition
    debug call BJDebugMsg("GenericUnitEvent::RegisterAnyUnitEvent -> This has been deprecated, please use GenericUnitEvent::RegisterUnitEvent instead.")
    return RegisterUnitEvent(uv, c)
endfunction

function RegisterUnitEventById takes integer id, code callback returns triggercondition
    return RegisterUnitEvent(ConvertUnitEvent(id), callback)
endfunction

function GetUnitEventId takes nothing returns unitevent
    return GenericUnitEvent.execUnitEvent
endfunction

endlibrary

This could be used to unearth possible exploits which have not been seen before or add a twist to existing ones.
 
Last edited:

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
Nice Idea, a lib like RegisterPlayerUnitEvent but for specific instead of playerunit events =).

But there are important issues to be resolved and some of these issues are also present in your other submissions.

First, it is important to observe modularity when making a resource (This issue is also apparent in your AllocationAndLinks).
For instance, this resource is about registering events but you included a functionality of a unit indexer (Not only for system use as a backup, but you also made its functionalities public). I think it would be more preferable to make UnitDex a hardcoded requirement here.

Another thing is to please be consistent with your naming style. In some parts, you followed the JPAG but in other parts, you used C-like namings.

Maybe you could also support something like RegisterGenericUnitEvent(unitevent, code) and an equivalent for Deregister/Unregister because sometimes we need dynamic event handlers.

I have not looked into details yet, these are just those apparent from skimming.
 
Last edited:
Nice Idea, a lib like RegisterPlayerUnitEvent but for specific instead of playerunit events =).

I got the idea of generating specific unit events from @Spellbound who wanted to examine the behaviour of some natives.

First, it is important to observe modularity when making a resource (This issue is also apparent in your AllocationAndLinks).

You're right, I just included that because I don't want others to be burdened with getting the necessary libraries, but I'll change that in the future.
I actually added some more textmacros with the AllocationAndLinks library for easy recreation.

Another thing is to please be consistent with your naming style. In some parts, you followed the JPAG but in other parts, you should C-like namings.

Okay, I didn't notice that. Is it the part where I initialise the variable Num01?

Maybe you could also support something like RegisterGenericUnitEvent(unitevent, code) and an equivalent for Deregister/Unregister because sometimes we need dynamic event handlers.

I could support something like that, though that would require my Pseudo-Var library.

Overall, I think I should keep the modules as they are. , but I would need to generate two structs that would specifically resolve the Register and Deregister function (will add a library for that). It might take some time, but I'll see with what I could put up. :)
[/s]

EDIT:

New extension library made! Bumping!

EDIT (2):

Wurst version added!
 
Last edited:

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
Currently, the only downside I can see with the design is that it greatly increases the script size greatly with each module implementation. Otherwise, it looks good. This can easily be solved by putting your the system's core inside a struct, and then provide an api for event registration/deregistration. Something like for example GenericUnitEvent.registerHandler(unitevent whichEvent, code handler) and GenericUnitEvent.register(unit u). Then, what the module would only do is to automatically call these methods for every newly indexed unit who passes a filter and likewise unregister every deindexed unit. By doing this, you would also be able to cover the functionality of your UnitEventUtils in the same library.
 

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
Btw, by applying my proposed design, you can also greatly minimize the handle count by registering all the events into a single bucket (what I mean is that there's no need to restrict one event per bucket). Instead, every time a user registers an event, you add the event to all currently active buckets. Then as for calling the proper event handlers, you can do:
JASS:
private static method onEventFire ...
    //...
    call TriggerEvaluate(handlerTrigger[GetTriggerEventId()])
    //...
endmethod
 
That approach is basically what I did in the second library. Since I don't have a reliable internet connection, it might take me a while to update this.

However, I worry about natives not returning the correct values in the handler function due to how they are implemented to always check for the event. A forced trigger evaluation is my primary consideration on this case, since GetTriggerEventId returns the current event ID of the current trigger, at least to my knowledge.

To work around this, (if needed), I'll add the ability to assign global values to the system based on the natives themselves.
 

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
However, I worry about natives not returning the correct values in the handler function due to how they are implemented to always check for the event. A forced trigger evaluation is my primary consideration on this case, since GetTriggerEventId returns the current event ID of the current trigger, at least to my knowledge.
I'm not so sure on what you mean but if you mean something like this
JASS:
function OnEventFire()
    // The event that fired this function is a spell effect event
    //...
    TriggerEvaluate(table.trigger[GetSpellAbilityId()])
    // ^ Are you unsure if the event responses like GetSpellTargetUnit() will not return the proper value since the trigger was being forced evaluated?
In this case, those event responses inside the forced evaluated trigger would return the same value inside the function calling the TriggerEvaluate() so it's safe (problems would only arise during recursions coz some event responses aren't designed to be recursion safe, but then again, not doing that forced evaluate still face that same issue). Again, I'm not sure if this is what you mean so please tell me if not.
 
Updated to version 1.1 with severe consequences:


  • Destroyed backwards-compatibility. (This will be a lot more impactful towards those already using this)
  • Combined both Main library and Add-on library, as per @AGD 's suggestions.
  • Removed a lot of functions (Convenience)
  • Removed the module.


Now, writing up a damage handler would be too easy, now without a lot of the complexity of having to attach the units to buckets manually.
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Just an FYI - the .nodeMap references should probably be a sized or arithmetically-2D array. Why? Because if you know the size and indices of the Table before you even start, you're able to do this:

nodeMap[this*maxIndex + index]

It's much faster than using a hashtable.

Edit: I see - so you have a bunch of different types stored in there. Well then the performance trade-off isn't really substantial to merit creating a bunch of array variables.
 

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
JASS:
            set this.nodeMap               = Table.create()
            //  Next
            set this.nodeMap[1]            = Table.create()
            //  Unit's Pointer
            set this.nodeMap[0]            = Table.create()
            //  Previous
            set this.nodeMap[-1]           = Table.create()
            //  Internal instance's event count
            set this.nodeMap[-2]           = Table.create()
            //  Internal instance's unit count
            set this.nodeMap[-3]           = Table.create()
            //  Internal instance's group
            set this.nodeMap[-4]           = Table.create()
            //  Internal instance's trigger
            set this.nodeMap[-5]           = Table.create()

            //  Internal allocator of boolexprs
            set this.nodeMap[2]            = Table.create()
            //  Internal next of boolexprs
            set this.nodeMap[3]            = Table.create()
            //  Internal prev of boolexprs
            set this.nodeMap[4]            = Table.create()
            //  Internal boolexpr holder
            set this.nodeMap[5]            = Table.create()
            //  Internal boolexpr's pointer
            set this.nodeMap[6]            = Table.create()
Instead of manually allocating many tables, you can shorten and optimize it by using a TableArray
JASS:
private TableArray nodeMap
//...
static method create takes nothing returns thistype
    //...
    set this.nodeMap = TableArray[13] //TableArray allocation is O(1)
    //...
endmethod

You also don't need the variable 'this.size' since its value is constant, so you can directly use 'DEFAULT_MAX_SIZE'

It seemed to me that you are not using the bucket technique despite of saying so. Basically, what you did is have a single trigger per unitevent. Then all units are registered into each of those trigger. A bucket technique should divide the total number of units into groups. Each group has a certain size and is assigned its own trigger. What the technique does then achieve is that everytime the maximum event leaks is reached (for a certain group/trigger), the trigger rebuilding is not so expensive in performance since the trigger only contains a group and not all of the units.

But it is up to you if you will implement the said technique since it also has its cons. I just pointed out your apparent misconception of the bucket method.
JASS:
/*
    *       It utilizes the bucket method of storing events, so that not too many handles will
    *       be created.
*/
The contrary is true. Bucket method results in more trigger handles but it's purpose is to make the trigger refreshing not so heavy.

As for the downside of the bucket method that I mentioned, not only does it increase the number of triggers (which is negligible) but also the maximum allowable leaked events. If N is the max number of leaked events without the bucket method, then it will be N times the current number of buckets with the bucket method. Note that the potential number of buckets has no set limit if the bucket size is constant. So with an average of 500 units in a map at any given time and a bucket size of 50, the total number of buckets is 10 and the maximum potential event leaks will be 10*N.

And lastly, the way you currently handle leaked events is problematic:
JASS:
//[-3] -> registered unit count
//[-2] -> unitevents handles created
if (this.nodeMap[-3].integer[curIndex] <= 0) and (this.nodeMap[-2].integer[curIndex] >= this.maxSize) then
    call DestroyTrigger(this.getTrigger(curIndex))
    call DestroyGroup(this.getGroup(curIndex))
    //...
This will continue to leak so long as there is a single unit registered to the system. And it's possible that a map will never run out of a registered unit at any given time while there will constantly be unit registration and unregistration (which causes the leak) the happens along the line.
It should be
JASS:
if [number of unregistered units] >= [max allowable unitevent handles leaks];
    recreate trigger;
    add again all the currently registered units into the trigger;


The current API is good. You could also add equivalent for handler deregistration, but is not mandatory especially if it complicates the code.
 
JASS:
            set this.nodeMap              = Table.create()
            //  Next
            set this.nodeMap[1]           = Table.create()
            //  Unit's Pointer
            set this.nodeMap[0]           = Table.create()
            //  Previous
            set this.nodeMap[-1]          = Table.create()
            //  Internal instance's event count
            set this.nodeMap[-2]          = Table.create()
            //  Internal instance's unit count
            set this.nodeMap[-3]          = Table.create()
            //  Internal instance's group
            set this.nodeMap[-4]          = Table.create()
            //  Internal instance's trigger
            set this.nodeMap[-5]          = Table.create()

            //  Internal allocator of boolexprs
            set this.nodeMap[2]           = Table.create()
            //  Internal next of boolexprs
            set this.nodeMap[3]           = Table.create()
            //  Internal prev of boolexprs
            set this.nodeMap[4]           = Table.create()
            //  Internal boolexpr holder
            set this.nodeMap[5]           = Table.create()
            //  Internal boolexpr's pointer
            set this.nodeMap[6]           = Table.create()

Yeah, using TableArray would be more intuitive in this case. Given that this was actually a procedural approach, I first thought of making it work. Then, I would only optimize it later on.

You also don't need the variable 'this.size' since its value is constant, so you can directly use 'DEFAULT_MAX_SIZE'
Indeed, though there is a reason why it is named DEFAULT_MAX_SIZE instead of MAX_SIZE. (I forgot to include the member maxSize in the previous version, or did I?)

It seemed to me that you are not using the bucket technique despite of saying so. Basically, what you did is have a single trigger per unitevent. Then all units are registered into each of those trigger. A bucket technique should divide the total number of units into groups. Each group has a certain size and is assigned its own trigger. What the technique does then achieve is that everytime the maximum event leaks is reached (for a certain group/trigger), the trigger rebuilding is not so expensive in performance since the trigger only contains a group and not all of the units.

But it is up to you if you will implement the said technique since it also has its cons. I just pointed out your apparent misconception of the bucket method.

If that is the bucket method, then what I must be doing is a naive version of it. What I am sure of in my approach is that it does not divide the total number of units and register them onto certain buckets; rather, it fills up each bucket as registration goes. Then, we assume that one starts with a predefined amount of buckets of 10 on a typical bucket technique.

Case in point, we have those 500 units divided by 50 units per trigger. Rather than evenly dividing the 500 units into those ten triggers, the systems goes about it one by one, eventually filling up ten buckets. Now, if we compare it to the above method, the results would end up being similar to each other.

Now, we lower it from 500 to 300 units. We would end up with (300/50) 6 buckets filled up, compared to that bucket technique above, which would have 30 units each. (We have to evenly distribute the number of units here).

Now, suppose we lower it even further. That is where your point becomes clear; for a small number of units, using the bucket technique would prove to be better than my naive bucket technique. However, for a large number of units, the benefits of the original bucket technique greatly diminish to the point of irrelevance, since the number of registered units would reach some sort of equilibrium in either of these techniques.

Thus, I will keep the method above, but I will redefine bucket to a naive bucket, since for a large amount of units, it wouldn't matter anyway.

And lastly, the way you currently handle leaked events is problematic:
JASS:
//[-3] -> registered unit count
//[-2] -> unitevents handles created
if (this.nodeMap[-3].integer[curIndex] <= 0) and (this.nodeMap[-2].integer[curIndex] >= this.maxSize) then
    call DestroyTrigger(this.getTrigger(curIndex))
    call DestroyGroup(this.getGroup(curIndex))
    //...

I agree on that. The one above was designed in consideration for melee maps.
Resolved with the upcoming update on the script.

The current API is good. You could also add equivalent for handler deregistration, but is not mandatory especially if it complicates the code.

That could be arranged, since I've got the linked list setup going for the triggers. All I need to do now is, maybe declare another Table instance that will hold the TableArrays that are mapped to the handle ids of the equivalent boolean expressions.

EDIT:

Updated the system with a new function, SetUnitEventBucketSize. I forgot to incorporate it into the changelog beforehand.

Changelog:
  • Fixed addUnit such that removeUnit will work properly. (removeUnit was not working due to a missing line in addUnit that binds a unit to a certain bucket)
  • Changed the condition of the statement in removeUnit to allow bucket clearance. (removeUnit now clears the bucket if the bucket is half-empty, and if the number of instances have exceeded the maximum size of the bucket (since maxSize is alterable).
 
Last edited:
I have now rewritten the library completely from scratch:

Changelogs:

  • Now, it no longer uses the naïve bucket method. It will fill up the buckets until they reach the defined threshold.
    When a unit belonging to a full bucket is removed from the game, the bucket will obtain the highest priority of being filled up.

  • Refresh has been reworked. Now, the list refreshes locally after reaching another defined threshold (must be less than max), and globally if no refreshes have occurred prior to exceeding the following value: (Such a case is when most of the buckets to be filled have a certain amount that does not deviate a lot, or tend to be more evenly-distributed)
    listOfBuckets_size*threshold/2

  • Removed SetUnitBucketSize in favor for a global trigger group, with a list of events per unit, and a list of units per bucket.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Approved, but I would like to see more here. Mainly, this might benefit someone who wants to build a fresh damage engine with an old WarCraft 3 patch.

I'd like to see a separate Lua version of this resource which hooks TriggerRegisterAnyUnitEventBJ and adds the missing EVENT_PLAYER_UNIT_ globals, which would then deliver consistency.

All individual unit events could potentially extend off of one bucket of "playerunitevents", by simply running a callback if a function is registered for a specific unit.

Currently, we only have these outstanding:

JASS:
    constant unitevent EVENT_UNIT_STATE_LIMIT                           = ConvertUnitEvent(59)                                                                        
    constant unitevent EVENT_UNIT_ACQUIRED_TARGET                       = ConvertUnitEvent(60)
    constant unitevent EVENT_UNIT_TARGET_IN_RANGE                       = ConvertUnitEvent(61)

The bottom two are great for AI/combat detection systems and some other things. I am not sure what UNIT_STATE_LIMIT is; maybe it occurs when a unit has reached its max life/mana, or its max life/mana has changed? It's worth checking out.
 
Level 17
Joined
Jun 2, 2009
Messages
1,137
Uhm hello. I have a created AI system for my map and dear Uncle shared your system with me.
Here is my topic. But the problem is, i don't understand about Jass. How can i implement your system into mine?

 
Level 39
Joined
Feb 27, 2007
Messages
5,010
A problem with this resource for you, @JFAMAP: You use the "Custom Value" of units in some of your triggers (I saw it in the arrow trigger), and this system uses that Custom Value to keep track of its own data. This is the "unit indexer" referred to in the system. It will overwrite data your map uses, and your map will overwrite the indexer data which will be bad for the system.
 
Last edited:
Top