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

AttachObject v2.05

full





v2.00 and newer versions are not backward compatible with previous versions of AttachObject (they would be called AttachUnit). This is because of some changes to main functions code to make it easier to customise, but also to accommodate attaching special effects. My apologies for any inconvenience caused.

-v2.05
- Fixed an issue which was causing Guests to appear as Hosts.​
-v2.04
- Fixed an issue with attached unit Guests updating their z axis to the incorrect value​
- Fixed an issue with attached special effect Guests failing to update their yaw (facing)​
-v2.03
- Removed LinkedList and ListItem structs as they were buggy.​
- ListT is now a requirement for AttachObject to function.​
- Updated documentation to match​
-v2.02
- Fixed a bug that was prevent re-hosting of existing guests to attach properly.​
- DettachUnit() now takes 3 arguments rather than 2. A boolean parameter is now needed to tell the system whether to reset the unit's height or not upon Dettachment. This was needed to prevent re-hosted guests to reset their heights as their are being changing hosts.​
- LinkedList struct has a new .empty() member to return whether a list is empty or not.​
- Updated test map to use RiseAndFall v1.05.​
- Updated documentation.​
- [UPDATE] re-uploaded test map to fix issues with Demo code as well as updating RiseAndFall to v1.05. There has been no change to the AttachObject library itself.​
- v2.01
- DettachGuest() replaced with DettachUnit().​
- Added GetGuestList()​
- Guest and Host are private structs. GuestEx will now be the main reference for the Guest struct.​
- Add functions that previously took Guest or returned Guest instances now take or return GuestEx instead.​
- Added EffectTimed struct for destroying special effects.​
- Updated documentation with all the new features and changes.​
- v2.00 Library renamed to AttachObject and now supports attaching special effects. Unit Event was also removed as a dependency as it was unnecessary. Now uses a linked list for better performance when removing attached Guests. Not backward compatible.
- v1.01 added functions to hide and unhide guests. Those functions are called automatically when loading into or unload from a transport but now requires Unit Event by Bribe.
- v1.00 initial release
Contents

AttachObject v2.05 (Map)

Reviews
MyPad
I see that the resource has been updated with the points being addressed. Knowing that it is now at a state where refinement isn't really needed, and that I took a glance and could not find a logical flaw (quick search), I will approve this now...
v1.26 compatible version available here (does not support special effects): AttachObjects

Clanzion's post:
 
Last edited:
Level 7
Joined
Jun 28, 2017
Messages
317
Cool idea like the Docking System. :infl_thumbs_up: I use maybe in the future, everybody and I will try this and the docking system you made unless them and I will give you credit for all this. :)

EDIT: Also, i'll be watching your YT video of the Docking System again i think.
 
Level 22
Joined
Feb 6, 2014
Messages
2,466
Not a fan of functions with long list of arguments especially if most of the time, those values will be the same (staticAngle, offsetFix, etc.). Just my $0.02. You could make this object oriented so that these parameters can easily be controlled other than as an input argument (e.g. this.staticAngle = true)

By the way, the highlighting/text coloring is wrong. Most of the code is in color blue.
 
Not a fan of functions with long list of arguments especially if most of the time, those values will be the same (staticAngle, offsetFix, etc.). Just my $0.02. You could make this object oriented so that these parameters can easily be controlled other than as an input argument (e.g. this.staticAngle = true)
True, and while setting this up can be a tad confusing, just having a reference should be enough to make this not a problem. There aren't that many arguments to work with, anyway.

By the way, the highlighting/text coloring is wrong. Most of the code is in color blue.
That's an issue with the Hive's syntax highlighting not picking up comments within comments. I user /* */ to emphasize important sections of the API.
 

Deleted member 247165

D

Deleted member 247165

So awesome! Just imagine a human Siege Tank with a greater scaling value and equipped with 4-5 Riflemen on it. The ultimate warfare thing in Warcraft 3 universe. <3 5/5 for sure!
 
It can be done in GUI easily with one or two lines of custom script:
  • GUI Example
    • Events
      • Game - UnitIndexEvent becomes Equal to 1.00
    • Conditions
    • Actions
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • (Unit-type of UDexUnits[UDex]) Equal to Spell Breaker
        • Then - Actions
          • Set host = UDexUnits[UDex]
          • Set TempLoc = (Position of host)
          • Unit - Create 1 Faerie Dragon for (Owner of host) at TempLoc facing Default building facing degrees
          • Set guest = (Last created unit)
          • Custom script: call AttachUnitToHost(udg_guest, udg_host, 0., false, GetUnitFacing(udg_host), 0., 200., -16.)
          • Custom script: call SetGuestFacingProperties(udg_guest, GetUnitFacing(udg_host),10., 4., false, true)
          • Custom script: call RemoveLocation(udg_TempLoc)
        • Else - Actions
Refer to the API section of the code to know what argument does what. It's a bit messy in GUI because of no syntax-highlighting, but it's pretty simple.
JASS:
call AttachUnitToHost takes unit guest, unit host, real angle, boolean staticAngle, real angleFacing, real distance, real zOffset, real offsetFix
            ^ This function will attach a unit to a host.
         
            [_ARGUMENTS_]
            unit guest              - the unit to attach (aka the Guest)
            unit host               - the Host
            real angle              - the angle relative to the Host's facing angle at which the Guest is offset to.
            boolean staticAngle     - if true, the Guest's angle offset will ignore the Host's facing direction
            real angleFacing        - the starting facing angle of the Guest
            real distance           - the distance between the Host and the Guest
            real zOffset            - the height difference from the Host
            real offsetFix          - if your Guest is off-center, try setting this to -16 or 16.
         
         
        call SetGuestFacingProperties takes unit guest, real startAngle, real rate, real cooldown, boolean dynamicFacing, boolean turretMode
            ^ This sets an attached unit's facing properties. If this is not called, the unit will always face the same direction.
         
            [_ARGUMENTS_]
            unit guest              - the attached unit (aka Guest)
            real startAngle         - the angle at which the Guest begins at. If dynamicFacing is false, the guest will always face this specific angle.
            real rate               - the speed at which the Guest rotates over time. Set to zero to ignore.
            real cooldown           - this is neccessary if the Guest can attack. It will resume it facing parameters after that cooldown has expired.
            boolean dynamicFacing   - if this is true, the facing angle of the Guest will depend on the facing of the Host.
            boolean turretMode      - Set this to true if you want your Guest to be able to attack. Otherwise, the facing parameters will keep interferring.

Hope that helps!
 

Deleted member 219079

D

Deleted member 219079

Not a fan of functions with long list of arguments especially if most of the time, those values will be the same (staticAngle, offsetFix, etc.). Just my $0.02.
upload_2017-12-3_10-40-55.png


About the system, it doesn't work well with transport units: the mountees instantly warp to unload position of mount, dismissing their cargo cost. I think they should either get loaded along with the mount or the attachment should be broken.

And if a mountee is loaded it warps back to mount upon unload.
 
Level 11
Joined
Jul 4, 2016
Messages
626
What I meant was would it be possible to move the unit to various locations on the carrier while attached to the carrier.
 
I like the simplicity of how things are handled here, and how this does things so well.

Nitpicks:

  • When making vJASS resources, it is more appropriate to use existing vJASS resources as opposed to GUI resources.

Notes:

  • LoadUnloadInit should be a module initializer instead of a library initializer.
  • There is quite a lot of overhead when calling DeattachGuests (I suppose) as an on Death event that calls this function does that.

Status:

  • Awaiting Update
 
- I'll be updating LoadUnloadInit to use UnitEventsEx for the next version. I'll keep the GUI in a pastebin for those who use GUI Unit Event.
- I haven't read my own code in a while ;^^ Would you mind elaborating on the overhead for DettachGuests?

PS: I tried to write my own linked list to maybe get rid of the cascading list (and I think no longer having to store slot location?):
JASS:
// LINKED LIST
private struct uNode
  
    uNode next
    uNode prev
    readonly unit data
  
    method destroy takes nothing returns nothing
        set this.next = 0
        set this.prev = 0
        set this.data = null
        call this.deallocate()
    endmethod
  
    static method create takes unit u returns thistype
        local thistype this = allocate()
        set this.next = 0
        set this.prev = 0
        set this.data = u
        return this
    endmethod
  
endstruct

