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

[Snippet] ConstructEvent

Bannar

Code Reviewer
Level 26
Joined
Mar 19, 2008
Messages
3,140
Provides functionality of generic CONSTRUCT events, replacing them in functionality. An addition event: INTERRUPTED is also defined.
Allows to retrieve unit which actually started construction of given structure.

Catches:
- orc/ne/special by checking workers position
- hum and ud by measuring worker's distance from started construction
- item-build orders are treated as ud-type constructions

Encouraging you guys to test it.
JASS:
/*****************************************************************************
*
*    ConstructEvent v2.2.0.1
*       by Bannar
*
*    Provides complete solution to construction events.
*    Allows to retrieve unit which actually started construction of given structure.
*
******************************************************************************
*
*    Requirements:
*
*       RegisterPlayerUnitEvent by Bannar
*          hiveworkshop.com/threads/snippet-registerevent-pack.250266/
*
*       ListT by Bannar
*          hiveworkshop.com/threads/containers-list-t.249011/
*
*       UnitDex by TriggerHappy
*          hiveworkshop.com/threads/system-unitdex-unit-indexer.248209/
*
******************************************************************************
*
*    Event API:
*
*       integer EVENT_UNIT_CONSTRUCTION_START
*
*       integer EVENT_UNIT_CONSTRUCTION_CANCEL
*          Intentional construction stop.
*
*       integer EVENT_UNIT_CONSTRUCTION_FINISH
*
*       integer EVENT_UNIT_CONSTRUCTION_INTERRUPT
*          Undesired construction stop e.g. unit death.
*
*       Use RegisterNativeEvent or RegisterIndexNativeEvent for event registration.
*       GetNativeEventTrigger and GetIndexNativeEventTrigger provide access to trigger handles.
*
*
*       function GetConstructingBuilder takes nothing returns unit
*          Retrieves event builder unit, valid only for START event.
*
*       function GetConstructingBuilderId takes nothing returns integer
*          Returns index of builder unit.
*
*       function GetTriggeringStructure takes nothing returns unit
*          Retrieves event structure unit.
*
*       function GetTriggeringStructureId takes nothing returns integer
*          Returns index of constructed structure unit.
*
******************************************************************************
*
*    Functions:
*
*       function GetStructureBuilder takes unit whichUnit returns unit
*          Gets unit which constructed given structure.
*
*       function GetStructureBuilderById takes integer whichIndex returns unit
*          Gets unit which constructed given structure.
*
*       function IsStructureFinished takes unit whichUnit returns boolean
*          Checks whether construction of provided structure has been completed.
*
*****************************************************************************/
library ConstructEvent requires RegisterPlayerUnitEvent, ListT, UnitDex

globals
    integer EVENT_UNIT_CONSTRUCTION_START
    integer EVENT_UNIT_CONSTRUCTION_FINISH
    integer EVENT_UNIT_CONSTRUCTION_CANCEL
    integer EVENT_UNIT_CONSTRUCTION_INTERRUPT
endglobals

native UnitAlive takes unit id returns boolean

globals
    private IntegerList ongoing = 0
    private timer looper = CreateTimer()
    private unit eventBuilder = null
    private unit eventConstruct = null

    private unit array builders
    private boolean array finished
    private integer array instances
    private boolean array cancelled
endglobals

function GetConstructingBuilder takes nothing returns unit
    return eventBuilder
endfunction

function GetConstructingBuilderId takes nothing returns integer
    return GetUnitId(eventBuilder)
endfunction

function GetTriggeringStructure takes nothing returns unit
    return eventConstruct
endfunction

function GetTriggeringStructureId takes nothing returns integer
    return GetUnitId(eventConstruct)
endfunction

function GetStructureBuilder takes unit whichUnit returns unit
    return builders[GetUnitId(whichUnit)]
endfunction

function GetStructureBuilderById takes integer whichIndex returns unit
    return builders[whichIndex]
endfunction

function IsStructureFinished takes unit whichUnit returns boolean
    return finished[GetUnitId(whichUnit)]
endfunction

private function FireEvent takes integer evt, unit builder, unit structure returns nothing
    local unit prevBuilder = eventBuilder
    local unit prevConstruct = eventConstruct
    local integer playerId = GetPlayerId(GetOwningPlayer(builder))

    set eventBuilder = builder
    set eventConstruct = structure

    call TriggerEvaluate(GetNativeEventTrigger(evt))
    if IsNativeEventRegistered(playerId, evt) then
        call TriggerEvaluate(GetIndexNativeEventTrigger(playerId, evt))
    endif

    set eventBuilder = prevBuilder
    set eventConstruct = prevConstruct
    set prevBuilder = null
    set prevConstruct = null
endfunction

/*
*  Unit with no path-texture can be placed in 'arbitrary' location, that is, its x and y
*  won't have integral values. Whatmore, those values will be modified, depending on quarter of
*  coordinate axis where unit is going to be built. This takes form of rounding up and down to
*  0.250-floor.
*
*  Calculus is different for positive (+x/+y) and negative values (-x/-y).
*  This function makes sure data is translated accordingly.
*/
private function TranslateXY takes real r returns real
    if r >= 0 then
        return R2I(r / 0.250) * 0.250
    else
        return (R2I(r / 0.250) - 1) * 0.250
    endif
endfunction

