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