private struct uList
  
    readonly uNode first
    readonly uNode last
    readonly integer count
  
    method destroy takes nothing returns nothing
        local uNode node = .first
        local uNode nodeNext
        loop
            exitwhen node == 0
            set nodeNext = node.next
            call node.destroy()
        endloop
        set .first = 0
        set .last = 0
        set .count = 0
        call this.deallocate()
    endmethod
  
    // returns the size of a list
    method size takes nothing returns integer
        return this.count
    endmethod
  
    // inserts an element at the end of the list
    method push takes unit u returns nothing
        local uNode node = allocate()
      
        if .count == 0 then
            set .first = node
        else
            set .last.next = node
        endif
      
        set node.prev = .last   // .last is already set to 0 a list creation
        set node.next = 0       // otherwise the list will cycle forever
        set .last = node
      
        set .count = .count - 1
    endmethod
  
    // removes a node from the list
    method erase takes uNode node returns nothing
      
        if node == .first then
            set node.prev.next = 0
            set node.next.prev = 0
            set .first = node.next
        elseif node == .last then
            set node.prev.next = 0
            set node.next.prev = 0
            set .last = node.prev
        else
            set node.prev.next = node.next  // set the next of the previous to...
            set node.next.prev = node.prev  // set the previous of the next to...
        endif
      
        call node.destroy()
        set .count = .count + 1
    endmethod
  
    method find takes unit u returns uNode
        local uNode node = .first
        loop
            exitwhen node == 0 or node.data == u
            set node = node.next
        endloop
        return node
    endmethod
  
    static method create takes nothing returns thistype
        local thistype this =  allocate()
        set this.first = 0
        set this.last = 0
        set this.count = 0
        return this
    endmethod

endstruct

PS2: how would you write a GetRandomNode function?
 
It comes from testing the map, and I experienced some sort of lag while that function was being called.

In a linked list, I think it would be very costly space-wise to implement an O(1) getRandomNode. Likewise, it would be operation-costly to implement an O(n) getRandomNode, but it is one of the simplest ones.

Simply put, just iterate from the start of the list up until you've done it enough or you've gone beyond.

Also, in your uNode, the destructor call makes it so that you'll instantly disconnect your list. What you'll have to do is this: ( thanks to PurgeandFire for teaching me this)

JASS:
method destroy takes nothing returns nothing
    set this.prev.next = this.next
    set this.next.prev = this.prev

What it does is let the next member of the previous node point to the next node and vice versa, or like this.

1<-2->3
1->2>
1->3
<2<-3
1<-3

1->3
 
Hmm, interesting. So essentially, I can reconnect the elements of the list directly in the destroy method rather than do it in List.erase? Because that's what (I hope) I did in that method:

JASS:
    // removes a node from the list
    method erase takes uNode node returns nothing
       
        if node == .first then
            set node.prev.next = 0
            set node.next.prev = 0
            set .first = node.next
        elseif node == .last then
            set node.prev.next = 0
            set node.next.prev = 0
            set .last = node.prev
        else
            set node.prev.next = node.next  // set the next of the previous to...
            set node.next.prev = node.prev  // set the previous of the next to...
        endif
       
        call node.destroy()
        set .count = .count + 1
    endmethod

About the getRandom, how does this sound:
JASS:
    method getRandom takes nothing returns uNode
        local integer i
        local uNode node = 0
        if this.count > 0 then
            set i = GetRandomInt(1, .count)
            loop
                exitwhen i == 0
                set node = node.next
                set i = i - 1
            endloop
        endif
        return node
    endmethod
 
Level 22
Joined
Sep 24, 2005
Messages
4,821
Since you are using a doubly linked list, I figure you can access the last node (rootnode.prev)? If so, try setting up a conditional to branch from the first or last node.

Code:
if RandomInt(0,1)==0 cycle from first node to listsize/2
else cycle from listsize/2 to last nod

That would split in half the amount of iterations you would need to get a random int from a list sized X [O(n/2)].
 
That's pretty clever. Thanks, I'll be implementing that ^^

EDIT: I might have spoken too soon, lol. How would I cycle from listsize/2 to last node?
EDIT2: actually maybe cycle from last to listsize/2?
JASS:
    method getRandom takes nothing returns uNode
        local integer halfSize = .count / 2
        local integer i
        local uNode node = 0
       
        if .count > 0 then
            set i = GetRandomInt(1, halfSize)
            if GetRandomInt(0, 1) == 0 then
                set node = .first
                loop
                    exitwhen i == 0
                    set node = node.next
                    set i = i - 1
                endloop
            else
                set node = .last
                loop
                    exitwhen i == 0
                    set node = node.prev
                    set i = i - 1
                endloop
            endif
        endif
       
        return node
    endmethod
 
Last edited:
Level 22
Joined
Sep 24, 2005
Messages
4,821
EDIT2: actually maybe cycle from last to listsize/2?

Start at the last node and stop at listsize/2.

EDIT: Ah yeah, you did it already, I forgot it was a list and I didn't mention you had to iterate backwards (.prev iteration)

EDIT2: You gotta be careful when dealing with odd numbers as you would lose the ability to look at the midpoint node.

3/2 = 1 would only search the nodes {1 , 3}, you can use a modulo operation to fix this.

(3/2) + (3 mod 2) = 2
 
Last edited:
Huh, I see. That being said, wareditor actually gave me a bit of code for getRandom():
JASS:
    method getRandom takes nothing returns iNode
        local integer i = GetRandomInt(1, .count)
        local iNode node = 0
     
        if i > .count / 2 then
            set i = .count - i
            set node = .last
            loop
                exitwhen i == 0
                set node = node.prev
                set i = i - 1
            endloop
        else
            set i = i - 1
            set node = .first
            loop
                exitwhen i == 0
                set node = node.next
                set i = i - 1
            endloop
        endif
     
        return node
    endmethod
Thoughts?

__________________________________________

So. I might have gone slightly overboard and, erm... 'structified' whole thing. It seemed like an unnecessary conversion at the time but I couldn't help myself... I'm curious what you think about this @MyPad. I know you said you liked the simplicity and while I don't believe this breaks said simplicity, it now has struct allocations like Host/Guest instances and whatnot.

JASS:
library AttachUnit requires UnitEventsEx optional ListT

/*

    AttachUnit v1.02
 
    by
 
    Spellbound
 
 
    ________DESCRIPTION______________________________________________________________________________________________________________________________
 
    AttachUnit simply binds a unit to another, with respect to angle, distance and, optionally,
    facing angle of the host. Carrier units are called Hosts and attached units are called Guests.
 
    AttachUnit comes with a turret functionality that allows Guests to rotates and attack normally,
    after which they will return to a specific facing position. They can also be made to slowly
    rotate over time, which maybe be useful for sci-fi type attachements.
 
 
 
    ________REQUIREMENTS_____________________________________________________________________________________________________________________________
 
    Unit Event by Bribe - https://www.hiveworkshop.com/threads/gui-unit-event-v2-4-0-0.201641/#resource-45995
 
 
 
    ________INSTALLATION_____________________________________________________________________________________________________________________________
 
    Simply copy-paste this trigger into your map and install a unit indexer that uses custom values.
 
 
 
    ________API______________________________________________________________________________________________________________________________________
 
    /* IMPORTANT */ For units to function as Guests, they must have a positive, non-zero movement speed. Otherwise they will not move along with
                    their Host.
 
 
    call AttachUnitToHost takes unit guest, unit host, real angle, boolean staticAngle, real angleFacing, real distance, real zOffset, real offsetFix
        ^ This function will attach a unit to a host.
     
        [_ARGUMENTS_]
        unit guest              - the unit to attach (aka the Guest)
        unit host               - the Host
        real angle              - the angle relative to the Host's facing angle at which the Guest is offset to. In degrees.
        boolean staticAngle     - if true, the Guest's angle offset will ignore the Host's facing direction
        real angleFacing        - the starting facing angle of the Guest
        real distance           - the distance between the Host and the Guest
        real zOffset            - the height difference from the Host
        real offsetFix          - if your Guest is off-center, try setting this to -16 or 16.
     
     
    call SetGuestFacingProperties takes unit guest, real startAngle, real rate, real cooldown, boolean dynamicFacing, boolean turretMode
        ^ This sets an attached unit's facing properties. If this is not called, the unit will always face the same direction.
     
        [_ARGUMENTS_]
        unit guest              - the attached unit (aka Guest)
        real startAngle         - the angle at which the Guest begins at. If dynamicFacing (see below) is false, the guest will always face this specific angle.
        real rate               - the speed at which the Guest rotates over time. Set to zero to ignore.
        real cooldown           - this is neccessary if the Guest can attack. It will resume it facing parameters after that cooldown has expired.
        boolean dynamicFacing   - if this is true, the facing angle of the Guest will depend on the facing of the Host.
        boolean turretMode      - Set this to true if you want your Guest to be able to attack. Otherwise, the facing parameters will keep interferring.
 
 
    call DettachGuest takes unit guest, real time
        ^ Call this function when you want to Dettach a Guest from a Host. real time is for how long after Dettachement do you want the Guest to die.
        Set real time to zero if you don't want the Guest to die. This function is called automatically when a Guest dies.
     
     
    call DettachAllGuests takes unit host, real time
        ^ Call this function on a Host to Dettach all its Guests. This function will not get called automatically on Host death and must be done manually.
        Similarly to DettachGuest, real time will determine how long after Dettachment will the Guest die. Set to zero to not kill your Dettached Guests.
 
 
    call GetRandomGuest takes unit host, boolepx guestFilter returns unit
        ^ This will return a random Guest attached to a Host. Set guestFilter to null if you do not wish to filter your guests.
        /* IMPORTANT */ In your filter you MUST use the variable 'FilterGuest' and NOT GetFilterUnit().
     
     
    call GetNumberOfGuests takes unit host returns integer
        ^ This will return the number of Guests currently connected to a Host.
     
 
    call GroupGuests takes unit host returns group
        ^ this will take all Guests attached to a Host and put them in a unit group via:
        set YourGroup = GrouGuests(host)
 
 
    call IsUnitHost takes unit host returns boolean
        ^ Returns true if a unit is a Host.
 
 
    call IsUnitGuest takes unit guest returns boolean
        ^ Returns true if a unit is a Guest.
 
 
    call ShowGuest takes unit guest returns nothing
        ^ Unhides a hidden Guest without breaking locust.
 
 
    call ShowGuests takes unit host returns nothing
        ^ Cycles through all the Guests of a Host and calls ShowGuest() on them to unhide them. This function is called automatically when leaving a transport.
        If you wish for Guests to remain hidden when unloading from transport, call HideGuests() on your Host after it unloads. This will not break locust.
 
 
    call HideGuests takes unit host returns nothing
        ^ Cycles through all Guests a Host has and hides them. This function is called automatically when loading a Host in a transport, but not when hiding a unit by
        triggers. Call this function manually then and it's corresponding ShowGuests() when unhiding.
 
 
 