/*
*  Whether issued order can be counted as a build-ability order. Accounts for:
*  orders between useslot1 and useslot6, plus build tiny building - item ability.
*  Build type: start and forget, just like undead Acolytes do.
*/
private function IsBuildOrder takes integer o returns boolean
    return (o >= 852008 and o <= 852013) or (o == 852619)
endfunction

/*
*  On the contrary to what's described in TranslateXY regarding building position,
*  builder will have his coords "almost" unchanged. This creates situation
*  where builder and construct do not share the same location (x & y).
*
*  Additionally, builder's coords will differ from order's by a small margin. It could be
*  negligible if not for the fact that difference can be greater than or equal to 0.001.
*  In consequence, this invalidates usage of '==' operator when comparing coords
*  (i. e. jass reals - 3 significant digits displayed).
*/
private function IsUnitWithinCoords takes unit u, real x, real y returns boolean
    return (RAbsBJ(GetUnitX(u) - x) == 0) and (RAbsBJ(GetUnitY(u) - y) == 0)
endfunction

private struct PeriodicData extends array
    unit builder     // unit which started construction
    race btype       // build-type, race dependant
    real cx          // future construction point x
    real cy          // future construction point y
    real distance    // for distance measurement, UD/HUM only
    integer order    // unit-type order id
    real ox          // order point x
    real oy          // order point y

    implement Alloc

    method destroy takes nothing returns nothing
        set instances[GetUnitId(builder)] = 0
        set builder = null
        set btype = null

        call ongoing.erase(ongoing.find(this))
        if ongoing.empty() then
            call PauseTimer(looper)
        endif

        call deallocate()
    endmethod

    static method create takes thistype this, unit u, integer orderId, boolean flag, real x, real y returns thistype
        if this == 0 then
            set this = allocate()
            set builder = u
            set instances[GetUnitId(u)] = this
            call ongoing.push(this)
        endif

        set order = orderId
        if flag then // ability based build order
            set btype = RACE_UNDEAD
        else
            set btype = GetUnitRace(builder)
        endif

        set cx = x
        set cy = y
        set ox = cx
        set oy = cy

        if cx - I2R(R2I(cx)) != 0 then
            set cx = TranslateXY(cx)
        endif
        if cy - I2R(R2I(cy)) != 0 then
            set cy = TranslateXY(cy)
        endif

        return this
    endmethod
endstruct

private function OnCallback takes nothing returns nothing
    local IntegerListItem iter = ongoing.first
    local PeriodicData obj
    local real dx
    local real dy

    loop
        exitwhen iter == 0
        set obj = iter.data

        if not UnitAlive(obj.builder) then
            call obj.destroy()
        elseif obj.btype == RACE_UNDEAD or obj.btype == RACE_HUMAN then
            set dx = obj.cx - GetUnitX(obj.builder)
            set dy = obj.cy - GetUnitY(obj.builder)
            set obj.distance = dx*dx + dy*dy
        endif

        set iter = iter.next
    endloop
endfunction

private function OnNonPointOrder takes nothing returns nothing
    local unit u = GetTriggerUnit()
    local unit target = GetOrderTargetUnit()
    local integer index = GetUnitId(u)
    local integer orderId = GetIssuedOrderId()
    local PeriodicData obj = instances[index]

    // Non handle-type orders usually take 852XXX form and are below 900000
    if orderId > 900000 and target != null then
        if ongoing.empty() then
            call TimerStart(looper, 0.031250000, true, function OnCallback)
        endif
        call PeriodicData.create(obj, u, orderId, true, GetUnitX(target), GetUnitY(target))

        set target =  null
    elseif obj != 0 then
        call obj.destroy()
    elseif orderId == 851976 and builders[index] != null and not finished[index] then // order cancel
        if not IsUnitType(u, UNIT_TYPE_STRUCTURE) then
            set cancelled[index] = true
            call FireEvent(EVENT_UNIT_CONSTRUCTION_CANCEL, null, u)
        endif
    endif

    set u = null
endfunction

private function OnPointOrder takes nothing returns nothing
    local unit u = GetTriggerUnit()
    local integer orderId = GetIssuedOrderId()
    local PeriodicData obj = instances[GetUnitId(u)]
    local boolean isBuildOrder = IsBuildOrder(orderId)

    // Non handle-type orders usually take 852XXX form and are below 900000
    if orderId > 900000 or isBuildOrder then
        if ongoing.empty() then
            call TimerStart(looper, 0.031250000, true, function OnCallback)
        endif
        call PeriodicData.create(obj, u, orderId, isBuildOrder, GetOrderPointX(), GetOrderPointY())
    elseif obj != 0 then
        call obj.destroy()
    endif

    set u = null
endfunction

private function OnConstructCancel takes nothing returns nothing
    local unit u = GetTriggerUnit()

    set cancelled[GetUnitId(u)] = true
    call FireEvent(EVENT_UNIT_CONSTRUCTION_CANCEL, null, u)

    set u = null
endfunction

private function OnConstructFinish takes nothing returns nothing
    local unit u = GetTriggerUnit()

    set finished[GetUnitId(u)] = true
    call FireEvent(EVENT_UNIT_CONSTRUCTION_FINISH, null, u)

    set u = null
endfunction