*/ 

globals
    private constant real TIMEOUT = .03125
    private constant integer ORDER_ATTACK = 851983
 
    unit FilterGuest = null
    private trigger FilterTrig = CreateTrigger()

    private hashtable TurretStorage = InitHashtable()
    private timer Clock = CreateTimer()
 
    private iList GlobalGuestList = 0
    private iList GlobalHostList = 0
    private iList array GuestList
 
    private Host array H_ID
    private Guest array G_ID
     
    private group GuestGroup = CreateGroup()
endglobals

//! runtextmacro optional DEFINE_LIST("", "AUList", "integer")
 
static if LIBRARY_ListT then

    globals
        private AUList GlobalGuestListT = 0
        private AUList GlobalHostListT = 0
        private AUList array GuestListT
    endglobals
 
    function ListTGetRandom takes AUList list returns AUListItem
        local integer size = list.size()
        local integer i = GetRandomInt(1, size)
        local AUListItem node = 0
     
        // thanks to Wareditor for that bit of code
        if i > size / 2 then
            set i = size - i
            set node = list.last
            loop
                exitwhen i == 0
                set node = node.prev
                set i = i - 1
            endloop
        else
            set i = i - 1
            set node = list.first
            loop
                exitwhen i == 0
                set node = node.next
                set i = i - 1
            endloop
        endif
     
        return node
    endfunction
 
endif
 
// LINKED LIST
private struct iNode
 
    iNode next
    iNode prev
    integer data
 
    method destroy takes nothing returns nothing
        set this.prev.next = this.next
        set this.next.prev = this.prev
        set this.next = 0
        set this.prev = 0
        set this.data = 0
        call this.deallocate()
    endmethod
 
    static method create takes integer i returns thistype
        local thistype this = allocate()
        set this.next = 0
        set this.prev = 0
        set this.data = i
        return this
    endmethod
 
endstruct

struct iList
 
    readonly iNode first
    readonly iNode last
    readonly integer count
 
    method destroy takes nothing returns nothing
        local iNode node = .first
        local iNode nodeNext
        loop
            exitwhen node == 0
            set nodeNext = node.next
            call node.destroy()
        endloop
        set .first = 0
        set .last = 0
        set .count = 0
        call this.deallocate()
    endmethod
 
    // returns the size of a list
    method size takes nothing returns integer
        return this.count
    endmethod
 
    // inserts an element at the end of the list
    method push takes integer i returns nothing
        local iNode node = allocate()
     
        if .count == 0 then
            set .first = node
        else
            set .last.next = node
        endif
     
        set node.prev = .last   // .last is already set to 0 at list creation
        set node.next = 0       // otherwise the list will cycle forever
        set .last = node
     
        set node.data = i
     
        set .count = .count + 1
    endmethod
 
    // removes a node from the list
    method erase takes iNode node returns nothing
     
        /*if node == .first then
            set node.next.prev = 0
            set .first = node.next
        elseif node == .last then
            set node.prev.next = 0
            set .last = node.prev
        else
            set node.prev.next = node.next  // set the next of the previous to...
            set node.next.prev = node.prev  // set the previous of the next to...
        endif*/
     
        call node.destroy()
        set .count = .count - 1
    endmethod
 
    method getRandom takes nothing returns iNode
        local integer i = GetRandomInt(1, .count)
        local iNode node = 0
     
        // thanks to Wareditor for that bit of code
        if i > .count / 2 then
            set i = .count - i
            set node = .last
            loop
                exitwhen i == 0
                set node = node.prev
                set i = i - 1
            endloop
        else
            set i = i - 1
            set node = .first
            loop
                exitwhen i == 0
                set node = node.next
                set i = i - 1
            endloop
        endif
     
        return node
    endmethod
 
    method find takes integer i returns iNode
        local iNode node = .first
        loop
            exitwhen node == 0 or node.data == i
            set node = node.next
        endloop
        return node
    endmethod
 
    static method create takes nothing returns thistype
        local thistype this =  allocate()
        set this.first = 0
        set this.last = 0
        set this.count = 0
        return this
    endmethod

endstruct

struct Guest
 
    real cooldown
    real cooldownReset
    trigger turretTrigger
    boolean dynamicFacing
    boolean updateFacing
 
    real distance
    real angle
    real zOffset
    real offsetFix
    real angleRate
    real facing
    real guestFacing
    boolean staticAngle
 
    unit parent
    unit u
 
    method destroy takes nothing returns nothing
        set this.u = null
        set this.parent = null
        set this.staticAngle = false
        set this.dynamicFacing = false
        set this.updateFacing = false
        call this.deallocate()
    endmethod
 
    static method create takes unit g, unit h returns thistype
        local thistype this = allocate()
        static if LIBRARY_ListT then
            call GlobalGuestListT.push(this)
        else
            call GlobalGuestList.push(this)
        endif
        set this.parent = h
        set this.u = g
        return this
    endmethod
 
endstruct

struct Host
 
    real x
    real y
    real z
    real f
    unit u
 
    static if LIBRARY_ListT then
        AUList guestList
    else
        iList guestList
    endif
 
    method destroy takes nothing returns nothing
        call this.guestList.destroy()
        static if LIBRARY_ListT then
            call GlobalHostListT.removeElem(this)
        else
            call GlobalHostList.erase(this)
        endif
        call this.deallocate()
    endmethod
 
    method addGuest takes Guest guest returns nothing
        call this.guestList.push(guest)
    endmethod
 
    static method create takes unit h returns thistype
        local thistype this = allocate()
        static if LIBRARY_ListT then
            if this.guestList == 0 then
                set this.guestList = AUList.create()
                call GlobalHostListT.push(this)
            endif
        else
            if this.guestList == 0 then
                set this.guestList = iList.create()
                call GlobalHostList.push(this)
            endif
        endif
        set this.u = h
        return this
    endmethod
 
endstruct

//REFERENCE AND OTHER USEFUL FUNCTIONS

function IsUnitHost takes unit h returns boolean
    return H_ID[GetUnitId(h)] != 0
endfunction

function IsUnitGuest takes unit g returns boolean
    return G_ID[GetUnitId(g)] != 0
endfunction

function GetNumberOfGuests takes unit h returns integer
    local Host host = H_ID[GetUnitId(h)]
    return host.guestList.size()
endfunction

function GroupGuests takes unit h returns group
    local Host host = H_ID[GetUnitId(h)]
    local Guest guest
 
    static if LIBRARY_ListT then
        local AUListItem node = host.guestList.first
    else
        local iNode node = host.guestList.first
    endif
 
    call GroupClear(GuestGroup)
 
    if IsUnitHost(h) then
        loop
            exitwhen node == 0
            set guest = node.data
            call GroupAddUnit(GuestGroup, guest.u)
            set node = node.next
        endloop
    endif
 
    return GuestGroup
endfunction

function GetRandomGuest takes unit h, boolexpr guestFilter returns unit
 
    local Host host = H_ID[GetUnitId(h)]
    local Guest guest
 
    static if LIBRARY_ListT then
        local AUListItem node = ListTGetRandom(host.guestList)
    else
        local iNode node = host.guestList.getRandom()
    endif
 
    local unit u
    local triggercondition tcnd
    local boolean firstPass = false
    local integer n = host.guestList.size()
     
    if guestFilter == null then
        set guest = node.data
        return guest.u
    else
        set firstPass = (node == host.guestList.first)
        set FilterGuest = null
     
        loop
            exitwhen node == 0 and firstPass
         
            if node == 0 and not firstPass then
                set node = host.guestList.first
                set firstPass = true
            endif
         
            set guest = node.data
            set u = guest.u
         
            set tcnd = TriggerAddCondition(FilterTrig, guestFilter)
            if TriggerEvaluate(FilterTrig) then
                set FilterGuest = u
            endif
            call TriggerRemoveCondition(FilterTrig, tcnd)
         
            set node = node.next
        endloop
     
        set u = null
        set tcnd = null
     
        return FilterGuest
    endif
    return null