private function OnIndex takes nothing returns nothing
    local unit u = GetIndexedUnit()
    local integer id = GetUnitTypeId(u)
    local IntegerListItem iter = ongoing.first
    local PeriodicData obj
    local PeriodicData found = 0
    local real d = 1000000

    loop
        exitwhen iter == 0
        set obj = iter.data

        if obj.order == id or IsBuildOrder(obj.order) then
            if obj.cx == GetUnitX(u) and obj.cy == GetUnitY(u) then
                if obj.btype == RACE_HUMAN or obj.btype == RACE_UNDEAD then
                    if obj.distance < d then
                        set d = obj.distance
                        set found = obj
                    endif
                elseif IsUnitWithinCoords(obj.builder, obj.ox, obj.oy) then
                    set found = obj
                    exitwhen true
                endif
            endif
        endif

        set iter = iter.next
    endloop

    if found != 0 then
        set builders[GetIndexedUnitId()] = found.builder
        call FireEvent(EVENT_UNIT_CONSTRUCTION_START, found.builder, u)
        call found.destroy()
    endif

    set u = null
endfunction

private function OnDeindex takes nothing returns nothing
    local integer index = GetIndexedUnitId()

    if instances[index] != 0 then
        call PeriodicData(instances[index]).destroy()
    elseif builders[index] != null then
        if not (finished[index] or cancelled[index]) then
            call FireEvent(EVENT_UNIT_CONSTRUCTION_INTERRUPT, null, GetIndexedUnit())
        endif

        set builders[index] = null
        set finished[index] = false
        set cancelled[index] = false
    endif
endfunction

private module ConstructEventInit
    private static method onInit takes nothing returns nothing
        set EVENT_UNIT_CONSTRUCTION_START = CreateNativeEvent()
        set EVENT_UNIT_CONSTRUCTION_CANCEL = CreateNativeEvent()
        set EVENT_UNIT_CONSTRUCTION_FINISH = CreateNativeEvent()
        set EVENT_UNIT_CONSTRUCTION_INTERRUPT = CreateNativeEvent()
        set START = EVENT_UNIT_CONSTRUCTION_START
        set CANCEL = EVENT_UNIT_CONSTRUCTION_CANCEL
        set FINISH = EVENT_UNIT_CONSTRUCTION_FINISH
        set INTERRUPT = EVENT_UNIT_CONSTRUCTION_INTERRUPT

        set ongoing = IntegerList.create()

        call RegisterAnyPlayerUnitEvent(EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER, function OnNonPointOrder)
        call RegisterAnyPlayerUnitEvent(EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER, function OnPointOrder)
        call RegisterAnyPlayerUnitEvent(EVENT_PLAYER_UNIT_ISSUED_ORDER, function OnNonPointOrder)
        call RegisterAnyPlayerUnitEvent(EVENT_PLAYER_UNIT_CONSTRUCT_CANCEL, function OnConstructCancel)
        call RegisterAnyPlayerUnitEvent(EVENT_PLAYER_UNIT_CONSTRUCT_FINISH, function OnConstructFinish)

        call RegisterUnitIndexEvent(Condition(function OnIndex), EVENT_UNIT_INDEX)
        call RegisterUnitIndexEvent(Condition(function OnDeindex), EVENT_UNIT_DEINDEX)
    endmethod
endmodule

struct ConstructEvent
    readonly static integer START
    readonly static integer CANCEL
    readonly static integer FINISH
    readonly static integer INTERRUPT

    implement ConstructEventInit
endstruct

function GetEventBuilder takes nothing returns unit
    debug call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60,"Function GetEventBuilder is obsolete, use GetContructingBuilder instead.")
    return GetConstructingBuilder()
endfunction

function GetEventBuilderId takes nothing returns integer
    debug call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60,"Function GetEventBuilderId is obsolete, use GetContructingBuilderId instead.")
    return GetConstructingBuilderId()
endfunction

function GetEventStructure takes nothing returns unit
    debug call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60,"Function GetEventStructure is obsolete, use GetContructedStructure instead.")
    return GetTriggeringStructure()
endfunction

function GetEventStructureId takes nothing returns integer
    debug call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60,"Function GetEventStructureId is obsolete, use GetContructedStructureId instead.")
    return GetTriggeringStructureId()
endfunction

function RegisterConstructEvent takes code func, integer whichEvent returns nothing
    debug call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60,"Function RegisterConstructEvent is obsolete, use RegisterNativeEvent instead.")
    call RegisterNativeEvent(whichEvent, func)
endfunction

function GetConstructEventTrigger takes integer whichEvent returns trigger
    debug call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60,"Function GetConstructEventTrigger is obsolete, use GetIndexNativeEventTrigger instead.")
    return GetNativeEventTrigger(whichEvent)
endfunction

endlibrary

Demo code:
JASS:
scope ConstructEventDemo initializer Init

private function OnStart takes nothing returns nothing
    call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 5, GetUnitName(GetConstructingBuilder())+" started construction of: "+/*
    */GetUnitName(GetTriggeringStructure())+" builder index: "+I2S(GetConstructingBuilderId())+/*
    */" builder life pts: "+R2S(GetWidgetLife(GetConstructingBuilder())))
endfunction

private function OnCancel takes nothing returns boolean
    call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 5, GetUnitName(GetConstructingBuilder())+" cancelled construction of: "+/*
    */GetUnitName(GetTriggeringStructure())+" structure index: "+I2S(GetTriggeringStructureId())+/*
    */" builder life pts: "+R2S(GetWidgetLife(GetConstructingBuilder())))
    return false
endfunction