endfunction

function ShowGuest takes unit g returns nothing
    if IsUnitHidden(g) then
        call ShowUnit(g, true)
        if GetUnitAbilityLevel(g, 'Aloc') > 0 then
            call UnitRemoveAbility(g, 'Aloc')
            call UnitAddAbility(g, 'Aloc')
        endif
    endif
endfunction

function ShowGuestsEx takes unit h, boolean flag returns nothing
 
    local Host host = H_ID[GetUnitId(h)]
    local Guest guest
 
    static if LIBRARY_ListT then
        local AUListItem node = host.guestList.first
        local AUListItem nodeNext
    else
        local iNode node = host.guestList.first
        local iNode nodeNext
    endif
 
    loop
        exitwhen node == 0
        set nodeNext = node.next
        set guest = node.data
        if flag then
            call ShowGuest(guest.u)
        else
            call ShowUnit(guest.u, false)
        endif
    endloop
 
endfunction

function ShowGuests takes unit h returns nothing
    call ShowGuestsEx(h, true)
endfunction

function HideGuests takes unit h returns nothing
    call ShowGuestsEx(h, false)
endfunction


//MAIN API FUNCITONS
//NB: Functions that start with 'private' cannot be called outside of this library

function DettachGuest takes unit g, real time returns nothing
    local Guest guest = G_ID[GetUnitId(g)]
    local Host host
 
    if guest.turretTrigger != null then
        call FlushChildHashtable(TurretStorage, GetHandleId(guest.turretTrigger))
        call DestroyTrigger(guest.turretTrigger)
        set guest.turretTrigger = null
    endif
 
    if guest.parent != null then
        set host = H_ID[GetUnitId(guest.parent)]
     
        static if LIBRARY_ListT then
            call host.guestList.removeElem(guest)
        else
            call host.guestList.erase(guest)
        endif
     
        if UnitAlive(g) and time > 0. then
            call UnitApplyTimedLife(g , 'BTLF', time)
        endif
     
        call SetUnitPropWindow(g, GetUnitDefaultPropWindow(g) * bj_DEGTORAD)
        call UnitRemoveAbility(g, 'Aeth')
        call SetUnitPathing(g, true)
     
        if host.guestList.size() == 0 then
            static if LIBRARY_ListT then
                call GlobalHostListT.removeElem(host)
            else
                call GlobalHostList.erase(host)
            endif
            call host.destroy()
        endif
    endif
 
    call guest.destroy()
 
endfunction


function DettachAllGuests takes unit h, real time returns nothing
    local Host host = H_ID[GetUnitId(h)]
    local Guest guest
 
    static if LIBRARY_ListT then
        local AUListItem node = host.guestList.first
        local AUListItem nodeNext
    else
        local iNode node = host.guestList.first
        local iNode nodeNext
    endif
     
    loop
        exitwhen node == 0
        set nodeNext = node.next
        set guest = node.data
        call DettachGuest(guest.u, time)
        set node = nodeNext
    endloop
 
endfunction


private function TurretActions takes nothing returns nothing
    local Guest guest = LoadInteger(TurretStorage, GetHandleId(GetTriggeringTrigger()), 0)
    set guest.cooldown = guest.cooldownReset
    if GetUnitCurrentOrder(guest.u) != ORDER_ATTACK then
        call IssuePointOrderById(guest.u, ORDER_ATTACK, GetUnitX(guest.u), GetUnitY(guest.u))
    endif
endfunction


private function UpdateGuests takes nothing returns nothing
 
    local real x
    local real y
    local real z
    local real f
    local real a
 
    static if LIBRARY_ListT then
        local AUListItem node = GlobalHostListT.first
        local AUListItem nodeNext
        local AUListItem nNode
        local AUListItem nNodeNext
    else
        local iNode node = GlobalHostList.first
        local iNode nodeNext
        local iNode nNode
        local iNode nNodeNext
    endif
 
    local Host host
    local Guest guest
 
    // This loop cycles through all the Hosts and a secondary loop inside cycles through their
    // attached Guests, updating their x, y and z coordiates.
    loop
 
        exitwhen node == 0
        set nodeNext = node.next
     
        set host = node.data
        set x = GetUnitX(host.u)
        set y = GetUnitY(host.u)
        set z = BlzGetLocalUnitZ(host.u)
        set f = GetUnitFacing(host.u)
     
        if not IsUnitHidden(host.u) then
         
            if x != host.x or y != host.y or z != host.z or f != host.f then
             
                set nNode = host.guestList.first
                loop
                    exitwhen nNode == 0
                    set nNodeNext = nNode.next
                 
                    set guest = nNode.data
                 
                    if guest.staticAngle then
                        set a = 0.
                    else
                        set a = f * bj_DEGTORAD
                    endif
                 
                    call SetUnitX(guest.u, guest.offsetFix + x + Cos(guest.angle + a) * guest.distance)
                    call SetUnitY(guest.u, guest.offsetFix + y + Sin(guest.angle + a) * guest.distance)
                    call SetUnitFlyHeight(guest.u, z + guest.zOffset, 0.)
                 
                    set nNode = nNodeNext
                endloop
             
                set host.x = x
                set host.y = y
                set host.z = z
                set host.f = f * bj_RADTODEG
         
            endif
         
        endif
     
        set node = nodeNext
     
    endloop
 
    // Updates the facing angle of Guests and when not to turn (eg when in combat). Guests that
    // have no idle rotation will not get added to this list.
    static if LIBRARY_ListT then
        set node = GlobalGuestListT.first
    else
        set node = GlobalGuestList.first
    endif
 
    loop
 
        exitwhen node == 0
        set nodeNext = node.next
     
        set guest = node.data
     
        if not UnitAlive(guest.u) then
            call DettachGuest(guest.u, 0.)
        else
         
            if guest.updateFacing then
             
                if guest.dynamicFacing then
                    set f = GetUnitFacing(guest.parent)
                else
                    set f = 0.
                endif
             
                if GetUnitCurrentOrder(guest.u) != 0 then
                    if guest.cooldown > 0. then
                        set guest.cooldown = guest.cooldown - TIMEOUT
                    else
                        set guest.cooldown = guest.cooldownReset
                        call IssueImmediateOrderById(guest.u, 851973) //order stunned
                    endif
                 
                    if guest.angleRate != 0. then
                        // Store the facing of the unit if it needs to rotate later
                        set guest.facing = GetUnitFacing(guest.u)
                    endif
             
                else
                    if guest.angleRate == 0. then
                        // If the unit doesn't rotate
                        call SetUnitFacing(guest.u, guest.facing + f)
                    else
                        set guest.facing = guest.facing + guest.angleRate
                        call SetUnitFacing(guest.u, guest.facing + f)
                    endif
                 
                endif
             
            endif
         
        endif
     
        set node = nodeNext
 
    endloop
endfunction


function SetGuestFacingProperties takes unit g, real startAngle, real rate, real cooldown, boolean dynamicFacing, boolean turretMode returns nothing
    local Guest guest = G_ID[GetUnitId(g)]
 
    set guest.cooldownReset = cooldown
    set guest.cooldown = cooldown
 
    if rate == 0. then
        set guest.angleRate = 0.
    else
        set guest.angleRate = rate * TIMEOUT
    endif
 
    set guest.dynamicFacing = dynamicFacing
 
    if dynamicFacing then
        set guest.guestFacing = startAngle - GetUnitFacing(guest.parent)
    else
        set guest.guestFacing = startAngle
    endif
    call SetUnitFacing(g, startAngle)
 
    set guest.updateFacing = true
 
    if turretMode then
        set guest.turretTrigger = CreateTrigger()
        call TriggerRegisterUnitEvent(guest.turretTrigger, g, EVENT_UNIT_ACQUIRED_TARGET)
        call TriggerRegisterUnitEvent(guest.turretTrigger, g, EVENT_UNIT_TARGET_IN_RANGE)
        call SaveInteger(TurretStorage, GetHandleId(guest.turretTrigger), 0, guest)
        call TriggerAddCondition(guest.turretTrigger, Condition(function TurretActions))
    endif
endfunction


function AttachUnitToHost takes unit g, unit h, real angle, boolean staticAngle, real angleFacing, real distance, real zOffset, real offsetFix returns nothing
    local integer idH = GetUnitId(h)
    local integer idG = GetUnitId(g)
 
    local Host host
    local Guest guest
 
    static if LIBRARY_ListT then
        local AUListItem node = GlobalHostListT.first
    else
        local iNode node = GlobalHostList.first
    endif
 
    local real x = GetUnitX(h)
    local real y = GetUnitY(h)
    local real z = BlzGetLocalUnitZ(h)
    local real f = GetUnitFacing(h)
    local real a
 
    // check if the Host has an instance
    if H_ID[idH] == 0 then
        set host = Host.create(h)
        set host.x = x
        set host.y = y
        set host.z = z
        set host.f = f
        set H_ID[idH] = host
    else
        set host = H_ID[idH]
    endif
 
    // if guest instance is not null then it's already attached to a unit
    if G_ID[idG] == 0 then
        set guest = Guest.create(g, h)
        set G_ID[idG] = guest
    else
        set guest = G_ID[idG]
        if guest.parent == h then
            return
        else
            call DettachGuest(g, 0.)
        endif
    endif
 
    call host.addGuest(guest)
 
    set guest.angle = angle * bj_DEGTORAD
    set guest.staticAngle = staticAngle
    set guest.facing = angleFacing
    set guest.distance = distance
    set guest.zOffset = zOffset
    set guest.offsetFix = offsetFix
    set guest.parent = h
 
    if staticAngle then
        set a = guest.angle
    else
        set a = (angle + f) * bj_DEGTORAD
    endif
 
    call SetUnitX(g, offsetFix + x + Cos(a) * distance)
    call SetUnitY(g, offsetFix + y + Sin(a) * distance)
    call SetUnitFlyHeight(g, z + zOffset, 0.)
    call SetUnitFacing(g, angleFacing + f)
             
    call SetUnitPropWindow(g, 0.)
    call UnitAddAbility(g, 'Aeth')
    call SetUnitPathing(g, false)
 
    static if LIBRARY_ListT then
        if GlobalHostListT.size() == 1 then
            call TimerStart(Clock, TIMEOUT, true, function UpdateGuests)
        endif
    else
        if GlobalHostList.size() == 1 then
            call TimerStart(Clock, TIMEOUT, true, function UpdateGuests)
        endif
    endif
 
endfunction

//This function will hide/unhide guests if the host is loaded into a transport. Uses custom
//load/unload events from Bribe's Unit Event system. Links in the Requirement section above.
/*private function LoadUnload takes nothing returns boolean
    if udg_CargoEvent == 1. then
        if IsUnitHost(udg_UDexUnits[udg_UDex]) then
            call HideGuests(udg_UDexUnits[udg_UDex])
        endif
    else
        if IsUnitHost(udg_UDexUnits[udg_UDex]) then
            call ShowGuests(udg_UDexUnits[udg_UDex])
        endif
    endif
    return false
endfunction*/

private function onLoad takes nothing returns nothing
    local unit u = GetEventUnit()
    if IsUnitHost(u) then
        call HideGuests(u)
    endif
    set u = null
endfunction

private function onUnload takes nothing returns nothing
    local unit u = GetEventUnit()
    if IsUnitHost(u) then
        call ShowGuests(u)
    endif
    set u = null
endfunction

// Initialisation
private module init
    private static method onInit takes nothing returns nothing
        call RegisterNativeEvent(EVENT_ON_CARGO_LOAD, function onLoad)
        call RegisterNativeEvent(EVENT_ON_CARGO_UNLOAD, function onUnload)
     
        static if LIBRARY_ListT then
            set GlobalGuestListT = AUList.create()
            set GlobalHostListT = AUList.create()
        else
            set GlobalGuestList = iList.create()
            set GlobalHostList = iList.create()
        endif
     
        /*call TriggerRegisterVariableEvent(LoadUnloadTrig, "udg_CargoEvent", EQUAL, 1.00)
        call TriggerRegisterVariableEvent(LoadUnloadTrig, "udg_CargoEvent", EQUAL, 2.00)
        call TriggerAddCondition(LoadUnloadTrig, Condition(function LoadUnload))*/
    endmethod
endmodule

private struct Init
    implement init
endstruct

endlibrary

PS: this bit is giving me an error if I disable ListT. Says AUList is undefined, even though it's inside of a static if. Is there a way to circumvent that?
JASS:
//! runtextmacro optional DEFINE_LIST("", "AUList", "integer")
 
static if LIBRARY_ListT then

    globals
        private AUList GlobalGuestListT = 0
        private AUList GlobalHostListT = 0
        private AUList array GuestListT
    endglobals
 
Last edited:
Level 22
Joined
Sep 24, 2005
Messages
4,821
I'm bad at probabilities but I think that works. I think it's even better than what I thought of (no modulus required).

EDIT: Wait, I think I might be wrong about the no modulus required, seems like the middle nodes are still going to be ignored with that.
 