private function OnFinish takes nothing returns nothing
    call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 5, GetUnitName(GetConstructingBuilder())+" finished construction of: "+/*
    */GetUnitName(GetTriggeringStructure())+" structure index: "+I2S(GetTriggeringStructureId())+/*
    */" builder life pts: "+R2S(GetWidgetLife(GetConstructingBuilder())))
endfunction

private function OnInterrupt takes nothing returns boolean
    call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 5, GetUnitName(GetConstructingBuilder())+" construction interrupted: "+/*
    */GetUnitName(GetTriggeringStructure())+" structure index: "+I2S(GetTriggeringStructureId())+/*
    */" builder life pts: "+R2S(GetWidgetLife(GetConstructingBuilder())))
    return false
endfunction

private function Init takes nothing returns nothing
    call RegisterNativeEvent(EVENT_UNIT_CONSTRUCTION_START, function OnStart)
    call RegisterNativeEvent(EVENT_UNIT_CONSTRUCTION_CANCEL, function OnCancel)
    call RegisterNativeEvent(EVENT_UNIT_CONSTRUCTION_FINISH, function OnFinish)
    call RegisterNativeEvent(EVENT_UNIT_CONSTRUCTION_INTERRUPT, function OnInterrupt)
endfunction

endscope
 
Last edited:
Level 23
Joined
Apr 16, 2012
Messages
4,041
you should set W and Q to null after all triggers stop their execution, because you can call GetEventBuilder and you should return null if it is ran by the trigger registration

Its similar to natives, GetTriggerUnit() will return null if there is no triggering unit
 