Last edited:
I guess it is satisfactory, since I like the struct format more than arrays (I know it's technically still arrays but it looks nicer). Also, I think I don't need to have cargo events at all since I already check if the unit is hidden. If so I can simply hide the guests once the host is hidden. Will test.

@chobibo I'm told no modulo is necessary and that the getRandom() will work. I guess we'll find out on review! :D
 
Level 22
Joined
Sep 24, 2005
Messages
4,821
EDITED out wrong comment.

Ah! Yes, he is correct, use his method.

Code:
list size 5

rand 5
5 - 5 {} return 5
rand 4
5 - 4 {5} return 4
rand 3
5 - 3 {5,4} return 3
rand 2
2 - 1 {1} return 2
rand 1
1 - 1 {} return 1
 
Last edited:
JASS:
library AttachUnit requires UnitEventsEx optional ListT
/*

    AttachUnit v1.02
 
    by
 
    Spellbound
 
 
    ________DESCRIPTION______________________________________________________________________________________________________________________________
 
    AttachUnit simply binds a unit to another, with respect to angle, distance and, optionally,
    facing angle of the host. Carrier units are called Hosts and attached units are called Guests.
 
    AttachUnit comes with a turret functionality that allows Guests to rotates and attack normally,
    after which they will return to a specific facing position. They can also be made to slowly
    rotate over time, which maybe be useful for sci-fi type attachements.
 
 
 
    ________REQUIREMENTS_____________________________________________________________________________________________________________________________
 
    Unit Event by Bribe - https://www.hiveworkshop.com/threads/gui-unit-event-v2-4-0-0.201641/#resource-45995
 
 
 
    ________INSTALLATION_____________________________________________________________________________________________________________________________
 
    Simply copy-paste this trigger into your map and install a unit indexer that uses custom values.
 
 
 
    ________API______________________________________________________________________________________________________________________________________
 
    /* IMPORTANT */ For units to function as Guests, they must have a positive, non-zero movement speed. Otherwise they will not move along with
                    their Host.
 
 
    call AttachUnitToHost takes unit guest, unit host, real angle, boolean staticAngle, real angleFacing, real distance, real zOffset, real offsetFix
        ^ This function will attach a unit to a host.
    
        [_ARGUMENTS_]
        unit guest              - the unit to attach (aka the Guest)
        unit host               - the Host
        real angle              - the angle relative to the Host's facing angle at which the Guest is offset to. In degrees.
        boolean staticAngle     - if true, the Guest's angle offset will ignore the Host's facing direction
        real angleFacing        - the starting facing angle of the Guest
        real distance           - the distance between the Host and the Guest
        real zOffset            - the height difference from the Host
        real offsetFix          - if your Guest is off-center, try setting this to -16 or 16.
    
    
    call SetGuestFacingProperties takes unit guest, real startAngle, real rate, real cooldown, boolean dynamicFacing, boolean turretMode
        ^ This sets an attached unit's facing properties. If this is not called, the unit will always face the same direction.
    
        [_ARGUMENTS_]
        unit guest              - the attached unit (aka Guest)
        real startAngle         - the angle at which the Guest begins at. If dynamicFacing (see below) is false, the guest will always face this specific angle.
        real rate               - the speed at which the Guest rotates over time. Set to zero to ignore.
        real cooldown           - this is neccessary if the Guest can attack. It will resume it facing parameters after that cooldown has expired.
        boolean dynamicFacing   - if this is true, the facing angle of the Guest will depend on the facing of the Host.
        boolean turretMode      - Set this to true if you want your Guest to be able to attack. Otherwise, the facing parameters will keep interferring.
 
 
    call DettachGuest takes unit guest, real time
        ^ Call this function when you want to Dettach a Guest from a Host. real time is for how long after Dettachement do you want the Guest to die.
        Set real time to zero if you don't want the Guest to die. This function is called automatically when a Guest dies.
    
    
    call DettachAllGuests takes unit host, real time
        ^ Call this function on a Host to Dettach all its Guests. This function will not get called automatically on Host death and must be done manually.
        Similarly to DettachGuest, real time will determine how long after Dettachment will the Guest die. Set to zero to not kill your Dettached Guests.
 
 
    call GetRandomGuest takes unit host, boolepx guestFilter returns unit
        ^ This will return a random Guest attached to a Host. Set guestFilter to null if you do not wish to filter your guests.
        /* IMPORTANT */ In your filter you MUST use the variable 'FilterGuest' and NOT GetFilterUnit().
    
    
    call GetNumberOfGuests takes unit host returns integer
        ^ This will return the number of Guests currently connected to a Host.
    
 
    call GroupGuests takes unit host returns group
        ^ this will take all Guests attached to a Host and put them in a unit group via:
        set YourGroup = GrouGuests(host)
 
 
    call IsUnitHost takes unit host returns boolean
        ^ Returns true if a unit is a Host.
 
 
    call IsUnitGuest takes unit guest returns boolean
        ^ Returns true if a unit is a Guest.
 
 
    call ShowGuest takes unit guest returns nothing
        ^ Unhides a hidden Guest without breaking locust.
 
 
    call ShowGuests takes unit host returns nothing
        ^ Cycles through all the Guests of a Host and calls ShowGuest() on them to unhide them. This function is called automatically when leaving a transport.
        If you wish for Guests to remain hidden when unloading from transport, call HideGuests() on your Host after it unloads. This will not break locust.
 
 
    call HideGuests takes unit host returns nothing
        ^ Cycles through all Guests a Host has and hides them. This function is called automatically when loading a Host in a transport, but not when hiding a unit by
        triggers. Call this function manually then and it's corresponding ShowGuests() when unhiding.
 
 
 
*/

globals
    private constant real TIMEOUT = .03125
    private constant integer ORDER_ATTACK = 851983
 
    unit FilterGuest = null
    private trigger FilterTrig = CreateTrigger()

    private hashtable TurretStorage = InitHashtable()
    private timer Clock = CreateTimer()
 
    private iList GlobalGuestList = 0
    private iList GlobalHostList = 0
    private iList array GuestList
 
    private Host array H_ID
    private Guest array G_ID
    
    private group GuestGroup = CreateGroup()
endglobals

//! runtextmacro optional DEFINE_LIST("", "AUList", "integer")
 
static if LIBRARY_ListT then

    globals
        private AUList GlobalGuestListT = 0
        private AUList GlobalHostListT = 0
        private AUList array GuestListT
    endglobals
 
    function ListTGetRandom takes AUList list returns AUListItem
        local integer size = list.size()
        local integer i = GetRandomInt(1, size)
        local AUListItem node = 0
    
        // thanks to Wareditor for that bit of code
        if i > size / 2 then
            set i = size - i
            set node = list.last
            loop
                exitwhen i == 0
                set node = node.prev
                set i = i - 1
            endloop
        else
            set i = i - 1
            set node = list.first
            loop
                exitwhen i == 0
                set node = node.next
                set i = i - 1
            endloop
        endif
    
        return node
    endfunction
 
endif
 
// LINKED LIST
private struct iNode
 
    iNode next
    iNode prev
    integer data
 
    method destroy takes nothing returns nothing
        set this.prev.next = this.next
        set this.next.prev = this.prev
        set this.next = 0
        set this.prev = 0
        set this.data = 0
        call this.deallocate()
    endmethod
 
    static method create takes integer i returns thistype
        local thistype this = allocate()
        set this.next = 0
        set this.prev = 0
        set this.data = i
        return this
    endmethod
 
endstruct

struct iList
 
    readonly iNode first
    readonly iNode last
    readonly integer count
 
    method destroy takes nothing returns nothing
        local iNode node = .first
        local iNode nodeNext
        loop
            exitwhen node == 0
            set nodeNext = node.next
            call node.destroy()
        endloop
        set .first = 0
        set .last = 0
        set .count = 0
        call this.deallocate()
    endmethod
 
    // returns the size of a list
    method size takes nothing returns integer
        return this.count
    endmethod
 
    // inserts an element at the end of the list
    method push takes integer i returns nothing
        local iNode node = allocate()
    
        if .count == 0 then
            set .first = node
        else
            set .last.next = node
        endif
    
        set node.prev = .last   // .last is already set to 0 at list creation
        set node.next = 0      // otherwise the list will cycle forever
        set .last = node
    
        set node.data = i
    
        set .count = .count + 1
    endmethod
 
    // removes a node from the list
    method erase takes iNode node returns nothing
    
        /*if node == .first then
            set node.next.prev = 0
            set .first = node.next
        elseif node == .last then
            set node.prev.next = 0
            set .last = node.prev
        else
            set node.prev.next = node.next  // set the next of the previous to...
            set node.next.prev = node.prev  // set the previous of the next to...
        endif*/
    
        call node.destroy()
        set .count = .count - 1
    endmethod
 
    method getRandom takes nothing returns iNode
        local integer i = GetRandomInt(1, .count)
        local iNode node = 0
    
        // thanks to Wareditor for that bit of code
        if i > .count / 2 then
            set i = .count - i
            set node = .last
            loop
                exitwhen i == 0
                set node = node.prev
                set i = i - 1
            endloop
        else
            set i = i - 1
            set node = .first
            loop
                exitwhen i == 0
                set node = node.next
                set i = i - 1
            endloop
        endif
    
        return node
    endmethod
 
    method find takes integer i returns iNode
        local iNode node = .first
        loop
            exitwhen node == 0 or node.data == i
            set node = node.next
        endloop
        return node
    endmethod
 
    static method create takes nothing returns thistype
        local thistype this =  allocate()
        set this.first = 0
        set this.last = 0
        set this.count = 0
        return this
    endmethod

endstruct

struct Guest
 
    real cooldown
    real cooldownReset
    trigger turretTrigger
    boolean dynamicFacing
    boolean updateFacing
 
    real distance
    real angle
    real zOffset
    real offsetFix
    real angleRate
    real facing
    real guestFacing
    boolean staticAngle
 
    unit parent
    unit u
 
    method destroy takes nothing returns nothing
        set this.u = null
        set this.parent = null
        set this.staticAngle = false
        set this.dynamicFacing = false
        set this.updateFacing = false
        call this.deallocate()
    endmethod
 
    static method create takes unit g, unit h returns thistype
        local thistype this = allocate()
        static if LIBRARY_ListT then
            call GlobalGuestListT.push(this)
        else
            call GlobalGuestList.push(this)
        endif
        set this.parent = h
        set this.u = g
        return this
    endmethod
 
endstruct

struct Host
 
    real x
    real y
    real z
    real f
    unit u
 
    static if LIBRARY_ListT then
        AUList guestList
    else
        iList guestList
    endif
 
    method destroy takes nothing returns nothing
        call this.guestList.destroy()
        static if LIBRARY_ListT then
            call GlobalHostListT.removeElem(this)
        else
            call GlobalHostList.erase(this)
        endif
        call this.deallocate()
    endmethod
 
    method addGuest takes Guest guest returns nothing
        call this.guestList.push(guest)
    endmethod
 
    static method create takes unit h returns thistype
        local thistype this = allocate()
        static if LIBRARY_ListT then
            if this.guestList == 0 then
                set this.guestList = AUList.create()
                call GlobalHostListT.push(this)
            endif
        else
            if this.guestList == 0 then
                set this.guestList = iList.create()
                call GlobalHostList.push(this)
            endif
        endif
        set this.u = h

        return this
    endmethod
 
endstruct

//REFERENCE AND OTHER USEFUL FUNCTIONS

function IsUnitHost takes unit h returns boolean
    return H_ID[GetUnitId(h)] != 0
endfunction

function IsUnitGuest takes unit g returns boolean
    return G_ID[GetUnitId(g)] != 0
endfunction

function GetNumberOfGuests takes unit h returns integer
    local Host host = H_ID[GetUnitId(h)]
    return host.guestList.size()
endfunction

function GroupGuests takes unit h returns group
    local Host host = H_ID[GetUnitId(h)]
    local Guest guest
 
    static if LIBRARY_ListT then
        local AUListItem node = host.guestList.first
    else
        local iNode node = host.guestList.first
    endif
 
    call GroupClear(GuestGroup)
 
    if IsUnitHost(h) then
        loop
            exitwhen node == 0
            set guest = node.data
            call GroupAddUnit(GuestGroup, guest.u)
            set node = node.next
        endloop
    endif
 
    return GuestGroup
endfunction

function GetRandomGuest takes unit h, boolexpr guestFilter returns unit
 
    local Host host = H_ID[GetUnitId(h)]
    local Guest guest
 
    static if LIBRARY_ListT then
        local AUListItem node = ListTGetRandom(host.guestList)
    else
        local iNode node = host.guestList.getRandom()
    endif
 
    local unit u
    local triggercondition tcnd
    local boolean firstPass = false
    local integer n = host.guestList.size()
    
    if guestFilter == null then
        set guest = node.data
        return guest.u
    else
        set firstPass = (node == host.guestList.first)
        set FilterGuest = null
    
        loop
            exitwhen node == 0 and firstPass
        
            if node == 0 and not firstPass then
                set node = host.guestList.first
                set firstPass = true
            endif
        
            set guest = node.data
            set u = guest.u
        
            set tcnd = TriggerAddCondition(FilterTrig, guestFilter)
            if TriggerEvaluate(FilterTrig) then
                set FilterGuest = u
            endif
            call TriggerRemoveCondition(FilterTrig, tcnd)
        
            set node = node.next
        endloop
    
        set u = null
        set tcnd = null
    
        return FilterGuest
    endif
    return null
endfunction

function ShowGuest takes unit g returns nothing
    if IsUnitHidden(g) then
        call ShowUnit(g, true)
        if GetUnitAbilityLevel(g, 'Aloc') > 0 then
            call UnitRemoveAbility(g, 'Aloc')
            call UnitAddAbility(g, 'Aloc')
        endif
    endif
endfunction

function ShowGuestsEx takes unit h, boolean flag returns nothing
 
    local Host host = H_ID[GetUnitId(h)]
    local Guest guest
 
    static if LIBRARY_ListT then
        local AUListItem node = host.guestList.first
        local AUListItem nodeNext
    else
        local iNode node = host.guestList.first
        local iNode nodeNext
    endif
 
    loop
        exitwhen node == 0
        set nodeNext = node.next
        set guest = node.data
        if flag then
            call ShowGuest(guest.u)
        else
            call ShowUnit(guest.u, false)
        endif
    endloop
 
endfunction

function ShowGuests takes unit h returns nothing
    call ShowGuestsEx(h, true)
endfunction

function HideGuests takes unit h returns nothing
    call ShowGuestsEx(h, false)
endfunction


//MAIN API FUNCITONS
//NB: Functions that start with 'private' cannot be called outside of this library

function DettachGuest takes unit g, real time returns nothing
    local Guest guest = G_ID[GetUnitId(g)]
    local Host host
 
    if guest.turretTrigger != null then
        call FlushChildHashtable(TurretStorage, GetHandleId(guest.turretTrigger))
        call DestroyTrigger(guest.turretTrigger)
        set guest.turretTrigger = null
    endif
 
    if guest.parent != null then
        set host = H_ID[GetUnitId(guest.parent)]
    
        static if LIBRARY_ListT then
            call host.guestList.removeElem(guest)
        else
            call host.guestList.erase(guest)
        endif
    
        if UnitAlive(g) and time > 0. then
            call UnitApplyTimedLife(g , 'BTLF', time)
        endif
    
        call SetUnitPropWindow(g, GetUnitDefaultPropWindow(g) * bj_DEGTORAD)
        call UnitRemoveAbility(g, 'Aeth')
        call SetUnitPathing(g, true)
    
        if host.guestList.size() == 0 then
            static if LIBRARY_ListT then
                call GlobalHostListT.removeElem(host)
            else
                call GlobalHostList.erase(host)
            endif
            call host.destroy()
        endif
    endif
 
    call guest.destroy()
 
endfunction


function DettachAllGuests takes unit h, real time returns nothing
    local Host host = H_ID[GetUnitId(h)]
    local Guest guest
 
    static if LIBRARY_ListT then
        local AUListItem node = host.guestList.first
        local AUListItem nodeNext
    else
        local iNode node = host.guestList.first
        local iNode nodeNext
    endif
    
    loop
        exitwhen node == 0
        set nodeNext = node.next
        set guest = node.data
        call DettachGuest(guest.u, time)
        set node = nodeNext
    endloop
 
endfunction


private function TurretActions takes nothing returns nothing
    local Guest guest = LoadInteger(TurretStorage, GetHandleId(GetTriggeringTrigger()), 0)
    set guest.cooldown = guest.cooldownReset
    if GetUnitCurrentOrder(guest.u) != ORDER_ATTACK then
        call IssuePointOrderById(guest.u, ORDER_ATTACK, GetUnitX(guest.u), GetUnitY(guest.u))
    endif
endfunction


private function UpdateGuests takes nothing returns nothing
 
    local real x
    local real y
    local real z
    local real f
    local real a
 
    static if LIBRARY_ListT then
        local AUListItem node = GlobalHostListT.first
        local AUListItem nodeNext
        local AUListItem nNode
        local AUListItem nNodeNext
    else
        local iNode node = GlobalHostList.first
        local iNode nodeNext
        local iNode nNode
        local iNode nNodeNext
    endif
 
    local Host host
    local Guest guest
 
    // This loop cycles through all the Hosts and a secondary loop inside cycles through their
    // attached Guests, updating their x, y and z coordiates.
    loop
 
        exitwhen node == 0
        set nodeNext = node.next
    
        set host = node.data
        set x = GetUnitX(host.u)
        set y = GetUnitY(host.u)
        set z = BlzGetLocalUnitZ(host.u)
        set f = GetUnitFacing(host.u)
    
        if not IsUnitHidden(host.u) then
        
            if x != host.x or y != host.y or z != host.z or f != host.f then
            
                set nNode = host.guestList.first
                loop
                    exitwhen nNode == 0
                    set nNodeNext = nNode.next
                
                    set guest = nNode.data
                
                    if guest.staticAngle then
                        set a = 0.
                    else
                        set a = f * bj_DEGTORAD
                    endif
                
                    call SetUnitX(guest.u, guest.offsetFix + x + Cos(guest.angle + a) * guest.distance)
                    call SetUnitY(guest.u, guest.offsetFix + y + Sin(guest.angle + a) * guest.distance)
                    call SetUnitFlyHeight(guest.u, z + guest.zOffset, 0.)
                
                    set nNode = nNodeNext
                endloop
            
                set host.x = x
                set host.y = y
                set host.z = z
                set host.f = f * bj_RADTODEG
        
            endif
        
        endif
    
        set node = nodeNext
    
    endloop
 
    // Updates the facing angle of Guests and when not to turn (eg when in combat). Guests that
    // have no idle rotation will not get added to this list.
    static if LIBRARY_ListT then
        set node = GlobalGuestListT.first
    else
        set node = GlobalGuestList.first
    endif
 
    loop
 
        exitwhen node == 0
        set nodeNext = node.next
    
        set guest = node.data
    
        if not UnitAlive(guest.u) then
            call DettachGuest(guest.u, 0.)
        else
        
            if guest.updateFacing then
            
                if guest.dynamicFacing then
                    set f = GetUnitFacing(guest.parent)
                else
                    set f = 0.
                endif
            
                if GetUnitCurrentOrder(guest.u) != 0 then
                    if guest.cooldown > 0. then
                        set guest.cooldown = guest.cooldown - TIMEOUT
                    else
                        set guest.cooldown = guest.cooldownReset
                        call IssueImmediateOrderById(guest.u, 851973) //order stunned
                    endif
                
                    if guest.angleRate != 0. then
                        // Store the facing of the unit if it needs to rotate later
                        set guest.facing = GetUnitFacing(guest.u)
                    endif
            
                else
                    if guest.angleRate == 0. then
                        // If the unit doesn't rotate
                        call SetUnitFacing(guest.u, guest.facing + f)
                    else
                        set guest.facing = guest.facing + guest.angleRate
                        call SetUnitFacing(guest.u, guest.facing + f)
                    endif
                
                endif
            
            endif
        
        endif
    
        set node = nodeNext
 
    endloop
endfunction


function SetGuestFacingProperties takes unit g, real startAngle, real rate, real cooldown, boolean dynamicFacing, boolean turretMode returns nothing
    local Guest guest = G_ID[GetUnitId(g)]
 
    set guest.cooldownReset = cooldown
    set guest.cooldown = cooldown
 
    if rate == 0. then
        set guest.angleRate = 0.
    else
        set guest.angleRate = rate * TIMEOUT
    endif
 
    set guest.dynamicFacing = dynamicFacing
 
    if dynamicFacing then
        set guest.guestFacing = startAngle - GetUnitFacing(guest.parent)
    else
        set guest.guestFacing = startAngle
    endif
    call SetUnitFacing(g, startAngle)
 
    set guest.updateFacing = true
 
    if turretMode then
        set guest.turretTrigger = CreateTrigger()
        call TriggerRegisterUnitEvent(guest.turretTrigger, g, EVENT_UNIT_ACQUIRED_TARGET)
        call TriggerRegisterUnitEvent(guest.turretTrigger, g, EVENT_UNIT_TARGET_IN_RANGE)
        call SaveInteger(TurretStorage, GetHandleId(guest.turretTrigger), 0, guest)
        call TriggerAddCondition(guest.turretTrigger, Condition(function TurretActions))
    endif
endfunction


function AttachUnitToHost takes unit g, unit h, real angle, boolean staticAngle, real angleFacing, real distance, real zOffset, real offsetFix returns nothing
    local integer idH = GetUnitId(h)
    local integer idG = GetUnitId(g)
 
    local Host host
    local Guest guest
 
    static if LIBRARY_ListT then
        local AUListItem node = GlobalHostListT.first
    else
        local iNode node = GlobalHostList.first
    endif
 
    local real x = GetUnitX(h)
    local real y = GetUnitY(h)
    local real z = BlzGetLocalUnitZ(h)
    local real f = GetUnitFacing(h)
    local real a
 
    // check if the Host has an instance
    if H_ID[idH] == 0 then
        set host = Host.create(h)
        set host.x = x
        set host.y = y
        set host.z = z
        set host.f = f
        set H_ID[idH] = host
    else
        set host = H_ID[idH]
    endif
 
    // if guest instance is not null then it's already attached to a unit
    if G_ID[idG] == 0 then
        set guest = Guest.create(g, h)
        set G_ID[idG] = guest
    else
        set guest = G_ID[idG]
        if guest.parent == h then
            return
        else
            call DettachGuest(g, 0.)
        endif
    endif
 
    call host.addGuest(guest)
 
    set guest.angle = angle * bj_DEGTORAD
    set guest.staticAngle = staticAngle
    set guest.facing = angleFacing
    set guest.distance = distance
    set guest.zOffset = zOffset
    set guest.offsetFix = offsetFix
    set guest.parent = h
 
    if staticAngle then
        set a = guest.angle
    else
        set a = (angle + f) * bj_DEGTORAD
    endif
 
    call SetUnitX(g, offsetFix + x + Cos(a) * distance)
    call SetUnitY(g, offsetFix + y + Sin(a) * distance)
    call SetUnitFlyHeight(g, z + zOffset, 0.)
    call SetUnitFacing(g, angleFacing + f)
            
    call SetUnitPropWindow(g, 0.)
    call UnitAddAbility(g, 'Aeth')
    call SetUnitPathing(g, false)
 
    static if LIBRARY_ListT then
        if GlobalHostListT.size() == 1 then
            call TimerStart(Clock, TIMEOUT, true, function UpdateGuests)
        endif
    else
        if GlobalHostList.size() == 1 then
            call TimerStart(Clock, TIMEOUT, true, function UpdateGuests)
        endif
    endif
 
endfunction

//This function will hide/unhide guests if the host is loaded into a transport. Uses custom
//load/unload events from Bribe's Unit Event system. Links in the Requirement section above.
/*private function LoadUnload takes nothing returns boolean
    if udg_CargoEvent == 1. then
        if IsUnitHost(udg_UDexUnits[udg_UDex]) then
            call HideGuests(udg_UDexUnits[udg_UDex])
        endif
    else
        if IsUnitHost(udg_UDexUnits[udg_UDex]) then
            call ShowGuests(udg_UDexUnits[udg_UDex])
        endif
    endif
    return false
endfunction*/

private function onLoad takes nothing returns nothing
    local unit u = GetEventUnit()
    if IsUnitHost(u) then
        call HideGuests(u)
    endif
    set u = null
endfunction

private function onUnload takes nothing returns nothing
    local unit u = GetEventUnit()
    if IsUnitHost(u) then
        call ShowGuests(u)
    endif
    set u = null
endfunction

// Initialisation
private module init
    private static method onInit takes nothing returns nothing
        call RegisterNativeEvent(EVENT_ON_CARGO_LOAD, function onLoad)
        call RegisterNativeEvent(EVENT_ON_CARGO_UNLOAD, function onUnload)
    
        static if LIBRARY_ListT then
            set GlobalGuestListT = AUList.create()
            set GlobalHostListT = AUList.create()
        else
            set GlobalGuestList = iList.create()
            set GlobalHostList = iList.create()
        endif
    
        /*call TriggerRegisterVariableEvent(LoadUnloadTrig, "udg_CargoEvent", EQUAL, 1.00)
        call TriggerRegisterVariableEvent(LoadUnloadTrig, "udg_CargoEvent", EQUAL, 2.00)
        call TriggerAddCondition(LoadUnloadTrig, Condition(function LoadUnload))*/
    endmethod
endmodule

private struct Init
    implement init
endstruct

endlibrary

The structs iList, and iNode exist even when AUList exists. Also, the allocate call in method push of iList potentially points to the allocator of iList instead of iNode. (Struct names follow the ProperNoun convention (capitalize first letter of every word), members and methods follow the camelCase)

Also. the function SetGuestFacingProperties has no safety mechanism for checking if a unit isn't a guest. Assume that the unit is not a guest, and refer to it when calling said function. The function will proceed.

In function GroupGuests, it returns a static group (the same group). Some clarification on that matter would be advised (desired), since some users might destroy the group and wonder why GroupGuests or any other function using that static group do not work.
 
I'm in the process of updating AttachUnit to also allow the user to attach special effects. The thing is, I'll be renaming the library to AttachObject and I'm wondering if I shouldn't just make a new post for that and deprecate AttachUnit? The other reason why is because while I wanted to retain backwards compatibility the order in which AttachUnit's function parameters are call is confusing (and there's one needless parameter in AttachUnitToHost) and I wanted to re-order them, which I did in AttachObject. Any thoughts on that?
 
Updated:
- v2.00 Library renamed to AttachObject and now supports attaching special effects. Unit Event was also removed as a dependency as it was unnecessary. Now uses a linked list for better performance when removing attached Guests. Not backward compatible.

EDIT: There's been an update for RiseAndFall, so I'll have to update AttachObject to fix this new change. If you need to use AttachObject for the time being, just disable RiseAndFall.
 
Last edited:
Okay, so I'm having an issue with encapsulation. I want to add another Getter function that will allow you to basically copy a guest list if you wish to iterate through it, but the issue I'm having now is that to work with the Guest struct will mean all its members will be accessible. If I set them to readonly or private then I can't modify them outside the struct, which I would much rather do that having to make a function call every .03125 seconds for the struct to internally update coordinates and stuff like that.

GetGuestList(whichHost) will essentially replicate the guest list of a host, granting you access to members like .u, .parent and .fx. Unfortunately, it also grants you access to .distance, .angle, .zOffset, etc. The only solution I can think of is to have a separate struct that will only have those three members and when you call GetGuestList(whichHost), it allocates and instance and copies stuff over. Another idea is to use private global arrays for the private stuff but that just sounds inelegant.

Thoughts?
 
I thought of two approaches for that case.

First one:

JASS:
private struct MainStructEx
    member variables...    // that are private in MainStruct