Level 23
Joined
Apr 16, 2012
Messages
4,041
yea, you could actually null it in OnBuildEvent method(at the very end, set Q = null and W = null, so when I just randomly call GetEventBuilder, I will not get value

its only suggestion tho, and if you think its not good, you dont need to do it, it would just go more to par with the native event responses
 
Level 17
Joined
Apr 27, 2008
Messages
2,455
Hello, no need credit just for the idea to simulate a lacking native event.
I mean it's not something original, nor something special.

If it can help you :
http://www.thehelper.net/threads/construction-events.128841/

Too bad i don't have anymore code about it, but iirc you can't have a 100 % accurate way.
But sure you can make some approximations.
Undead and human workers are a pain in the ass to handle.
 

Bannar

Code Reviewer
Level 26
Joined
Mar 19, 2008
Messages
3,140
Have you tested it? Test map proves that undead & buildings built via consumable items can be catch. Could not find any bugs as for now, although for RACE_HUMAN it's only registering the first peasant who started construction.

Should I work on getting all the peasants (or simply: workers with special repair ability) which took part in constructing given unit?
 
Level 17
Joined
Apr 27, 2008
Messages
2,455
Well, it's obvious that you're inaccurate here, since you're using timers, check distances, and not only rely on native events (orders and such).
Iirc i've managed to be 100 % accurate for builders which were not human and undead, simply with construct and order events, but for human and especially undead ones you need to "guess", like you do with your script.
Humans, because of power building (several workers on the same structure), and undead because the worker still keep the build order few time after the structure is being built, and check distance is not reliable (collision, range and such).

I suppose it's ok in almost every scenario, personnaly i just couldn't bare the innacuracy (even if it's most likely only theoric), hence the reason why i've never submitted it.

And no i have not tested it, i have not wc3 installed anymore since a while now.

Oh and remember the build abilities also. (an unit can have directly the ability, not through an item structure).
I suppose it's ok in almost every scenario, personnaly i just couldn't bare the inaccuracy (even if it's most likely only theoric), hence the reason why i've never submitted it.

And no i have not tested it, i have not wc3 installed anymore since a while now.

Oh and remember the build abilities also. (an unit can have directly the ability, not through an item).
Just check if the order is an unit rawcode or not (simply UnitId2String(<issued order>) != 0)
Maybe, you handle this with that : "o > 900000" but that seems terrible.

Also if you care about loaded games you shouldn't directly use UnitId2String != null but something like that instead :

JASS:
library IsUnitTypeId requires Table

/* The point of this library is to bypass a nasty bug, but it's only present
with loaded game, if you don't care about them then you would just inline
UnitId2String(yourUnitTypeId) != null instead of using this library */
 
    private keyword s_dummy 
     
    private function GameLoaded takes nothing returns boolean  
        set s_dummy.is_game_loaded = true  
        call DestroyBoolExpr(Condition(function GameLoaded))  
        call DestroyTrigger(GetTriggeringTrigger())  
        return false  
    endfunction  
     
    // coz of vJass madness initializer module priority, even if i  hardly see the usefulness of registring a such event on a module  initializer ...
    // but in some case this kind of initializer can be "handy", here we avoid an unecessary init
  
    private module onInit  
  
        static method onInit takes nothing returns nothing  
            local trigger trig = CreateTrigger()  
            call TriggerRegisterGameEvent(trig,EVENT_GAME_LOADED)  
            call TriggerAddCondition(trig,Condition(function GameLoaded))  
            set trig = null  
            set s_dummy.tab = Table.create() 
        endmethod  
  
    endmodule  
  
    private struct s_dummy extends array // the extends array avoid some unneeded stuff
        static Table tab 
        static boolean is_game_loaded = false 
        implement onInit  
    endstruct  
     
    function IsGameLoaded takes nothing returns boolean 
        return s_dummy.is_game_loaded 
    endfunction 
 
function IsUnitTypeId takes integer unitid returns boolean 
 
        local unit u 
        if IsGameLoaded() then 
 
            if s_dummy.tab.exists(unitid) then 
                return s_dummy.tab[unitid] != 0 
            endif 
 
            // IsUnitIdType is not enough , IsUnitHeroId only check if  the first letter of the id is an uppercase and not if the id is a valid  hero id
            // so i've figured only this way, or you can use a third  party tool instead, like GMSI, but imho it's overskilled here
            set u = CreateUnit(Player(13),unitid,0.,0.,0.) 
 
            if u != null then 
                call RemoveUnit(u) 
                set u = null 
                set s_dummy.tab[unitid] = 1 
                return true 
            else 
                set s_dummy.tab[unitid] = 0 
                return false 
            endif 
        endif
    return UnitId2String(unitid) != null // always returns null when the game was saved and loaded, and yes it's a bug
      
endfunction 
 
endlibrary
 
Last edited:

Bannar

Code Reviewer
Level 26
Joined
Mar 19, 2008
Messages
3,140
^ With UnitId2String usage I managed to get some crit errors, thats why I resignated. Meaby it was by my mistake, although still I prefered the (o>900000) since no unittype-order is magnitude lower than 10^9 or so.

Guys work on this tommorow - Game of Thrones is too good to miss it.

Edit: Btw, nice post edditing TB ^^
 

Bannar

Code Reviewer
Level 26
Joined
Mar 19, 2008
Messages
3,140
You don't have to be mean. I understand your suggestion about variable names and have just changed those. Hope names are good enough.

Updated.

TB - yeah, I've forgotten about 852619 order since I had not been touching this for year and a half or so.
You can not compareUnitId2String to integer nor real, since it returns string. I guess you meant null.
Replaced (o > 900000) with UnitId2String(o). Anyways, like I've already said - I don't think there is a posibility that magnitude of unit-type order is lower than 10^9 or so. If I'm right, the 1st option is faster and fits purpose equaly well.

I will start working on catching repair order instead of timer usage. Regarding ud accurancy - I havent found better trick than distance check. As for now, there were no bugs detected.
Will test few things here and there though.
 
There is a case in which the global array would take up more space, but it's not something that should be worried about:

JASS:
integer array data

set data[0] = 0 // data has been resized to an array of size 1
set data[1] = 0 // data has been resized to an array of size 2
set data[2] = 0 // data has been resized to an array of size 4
set data[3] = 0
set data[4] = 0 // data has been resized to an array of size 8

set data[8191] = 0 // data has been resized to an array of size 8192
 
Level 17
Joined
Apr 27, 2008
Messages
2,455
Checking if the order is > to some enough big integer should be fine, unless there are some special orders within this range (but i've not calculated which is the lowest unit rawcode possible).
When there is a 100 % sure way, i go for it, that makes sense.

Oh and i'm not mean , i'm just bored to see "everywhere" these fugly short names, nothing personal.
 

Bannar

Code Reviewer
Level 26
Joined
Mar 19, 2008
Messages
3,140
TB - I will wait for more opinions about checking for unit-type order. Personally I prefer simple comparison approach. As for now - ur method stays.

So..
JASS:
        private method onFire takes nothing returns nothing
            local unit sw = builder
            local unit sc = constrt
            set builder = this.worker
            set constrt = eventUnit
            static if LIBRARY_BuildUtils then
                set builderArray[GetUnitId(eventUnit)] = this.worker
            endif
            call FireEvent(BuildEvent.BUILD)
            set builder = sw
            set constrt = sc
        endmethod
or the current one.
 
You're free to use super long variable names.

Short variable names are only called for in cases when you're dealing with coordinates and geometric shit.

x, y, z, dx, dy, dz.

Sometimes u is acceptable for units (local units or parameters for small functions)
i, j and k are acceptable for iteration indexes (local ones)
 

Bannar

Code Reviewer
Level 26
Joined
Mar 19, 2008
Messages
3,140
Below updated BuildEvent code. As of now, timer designed to check state of "potential builders" will be paused if no such units are found. This is as almost as accurate as it gets. The last possible thing left is usage of onDeindex method to remove most of inaccurancy left within checkState method.
RPUE and Event can be made optional.

100% accurancy for Orc and NE. 99,9% for UD and HUM - honestly, I haven't been able to perform any bug that would declare fail builder, thats why I would like you to test this lib yourself. I will soon strip some of the code since getting "finishers" (units which were there, when construction has been finished - in regard to HUM race) is not the matter of this library. Instances will be lost as soon as construction event fires and builder is determinated.
JASS:
library BuildEvent requires Event UnitIndexer RegisterPlayerUnitEvent TimerUtils

    /* recongized orders:
        852024 - repair
        852008 to 852013 - slot item usage
        852619 - Albg : construct ability
    */
    globals
        private unit eventBuilder = null // Constructor
        private unit eventBuilding = null // Construct
        private integer array builders // helper array
        private timer looper = CreateTimer()
    endglobals

    function GetEventBuilder takes nothing returns unit
        return eventBuilder
    endfunction

    function GetEventStructure takes nothing returns unit
        return eventBuilding
    endfunction

    function RegisterUnitBuildEvent takes code c returns nothing
        call BuildEvent.BUILD.register(Filter(c))
        return
    endfunction

    module BuildEventModule
        static method operator builder takes nothing returns unit
            return eventBuilder
        endmethod

        static method operator building takes nothing returns unit
            return eventBuilding
        endmethod

        static if thistype.onBuild.exists then
            private static method onBuildEvent takes nothing returns nothing
                call thistype(GetUnitId(GetEventStructure())).onBuild()
            endmethod

            private static method onInit takes nothing returns nothing
                call RegisterUnitBuildEvent(function thistype.onBuildEvent)
            endmethod
        endif
    endmodule

    static if not LIBRARY_UnitEvent then
        function IsUnitDead takes unit u returns boolean
            return ( GetWidgetLife(u) < 0.405 ) or ( GetUnitTypeId(u) == 0 )
        endfunction
    endif

    //! runtextmacro optional BUILD_UTILS_STRUCT_MACRO()

    private module BuildEventInit
        private static method onInit takes nothing returns nothing
            call RegisterPlayerUnitEvent(EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER, function thistype.onPointOrder)
            call RegisterPlayerUnitEvent(EVENT_PLAYER_UNIT_CONSTRUCT_START, function thistype.onBuildEvent)
            call RegisterPlayerUnitEvent(EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER, function thistype.onDifferentOrder)
            call RegisterPlayerUnitEvent(EVENT_PLAYER_UNIT_ISSUED_ORDER, function thistype.onDifferentOrder)
            call RegisterUnitIndexEvent(Condition(function thistype.onDeindex), UnitIndexer.DEINDEX)
            set thistype.BUILD = CreateEvent()
        endmethod
    endmodule

    struct BuildEvent extends array
        readonly static Event BUILD
        private static unit eventUnit = null

        private static integer instanceCount = 0
        private thistype recycle
        private thistype next
        private thistype prev

        private unit worker
        private integer order
        private race btype  // build-type: race dependant
        private real cx // build-order x
        private real cy // build-order y
        private real dist // undead only

        private method deallocate takes nothing returns nothing
            set this.recycle = thistype(0).recycle
            set thistype(0).recycle = this
            set this.next.prev = this.prev
            set this.prev.next = this.next

            set builders[GetUnitId(this.worker)] = 0
            set this.worker = null
            set this.btype = null

            if ( thistype(0).next == 0 ) then
                call PauseTimer(looper)
            endif
        endmethod

        private static method allocate takes nothing returns thistype
            local thistype this = thistype(0).recycle

            if ( thistype(0).next == 0 ) then
                call TimerStart(looper, .03125, true, function thistype.checkState)
            endif

            if ( this == 0 ) then
                set instanceCount = instanceCount + 1
                set this = instanceCount
            else
                set thistype(0).recycle = this.recycle
            endif
            set this.next = 0
            set this.prev = thistype(0).prev
            set thistype(0).prev.next = this
            set thistype(0).prev = this

            return this
        endmethod

        private method onFire takes nothing returns nothing
            local unit saveBuilder = eventBuilder
            local unit saveConstruct = eventBuilding

            set eventBuilder = this.worker
            set eventBuilding = eventUnit

            static if LIBRARY_BuildUtils then
                set builderArray[GetUnitId(eventUnit)] = this.worker
            endif

            call FireEvent(BuildEvent.BUILD)
            set eventBuilder = saveBuilder
            set eventBuilding = saveConstruct
            call this.deallocate()
        endmethod

        private static method repairCheck takes nothing returns nothing
            local thistype this = GetTimerData(GetExpiredTimer())
            if ( GetUnitCurrentOrder(this.worker) == 852024 ) then
                call this.onFire()
            endif
            call ReleaseTimer(GetExpiredTimer())
        endmethod

        private static method onDifferentOrder takes nothing returns nothing
            local thistype this = builders[GetUnitId(GetTriggerUnit())]
            if ( this != 0 ) then
                if ( this.btype != RACE_HUMAN or GetIssuedOrderId() != 852024 ) then
                    call this.deallocate()
                endif
            endif
        endmethod

        private static method onDeindex takes nothing returns boolean
            local thistype this = builders[GetIndexedUnitId()]
            if ( this != 0 ) then
                call this.deallocate()
            endif
            return false
        endmethod

        private static method checkState takes nothing returns nothing
            local thistype this = thistype(0).next
            local real dx
            local real dy
            loop
                exitwhen 0 == this
                if IsUnitDead(this.worker) then
                    call this.deallocate()
                elseif this.btype == RACE_UNDEAD then
                    set dx = this.cx - GetUnitX(this.worker)
                    set dy = this.cy - GetUnitY(this.worker)
                    set this.dist = dx*dx + dy*dy
                endif
                set this = this.next
            endloop
        endmethod

        private static method isOrderProper takes integer o returns boolean
            return ( o >= 852008 and o <= 852013 ) or ( o == 852619 )
        endmethod

        private static method onPointOrder takes nothing returns nothing
            local integer o = GetIssuedOrderId()
            local boolean b = isOrderProper(o)
            local integer index = GetUnitId(GetTriggerUnit())
            local thistype this = builders[index]

            if ( UnitId2String(o) != null ) or ( b ) then
                if ( this == 0 ) then
                    set this = thistype.allocate()
                    set this.worker = GetTriggerUnit()
                    set builders[index] = this
                endif
                set this.order = o
                if ( b ) then
                    set this.btype = RACE_UNDEAD
                else
                    set this.btype = GetUnitRace(this.worker)
                endif
                set this.cx = GetOrderPointX()
                set this.cy = GetOrderPointY()
            elseif ( this != 0 ) then
                call this.deallocate()
            endif
        endmethod

        private static method onBuildEvent takes nothing returns nothing
            local thistype this = thistype(0).next
            local real qx
            local real qy
            local real d = 1000000
            local thistype undead = 0

            set eventUnit = GetTriggerUnit()
            set qx = GetUnitX(eventUnit)
            set qy = GetUnitY(eventUnit)
            loop
                exitwhen 0 == this
                if ( qx == this.cx and qy == this.cy ) then
                    if ( this.order == GetUnitTypeId(eventUnit) or isOrderProper(this.order) ) then

                        if ( this.btype == RACE_UNDEAD ) then
                            if ( this.dist < d ) then
                                set d = this.dist
                                set undead = this
                            endif
                        elseif ( this.btype == RACE_HUMAN ) then
                            call TimerStart( NewTimerEx(this), 0, false, function thistype.repairCheck )
                        elseif ( GetUnitX(this.worker) == qx ) and ( GetUnitY(this.worker) == qy ) then
                            call this.onFire()
                            return // we can safely return
                        endif

                    endif
                endif
                set this = this.next
            endloop
            if ( undead != 0 ) then
                call undead.onFire()
            endif
        endmethod

        implement BuildEventInit
    endstruct
endlibrary
Demo:
JASS:
struct BuildEventDemo extends array
    private method onBuild takes nothing returns nothing
        call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 90000, GetUnitName(builder)+/*
        */" started construction of: "+GetUnitName(building))
    endmethod

    private static method demo takes nothing returns nothing
        call BJDebugMsg(GetUnitName(GetEventBuilder())+" started construction of: "+/*
        */GetUnitName(GetEventStructure())+" index: "+I2S(GetUnitId(GetEventBuilder()))+/*
        */" life pts: "+R2S(GetWidgetLife(GetEventBuilder())))
    endmethod
    
    private static method onInit takes nothing returns nothing
        call RegisterUnitBuildEvent(function thistype.demo)
    endmethod
    implement BuildEventModule
endstruct
EDIT: Updated. checkState method is now only for RACE_UNDEAD-kind of build type (measures distance). Added onDeindex and onDifferentOrder methods to fix possibly last inaccurancy left within library. One will surely get his builder.
 
Last edited:

Bannar

Code Reviewer
Level 26
Joined
Mar 19, 2008
Messages
3,140
Updated to 2.0.0.0. Renamed to ConstructEvent. No longer features module interface.
There is no inner doc yet; However, scroll down for interface description. There were many wierd things I've discovered, though there are no comments, so I won't be explaining anything deeper untill appropriate questions show up.
After multiple tests, distance checking came out to be safer and more accurate than timer + check repair-order, thus TimerUtils requirement has been removed. UnitId2String replaced again with magnitude check.

Implementation of library varies highly, it takes in consideration both optionals and whatmore, there is 3rd variable in form of constant bool to advance library. If one wishses to receive construction orders from organic buildings, item-usage building, ability-usage building and units with invalid "path - texture" field ADVANCED option will allow him to. Additionally, features CANCEL and FINISH events. Once again, depending on implementation, event from every or just "standard" buildings will be caugh.

Existence of "Start/Cancel/Finish" api functions is subject to change. Generic ones may be enough.
 

Bannar

Code Reviewer
Level 26
Joined
Mar 19, 2008
Messages
3,140
Cleaned up the code and made this slighty more efficient. There is not much left to change now. I'm really happy how it turned out and had fun testing it :)

Added proper demo code and snippet's description at the top.

Resignated from ADVANCED const - as of now, this system handles every construct event, no matter what is being build and by which method -> you take it all, or you don't at all ^)^
Defined an additional event INTERRUPT to distinguish intentional construction stop events (CANCEL) from those which were not intentional i.e unit died or has been removed.

It's very flexible when it comes to requirements - requires only a Unit-Indexer library -> added support for both, most popular choices.
The rest is optional, and when it comes to RPUE, both, newer version of mine (from RegisterEvent pack) and Maggy's older one are supported.

Have fun using/testing.

Edit: Hope a mod can change the thread's title, as requested in february.
 

Bannar

Code Reviewer
Level 26
Joined
Mar 19, 2008
Messages
3,140
This is because ConstructEvent is implement to deal with Point-Order building events. Unfortunately, Haunted and Entangled Gold Nines are Target-Order oriented events with later being a corner case.

I've deployed small update to improve documentation a little, enable users to retrieve trigger event handle via function GetConstructEventTrigger takes integer whichEvent returns trigger and to include Haunted Gold Mine case as requested. ConstructEvent will now evaluate TargetOrders in almost exact same fashion as it does PointOrders. This covers "all" TargetOrder cases, which is actually just one case - increase of complexity is the loss here. This now also fully supports both versions of RegisterPlayerUnitEvent (mine & Maggy's).

Entangled Gold Mine is/should be treated as an entirely different chapter, because it is more of spell based building with almost as many flaws as native weathereffect handle. That should probably be covered in some plugin for ConstructEvent without unnecessarily increasing complexity of library just to cover one last case.
 

Bannar

Code Reviewer
Level 26
Joined
Mar 19, 2008
Messages
3,140
Fixed minor issue with INTERRUPT event. Added IsStructureFinished() function to complement API.
Completely removed support for UnitDex. Just UnitIndexer/ UnitIndexerGUI now.
Reviewed code again after last update (covered TargetOrder-oriented structure events) and did minor tweaks to improve readability/ enhance code.
 

Bannar

Code Reviewer
Level 26
Joined
Mar 19, 2008
Messages
3,140
ConstructEvent overhaul.

Refactored ConstructEvent library to be function based, rather than method based.

Improvements and readability:
- Appended documentation for key parts of code in hope of dispelling some mysteries around structure construction process
- Native 24-player support due to RegisterNativeEvent
- Added PeriodicData struct purely for storing data required for periodic handlers
- ConstructEvent now separate struct, for initialization purpose only
- ConstructEvent no longer a linked list struct
- Condition functions now declared as 'returns nothing' rather than 'returns boolean'

Now Wurst friendly:
- Removed TriggerRegisterAnyDoubleClickEvent function
- Removed TriggerRegisterVariableEvent related globals. FireEvent now uses TriggerEvaluate directly
- Renamed local variables which had their names conflicting with Wurst reserved keywords

Requirement changes:
- Now requires List (to strip ConstructEvent from list functionality and remove redundant code)
- No longer supports deprecated RegisterPlayerUnitEvent. Use one from RegisterNativeEvent pack instead
- By default supports only UnitIndexer: UnitDex by TriggerHappy. All the rest have been made obsolete.
Note:
Nestratus' UnitIndexer is out of scope - indexer library should be kept simple, just like TH's and Wurst UnitIndexer package are.
If in need, nothing stops user from modifing library initialization in order to support any UI he or she wishes to use.

Following functions and members have been made obsolete:
- RegisterConstructEvent function, deprecated in favor of RegisterNativeEvent
- GetConstructEventTrigger function, deprecated in favor of GetNativeEventTrigger

- GetEventBuilder function, deprecated in favor of GetConstructingBuilder
- GetEventBuilderId function, deprecated in favor of GetConstructingBuilderId
Change made to underline fact that those two functions above return valid results only for EVENT_UNIT_CONSTRUCTION_START.

- GetEventStructure function, deprecated in favor of GetTriggeringStructure
- GetEventStructureId function, deprecated in favor of GetTriggeringStructureId
- ConstructEvent.START integer deprecated in favor of EVENT_UNIT_CONSTRUCTION_START
- ConstructEvent.CANCEL integer deprecated in favor of EVENT_UNIT_CONSTRUCTION_CANCEL
- ConstructEvent.FINISH integer deprecated in favor of EVENT_UNIT_CONSTRUCTION_FINISH
- ConstructEvent.INTERRUPT integer deprecated in favor of EVENT_UNIT_CONSTRUCTION_INTERRUPT

Updated library header documentation.
Adjusted demo code.

Important note:
Entangled gold mine is a unique and separate case which will not be detected by ConstructEvent in current form if ever. In order to do this correctly, a separate plugin could be added. As of now I have not found a clear way of detecting builder for entangled gold mine.

One has to consider fact that both, gold mine and entangling building can be:
- build via item ability
- have arbitrary location, no collision size and pathing map
- host (entangler) can be placed on top of mine, in fact several buildings can

Automatic entangle generates no order. Whatmore, the entangle ability is preserved after triggering gold mine entangling, despite being disabled for user - user can only see & use this ability if host building has no gold mine attached to it. This disallows developer from using GetUnitAbilityLevel native.

Important note:
Having a host building built nearby unoccupied gold mine causes game to crush if slave - structure that is attempted to be created - does not posses 'Aegm', Entangled Gold Mine Ability.

I've reported this issue to Blizzard.
 
Last edited:
Bannar, might you update to use the new RegisterNativeEvent library? In case you have a map file with all libs combined, please attach it so it's easier to test the system, too..;p

Also, I would find it helpful if it's documented:
  • why 4 custom events are there (vs. natives)
  • how is your process of finding the correct worker etc in words (one could analyse the code, but I would find it useful to easier understand and to compare overall logics of work-mechanics)
What happens if a worker starts building, then dies, but then an other worker finishes building's construction -- then even at finish-event the returned unit from GetStructureBuilder( ) could return a non-existent builder, right? (from docu)

Could you please elaborate what this means that it is only at 'start event' available, does it mean otherwise there is no such starter unit? :
JASS:
/*
*       function GetConstructingBuilder takes nothing returns unit
*          Retrieves event builder unit, valid only for START event.
*/
 

Bannar

Code Reviewer
Level 26
Joined
Mar 19, 2008
Messages
3,140
It uses newest RegisterNativeEvent library already - RegisterPlayerUnitEvent requires it. If I'm missing something, please, quote code part so I can fix it.

START / FINISH - obvious. FINISH is mainly for the sake of completeness.
CANCEL - intentional construction stop event e.g. ESC press.
INTERRUPT - unintentional construction stop event e.g. death.
In-game CANCEL isn't precise enough, does not state if action was done intentionally or not and does not even address all the cases.

GetConstructingBuilder works only for START event and will retrieve only the unit that has started the construction of a particular building. Unit-finished construction is a completely different scenario.
This function should and will return invalid unit for handlers attached to all event other than START event.

Most of the math is here to cover ORC-type construction process, where worker is hidden and immune. (X,Y) of construction will not equal the worker's, plus those will be corrected in-game depending on the quarter you've placed your structure in: [x,y], [-x, y], [x, -y] and [-x, -y].
There are comments added for most of these private functions. If you need more explanations, let me know, but I strongly advice to just hop into game and test some scenarios yourself.
 
Top