endstruct

struct MainStruct
    member variables...

    // private member variables.... in MainStructEx
    readonly MainStructEx extension

    static method create()
        ...
        set this.extension = this
endstruct

Second approach:

JASS:
struct StructName
    member variables...
   
    static method methodName()
        ... // Do update

function TheFunctionThatStartsATimer()
    TimerStart(theTimerThatWillRun, constantInterval, ofCourseItsTrue, function StructName.methodName)

The first one would make more sense in terms of encapsulation, but will cost some readability points.
The second one is more straightforward, but it risks external usage. (Documentation can prevent this most of the time)
 
So if I'm understanding this correctly, for the first example, everytime you update a MainStructEx member, you also update it's extended version? Then when a user needs to read one of the .extension, they'll have a limited number to read from. Hmmmm.

I went ahead and tried the Struct copy thing I was talking about and... I'm not sure how to feel about it:

GetGuestList
JASS:
static if LIBRARY_ListT then
function GetGuestList takes unit h returns IntegerList
else
function GetGuestList takes unit h returns LinkedList
endif
    local Host host = GetHostUnitId(h)
 
    static if LIBRARY_ListT then
        local IntegerList l = IntegerList.create()
        local IntegerListItem node = host.guestList.first
    else
        local LinkedList l = LinkedList.create()
        local ListItem node = host.guestList.first
    endif
 
    loop
        exitwhen node == 0
        call l.push(GuestListItem.create(node.data))
        set node = node.next
    endloop
    return l
endfunction

GuestListItem
JASS:
struct GuestListItem
    unit host
    unit guest
    effect fx
 
    private method destroy takes nothing returns nothing
        set this.host = null
        set this.guest = null
        set this.fx = null
        call this.deallocate()
    endmethod
 
    static if LIBRARY_ListT then
    static method finish takes IntegerList list returns nothing
    else
    static method finish takes LinkedList list returns nothing
    endif
        local thistype gl
    
        static if LIBRARY_ListT then
            local IntegerList l = IntegerList.create()
            local IntegerListItem node = list.first
        else
            local LinkedList l = LinkedList.create()
            local ListItem node = list.first
        endif
    
        loop
            exitwhen node == 0
            set gl = node.data
            call gl.destroy()
            set node = node.next
        endloop
    
        call list.destroy()
    
    endmethod
 
    static method create takes Guest guest returns thistype
        local thistype this = allocate()
        set this.guest = guest.u
        set this.host = guest.parent
        set this.fx = guest.fx
        return this
    endmethod
 
endstruct

How to Use the above (comment are a tutorial for people new to vJASS)
JASS:
scope ListDemo
 
    // This demo will explain how to use the GetGuestList() function
    
    function UsingListsExample takes unit yourHost returns nothing
    
        // a static if will write the code only if the library in question is present.
        // If you have ListT, you will need the following locals
        static if LIBRARY_ListT then
    
        local IntegerList list = GetGuestList(yourHost)
        local IntegerListItem node = list.first
        local IntegerListItem nodeNext
    
        // If you do NOT have ListT, you will have to use AttachObject's internal linked list as locals
        else
    
        local LinkedList list = GetGuestList(yourHost)
        local ListItem node = list.first
        local ListItem nodeNext
    
        endif
    
        local GuestListItem guest
    
        loop
            exitwhen node == 0
            set nodeNext = node.next
            set guest = node.data
        
            // guest.host is your host
            // guest.guest is your guest
            // guest.fx if your special effect
        
            set node = nodeNext
        endloop
    
        // calling GuestListItem.finish(list) is ESSENTIAL. Call it at the end of your function,
        // OUTSIDE THE LOOP, and it deallocate all GuestListItem instances in the list as well as
        // destroy the temporary list.
        call GuestListItem.finish(list)
    
    endfunction
 
endscope

If you think I should switch to your first example, I'll do so on Saturday or Sunday (leaving for a few days)
 
Top