----- **Wrapper** - GROUP wraps the DCS Class Group objects. -- -- === -- -- The @{#GROUP} class is a wrapper class to handle the DCS Group objects. -- -- ## Features: -- -- * Support all DCS Group APIs. -- * Enhance with Group specific APIs not in the DCS Group API set. -- * Handle local Group Controller. -- * Manage the "state" of the DCS Group. -- -- **IMPORTANT: ONE SHOULD NEVER SANITIZE these GROUP OBJECT REFERENCES! (make the GROUP object references nil).** -- -- === -- -- For each DCS Group object alive within a running mission, a GROUP wrapper object (instance) will be created within the global _DATABASE object (an instance of @{Core.Database#DATABASE}). -- This is done at the beginning of the mission (when the mission starts), and dynamically when new DCS Group objects are spawned (using the @{Core.Spawn} class). -- -- The GROUP class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference -- using the DCS Group or the DCS GroupName. -- -- The GROUP methods will reference the DCS Group object by name when it is needed during API execution. -- If the DCS Group object does not exist or is nil, the GROUP methods will return nil and may log an exception in the DCS.log file. -- -- === -- -- ### Author: **FlightControl** -- -- ### Contributions: -- -- * [**Entropy**](https://forums.eagle.ru/member.php?u=111471), **Afinegan**: Came up with the requirement for AIOnOff(). -- -- === -- -- @module Wrapper.Group -- @image Wrapper_Group.JPG --- @type GROUP -- @extends Wrapper.Controllable#CONTROLLABLE -- @field #string GroupName The name of the group. --- Wrapper class of the DCS world Group object. -- -- The GROUP class provides the following functions to retrieve quickly the relevant GROUP instance: -- -- * @{#GROUP.Find}(): Find a GROUP instance from the global _DATABASE object (an instance of @{Core.Database#DATABASE}) using a DCS Group object. -- * @{#GROUP.FindByName}(): Find a GROUP instance from the global _DATABASE object (an instance of @{Core.Database#DATABASE}) using a DCS Group name. -- -- # 1. Tasking of groups -- -- A GROUP is derived from the wrapper class CONTROLLABLE (@{Wrapper.Controllable#CONTROLLABLE}). -- See the @{Wrapper.Controllable} task methods section for a description of the task methods. -- -- But here is an example how a group can be assigned a task. -- -- This test demonstrates the use(s) of the SwitchWayPoint method of the GROUP class. -- -- First we look up the objects. We create a GROUP object `HeliGroup`, using the @{#GROUP:FindByName}() method, looking up the `"Helicopter"` group object. -- Same for the `"AttackGroup"`. -- -- local HeliGroup = GROUP:FindByName( "Helicopter" ) -- local AttackGroup = GROUP:FindByName( "AttackGroup" ) -- -- Now we retrieve the @{Wrapper.Unit#UNIT} objects of the `AttackGroup` object, using the method `:GetUnits()`. -- -- local AttackUnits = AttackGroup:GetUnits() -- -- Tasks are actually text strings that we build using methods of GROUP. -- So first, we declare an list of `Tasks`. -- -- local Tasks = {} -- -- Now we loop over the `AttackUnits` using a for loop. -- We retrieve the `AttackUnit` using the `AttackGroup:GetUnit()` method. -- Each `AttackUnit` found, will be attacked by `HeliGroup`, using the method `HeliGroup:TaskAttackUnit()`. -- This method returns a string containing a command line to execute the task to the `HeliGroup`. -- The code will assign the task string command to the next element in the `Task` list, using `Tasks[#Tasks+1]`. -- This little code will take the count of `Task` using `#` operator, and will add `1` to the count. -- This result will be the index of the `Task` element. -- -- for i = 1, #AttackUnits do -- local AttackUnit = AttackGroup:GetUnit( i ) -- Tasks[#Tasks+1] = HeliGroup:TaskAttackUnit( AttackUnit ) -- end -- -- Once these tasks have been executed, a function `_Resume` will be called ... -- -- Tasks[#Tasks+1] = HeliGroup:TaskFunction( "_Resume", { "''" } ) -- -- --- @param Wrapper.Group#GROUP HeliGroup -- function _Resume( HeliGroup ) -- env.info( '_Resume' ) -- -- HeliGroup:MessageToAll( "Resuming",10,"Info") -- end -- -- Now here is where the task gets assigned! -- Using `HeliGroup:PushTask`, the task is pushed onto the task queue of the group `HeliGroup`. -- Since `Tasks` is an array of tasks, we use the `HeliGroup:TaskCombo` method to execute the tasks. -- The `HeliGroup:PushTask` method can receive a delay parameter in seconds. -- In the example, `30` is given as a delay. -- -- -- HeliGroup:PushTask( -- HeliGroup:TaskCombo( -- Tasks -- ), 30 -- ) -- -- That's it! -- But again, please refer to the @{Wrapper.Controllable} task methods section for a description of the different task methods that are available. -- -- -- -- ### Obtain the mission from group templates -- -- Group templates contain complete mission descriptions. Sometimes you want to copy a complete mission from a group and assign it to another: -- -- * @{Wrapper.Controllable#CONTROLLABLE.TaskMission}: (AIR + GROUND) Return a mission task from a mission template. -- -- ## GROUP Command methods -- -- A GROUP is a @{Wrapper.Controllable}. See the @{Wrapper.Controllable} command methods section for a description of the command methods. -- -- ## GROUP option methods -- -- A GROUP is a @{Wrapper.Controllable}. See the @{Wrapper.Controllable} option methods section for a description of the option methods. -- -- ## GROUP Zone validation methods -- -- The group can be validated whether it is completely, partly or not within a @{Core.Zone}. -- Use the following Zone validation methods on the group: -- -- * @{#GROUP.IsCompletelyInZone}: Returns true if all units of the group are within a @{Core.Zone}. -- * @{#GROUP.IsPartlyInZone}: Returns true if some units of the group are within a @{Core.Zone}. -- * @{#GROUP.IsNotInZone}: Returns true if none of the group units of the group are within a @{Core.Zone}. -- -- The zone can be of any @{Core.Zone} class derived from @{Core.Zone#ZONE_BASE}. So, these methods are polymorphic to the zones tested on. -- -- ## GROUP AI methods -- -- A GROUP has AI methods to control the AI activation. -- -- * @{#GROUP.SetAIOnOff}(): Turns the GROUP AI On or Off. -- * @{#GROUP.SetAIOn}(): Turns the GROUP AI On. -- * @{#GROUP.SetAIOff}(): Turns the GROUP AI Off. -- -- @field #GROUP GROUP GROUP = { ClassName = "GROUP", } --- Enumerator for location at airbases -- @type GROUP.Takeoff GROUP.Takeoff = { Air = 1, Runway = 2, Hot = 3, Cold = 4, } GROUPTEMPLATE = {} GROUPTEMPLATE.Takeoff = { [GROUP.Takeoff.Air] = { "Turning Point", "Turning Point" }, [GROUP.Takeoff.Runway] = { "TakeOff", "From Runway" }, [GROUP.Takeoff.Hot] = { "TakeOffParkingHot", "From Parking Area Hot" }, [GROUP.Takeoff.Cold] = { "TakeOffParking", "From Parking Area" } } --- Generalized group attributes. See [DCS attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes) on hoggit. -- @type GROUP.Attribute -- @field #string AIR_TRANSPORTPLANE Airplane with transport capability. This can be used to transport other assets. -- @field #string AIR_AWACS Airborne Early Warning and Control System. -- @field #string AIR_FIGHTER Fighter, interceptor, ... airplane. -- @field #string AIR_BOMBER Aircraft which can be used for strategic bombing. -- @field #string AIR_TANKER Airplane which can refuel other aircraft. -- @field #string AIR_TRANSPORTHELO Helicopter with transport capability. This can be used to transport other assets. -- @field #string AIR_ATTACKHELO Attack helicopter. -- @field #string AIR_UAV Unpiloted Aerial Vehicle, e.g. drones. -- @field #string AIR_OTHER Any airborne unit that does not fall into any other airborne category. -- @field #string GROUND_APC Infantry carriers, in particular Amoured Personell Carrier. This can be used to transport other assets. -- @field #string GROUND_TRUCK Unarmed ground vehicles, which has the DCS "Truck" attribute. -- @field #string GROUND_INFANTRY Ground infantry assets. -- @field #string GROUND_IFV Ground Infantry Fighting Vehicle. -- @field #string GROUND_ARTILLERY Artillery assets. -- @field #string GROUND_TANK Tanks (modern or old). -- @field #string GROUND_TRAIN Trains. Not that trains are **not** yet properly implemented in DCS and cannot be used currently. -- @field #string GROUND_EWR Early Warning Radar. -- @field #string GROUND_AAA Anti-Aircraft Artillery. -- @field #string GROUND_SAM Surface-to-Air Missile system or components. -- @field #string GROUND_OTHER Any ground unit that does not fall into any other ground category. -- @field #string NAVAL_AIRCRAFTCARRIER Aircraft carrier. -- @field #string NAVAL_WARSHIP War ship, i.e. cruisers, destroyers, firgates and corvettes. -- @field #string NAVAL_ARMEDSHIP Any armed ship that is not an aircraft carrier, a cruiser, destroyer, firgatte or corvette. -- @field #string NAVAL_UNARMEDSHIP Any unarmed naval vessel. -- @field #string NAVAL_OTHER Any naval unit that does not fall into any other naval category. -- @field #string OTHER_UNKNOWN Anything that does not fall into any other category. GROUP.Attribute = { AIR_TRANSPORTPLANE="Air_TransportPlane", AIR_AWACS="Air_AWACS", AIR_FIGHTER="Air_Fighter", AIR_BOMBER="Air_Bomber", AIR_TANKER="Air_Tanker", AIR_TRANSPORTHELO="Air_TransportHelo", AIR_ATTACKHELO="Air_AttackHelo", AIR_UAV="Air_UAV", AIR_OTHER="Air_OtherAir", GROUND_APC="Ground_APC", GROUND_TRUCK="Ground_Truck", GROUND_INFANTRY="Ground_Infantry", GROUND_IFV="Ground_IFV", GROUND_ARTILLERY="Ground_Artillery", GROUND_TANK="Ground_Tank", GROUND_TRAIN="Ground_Train", GROUND_EWR="Ground_EWR", GROUND_AAA="Ground_AAA", GROUND_SAM="Ground_SAM", GROUND_OTHER="Ground_OtherGround", NAVAL_AIRCRAFTCARRIER="Naval_AircraftCarrier", NAVAL_WARSHIP="Naval_WarShip", NAVAL_ARMEDSHIP="Naval_ArmedShip", NAVAL_UNARMEDSHIP="Naval_UnarmedShip", NAVAL_OTHER="Naval_OtherNaval", OTHER_UNKNOWN="Other_Unknown", } --- Create a new GROUP from a given GroupTemplate as a parameter. -- Note that the GroupTemplate is NOT spawned into the mission. -- It is merely added to the @{Core.Database}. -- @param #GROUP self -- @param #table GroupTemplate The GroupTemplate Structure exactly as defined within the mission editor. -- @param DCS#coalition.side CoalitionSide The coalition.side of the group. -- @param DCS#Group.Category CategoryID The Group.Category of the group. -- @param DCS#country.id CountryID the country.id of the group. -- @return #GROUP self function GROUP:NewTemplate( GroupTemplate, CoalitionSide, CategoryID, CountryID ) local GroupName = GroupTemplate.name _DATABASE:_RegisterGroupTemplate( GroupTemplate, CoalitionSide, CategoryID, CountryID, GroupName ) local self = BASE:Inherit( self, CONTROLLABLE:New( GroupName ) ) self.GroupName = GroupName if not _DATABASE.GROUPS[GroupName] then _DATABASE.GROUPS[GroupName] = self end self:SetEventPriority( 4 ) return self end --- Create a new GROUP from an existing Group in the Mission. -- @param #GROUP self -- @param #string GroupName The Group name -- @return #GROUP self function GROUP:Register( GroupName ) local self = BASE:Inherit( self, CONTROLLABLE:New( GroupName ) ) -- #GROUP self.GroupName = GroupName self:SetEventPriority( 4 ) return self end -- Reference methods. --- Find the GROUP wrapper class instance using the DCS Group. -- @param #GROUP self -- @param DCS#Group DCSGroup The DCS Group. -- @return #GROUP The GROUP. function GROUP:Find( DCSGroup ) local GroupName = DCSGroup:getName() -- Wrapper.Group#GROUP local GroupFound = _DATABASE:FindGroup( GroupName ) return GroupFound end --- Find the created GROUP using the DCS Group Name. -- @param #GROUP self -- @param #string GroupName The DCS Group Name. -- @return #GROUP The GROUP. function GROUP:FindByName( GroupName ) local GroupFound = _DATABASE:FindGroup( GroupName ) return GroupFound end -- DCS Group methods support. --- Returns the DCS Group. -- @param #GROUP self -- @return DCS#Group The DCS Group. function GROUP:GetDCSObject() local DCSGroup = Group.getByName( self.GroupName ) if DCSGroup then return DCSGroup end return nil end --- Returns the @{DCS#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission. -- @param Wrapper.Positionable#POSITIONABLE self -- @return DCS#Position The 3D position vectors of the POSITIONABLE or #nil if the groups not existing or alive. function GROUP:GetPositionVec3() -- Overridden from POSITIONABLE:GetPositionVec3() self:F2( self.PositionableName ) local DCSPositionable = self:GetDCSObject() if DCSPositionable then local unit = DCSPositionable:getUnits()[1] if unit then local PositionablePosition = unit:getPosition().p self:T3( PositionablePosition ) return PositionablePosition end end return nil end --- Returns if the group is alive. -- The Group must: -- -- * Exist at run-time. -- * Has at least one unit. -- -- When the first @{Wrapper.Unit} of the group is active, it will return true. -- If the first @{Wrapper.Unit} of the group is inactive, it will return false. -- -- @param #GROUP self -- @return #boolean `true` if the group is alive *and* active, `false` if the group is alive but inactive or `#nil` if the group does not exist anymore. function GROUP:IsAlive() self:F2( self.GroupName ) local DCSGroup = self:GetDCSObject() -- DCS#Group if DCSGroup then if DCSGroup:isExist() then local DCSUnit = DCSGroup:getUnit(1) -- DCS#Unit if DCSUnit then local GroupIsAlive = DCSUnit:isActive() self:T3( GroupIsAlive ) return GroupIsAlive end end end return nil end --- Returns if the group is activated. -- @param #GROUP self -- @return #boolean `true` if group is activated or `#nil` The group is not existing or alive. function GROUP:IsActive() self:F2( self.GroupName ) local DCSGroup = self:GetDCSObject() -- DCS#Group if DCSGroup then local unit = DCSGroup:getUnit(1) if unit then local GroupIsActive = unit:isActive() return GroupIsActive end end return nil end --- Destroys the DCS Group and all of its DCS Units. -- Note that this destroy method also can raise a destroy event at run-time. -- So all event listeners will catch the destroy event of this group for each unit in the group. -- To raise these events, provide the `GenerateEvent` parameter. -- @param #GROUP self -- @param #boolean GenerateEvent If true, a crash [AIR] or dead [GROUND] event for each unit is generated. If false, if no event is triggered. If nil, a RemoveUnit event is triggered. -- @param #number delay Delay in seconds before despawning the group. -- @usage -- -- Air unit example: destroy the Helicopter and generate a S_EVENT_CRASH for each unit in the Helicopter group. -- Helicopter = GROUP:FindByName( "Helicopter" ) -- Helicopter:Destroy( true ) -- @usage -- -- Ground unit example: destroy the Tanks and generate a S_EVENT_DEAD for each unit in the Tanks group. -- Tanks = GROUP:FindByName( "Tanks" ) -- Tanks:Destroy( true ) -- @usage -- -- Ship unit example: destroy the Ship silently. -- Ship = GROUP:FindByName( "Ship" ) -- Ship:Destroy() -- -- @usage -- -- Destroy without event generation example. -- Ship = GROUP:FindByName( "Boat" ) -- Ship:Destroy( false ) -- Don't generate an event upon destruction. -- function GROUP:Destroy( GenerateEvent, delay ) self:F2( self.GroupName ) if delay and delay>0 then self:ScheduleOnce(delay, GROUP.Destroy, self, GenerateEvent) else local DCSGroup = self:GetDCSObject() if DCSGroup then for Index, UnitData in pairs( DCSGroup:getUnits() ) do if GenerateEvent and GenerateEvent == true then if self:IsAir() then self:CreateEventCrash( timer.getTime(), UnitData ) else self:CreateEventDead( timer.getTime(), UnitData ) end elseif GenerateEvent == false then -- Do nothing! else self:CreateEventRemoveUnit( timer.getTime(), UnitData ) end end USERFLAG:New( self:GetName() ):Set( 100 ) DCSGroup:destroy() DCSGroup = nil end end return nil end --- Returns category of the DCS Group. Returns one of -- -- * Group.Category.AIRPLANE -- * Group.Category.HELICOPTER -- * Group.Category.GROUND -- * Group.Category.SHIP -- * Group.Category.TRAIN -- -- @param #GROUP self -- @return DCS#Group.Category The category ID. function GROUP:GetCategory() self:F2( self.GroupName ) local DCSGroup = self:GetDCSObject() if DCSGroup then local GroupCategory = DCSGroup:getCategory() self:T3( GroupCategory ) return GroupCategory end return nil end --- Returns the category name of the #GROUP. -- @param #GROUP self -- @return #string Category name = Helicopter, Airplane, Ground Unit, Ship, Train. function GROUP:GetCategoryName() self:F2( self.GroupName ) local DCSGroup = self:GetDCSObject() if DCSGroup then local CategoryNames = { [Group.Category.AIRPLANE] = "Airplane", [Group.Category.HELICOPTER] = "Helicopter", [Group.Category.GROUND] = "Ground Unit", [Group.Category.SHIP] = "Ship", [Group.Category.TRAIN] = "Train", } local GroupCategory = DCSGroup:getCategory() self:T3( GroupCategory ) return CategoryNames[GroupCategory] end return nil end --- Returns the coalition of the DCS Group. -- @param #GROUP self -- @return DCS#coalition.side The coalition side of the DCS Group. function GROUP:GetCoalition() self:F2( self.GroupName ) local DCSGroup = self:GetDCSObject() if DCSGroup then local GroupCoalition = DCSGroup:getCoalition() self:T3( GroupCoalition ) return GroupCoalition end return nil end --- Returns the country of the DCS Group. -- @param #GROUP self -- @return DCS#country.id The country identifier or nil if the DCS Group is not existing or alive. function GROUP:GetCountry() self:F2( self.GroupName ) local DCSGroup = self:GetDCSObject() if DCSGroup then local GroupCountry = DCSGroup:getUnit(1):getCountry() self:T3( GroupCountry ) return GroupCountry end return nil end --- Check if at least one (or all) unit(s) has (have) a certain attribute. -- See [hoggit documentation](https://wiki.hoggitworld.com/view/DCS_func_hasAttribute). -- @param #GROUP self -- @param #string attribute The name of the attribute the group is supposed to have. Valid attributes can be found in the "db_attributes.lua" file which is located at in "C:\Program Files\Eagle Dynamics\DCS World\Scripts\Database". -- @param #boolean all If true, all units of the group must have the attribute in order to return true. Default is only one unit of a heterogenious group needs to have the attribute. -- @return #boolean Group has this attribute. function GROUP:HasAttribute(attribute, all) -- Get all units of the group. local _units=self:GetUnits() if _units then local _allhave=true local _onehas=false for _,_unit in pairs(_units) do local _unit=_unit --Wrapper.Unit#UNIT if _unit then local _hastit=_unit:HasAttribute(attribute) if _hastit==true then _onehas=true else _allhave=false end end end if all==true then return _allhave else return _onehas end end return nil end --- Returns the maximum speed of the group. -- If the group is heterogenious and consists of different units, the max speed of the slowest unit is returned. -- @param #GROUP self -- @return #number Speed in km/h. function GROUP:GetSpeedMax() self:F2( self.GroupName ) local DCSGroup = self:GetDCSObject() if DCSGroup then local Units=self:GetUnits() local speedmax=nil for _,unit in pairs(Units) do local unit=unit --Wrapper.Unit#UNIT local speed=unit:GetSpeedMax() if speedmax==nil or speed The list of player occupied @{Wrapper.Unit} objects of the @{Wrapper.Group}. function GROUP:GetPlayerUnits() self:F2( { self.GroupName } ) local DCSGroup = self:GetDCSObject() if DCSGroup then local DCSUnits = DCSGroup:getUnits() local Units = {} for Index, UnitData in pairs( DCSUnits ) do local PlayerUnit = UNIT:Find( UnitData ) if PlayerUnit:GetPlayerName() then Units[#Units+1] = PlayerUnit end end self:T3( Units ) return Units end return nil end --- Check if an (air) group is a client or player slot. Information is retrieved from the group template. -- @param #GROUP self -- @return #boolean If true, group is associated with a client or player slot. function GROUP:IsPlayer() return self:GetUnit(1):IsPlayer() end --- Returns the UNIT wrapper object with number UnitNumber. If it doesn't exist, tries to return the next available unit. -- If no underlying DCS Units exist, the method will return nil. -- @param #GROUP self -- @param #number UnitNumber The number of the UNIT wrapper class to be returned. -- @return Wrapper.Unit#UNIT The UNIT object or nil function GROUP:GetUnit( UnitNumber ) local DCSGroup = self:GetDCSObject() if DCSGroup then local UnitFound = nil -- 2.7.1 dead event bug, return the first alive unit instead -- Maybe fixed with 2.8? local units = DCSGroup:getUnits() or {} if units[UnitNumber] then local UnitFound = UNIT:Find(units[UnitNumber]) if UnitFound then return UnitFound end else for _,_unit in pairs(units) do local UnitFound = UNIT:Find(_unit) if UnitFound then return UnitFound end end end end return nil end --- Returns the DCS Unit with number UnitNumber. -- If the underlying DCS Unit does not exist, the method will return try to find the next unit. Returns nil if no units are found. -- @param #GROUP self -- @param #number UnitNumber The number of the DCS Unit to be returned. -- @return DCS#Unit The DCS Unit. function GROUP:GetDCSUnit( UnitNumber ) local DCSGroup = self:GetDCSObject() if DCSGroup then if DCSGroup.getUnit and DCSGroup:getUnit( UnitNumber ) then return DCSGroup:getUnit( UnitNumber ) else -- 2.7.1 dead event bug, return the first alive unit instead local units = DCSGroup:getUnits() or {} for _,_unit in pairs(units) do if _unit and _unit:isExist() then return _unit end end end end return nil end --- Returns current size of the DCS Group. -- If some of the DCS Units of the DCS Group are destroyed the size of the DCS Group is changed. -- @param #GROUP self -- @return #number The DCS Group size. function GROUP:GetSize() local DCSGroup = self:GetDCSObject() if DCSGroup then local GroupSize = DCSGroup:getSize() if GroupSize then return GroupSize else return 0 end end return nil end --- Count number of alive units in the group. -- @param #GROUP self -- @return #number Number of alive units. If DCS group is nil, 0 is returned. function GROUP:CountAliveUnits() self:F3( { self.GroupName } ) local DCSGroup = self:GetDCSObject() if DCSGroup then local units=self:GetUnits() local n=0 for _,_unit in pairs(units) do local unit=_unit --Wrapper.Unit#UNIT if unit and unit:IsAlive() then n=n+1 end end return n end return 0 end --- Get the first unit of the group which is alive. -- @param #GROUP self -- @return Wrapper.Unit#UNIT First unit alive. function GROUP:GetFirstUnitAlive() self:F3({self.GroupName}) local DCSGroup = self:GetDCSObject() if DCSGroup then local units=self:GetUnits() for _,_unit in pairs(units) do local unit=_unit --Wrapper.Unit#UNIT if unit and unit:IsAlive() then return unit end end end return nil end --- Get the first unit of the group. Might be nil! -- @param #GROUP self -- @return Wrapper.Unit#UNIT First unit or nil if it does not exist. function GROUP:GetFirstUnit() self:F3({self.GroupName}) local DCSGroup = self:GetDCSObject() if DCSGroup then local units=self:GetUnits() return units[1] end return nil end --- Returns the average velocity Vec3 vector. -- @param Wrapper.Group#GROUP self -- @return DCS#Vec3 The velocity Vec3 vector or `#nil` if the GROUP is not existing or alive. function GROUP:GetVelocityVec3() self:F2( self.GroupName ) local DCSGroup = self:GetDCSObject() if DCSGroup and DCSGroup:isExist() then local GroupUnits = DCSGroup:getUnits() local GroupCount = #GroupUnits local VelocityVec3 = { x = 0, y = 0, z = 0 } for _, DCSUnit in pairs( GroupUnits ) do local UnitVelocityVec3 = DCSUnit:getVelocity() VelocityVec3.x = VelocityVec3.x + UnitVelocityVec3.x VelocityVec3.y = VelocityVec3.y + UnitVelocityVec3.y VelocityVec3.z = VelocityVec3.z + UnitVelocityVec3.z end VelocityVec3.x = VelocityVec3.x / GroupCount VelocityVec3.y = VelocityVec3.y / GroupCount VelocityVec3.z = VelocityVec3.z / GroupCount return VelocityVec3 end BASE:E( { "Cannot GetVelocityVec3", Group = self, Alive = self:IsAlive() } ) return nil end --- Returns the average group altitude in meters. -- @param Wrapper.Group#GROUP self -- @param #boolean FromGround Measure from the ground or from sea level (ASL). Provide **true** for measuring from the ground (AGL). **false** or **nil** if you measure from sea level. -- @return #number The altitude of the group or nil if is not existing or alive. function GROUP:GetAltitude(FromGround) self:F2( self.GroupName ) return self:GetHeight(FromGround) end --- Returns the average group height in meters. -- @param Wrapper.Group#GROUP self -- @param #boolean FromGround Measure from the ground or from sea level (ASL). Provide **true** for measuring from the ground (AGL). **false** or **nil** if you measure from sea level. -- @return #number The height of the group or nil if is not existing or alive. function GROUP:GetHeight( FromGround ) self:F2( self.GroupName ) local DCSGroup = self:GetDCSObject() if DCSGroup then local GroupUnits = DCSGroup:getUnits() local GroupCount = #GroupUnits local GroupHeight = 0 for _, DCSUnit in pairs( GroupUnits ) do local GroupPosition = DCSUnit:getPosition() if FromGround == true then local LandHeight = land.getHeight( { x = GroupPosition.p.x, y = GroupPosition.p.z } ) GroupHeight = GroupHeight + ( GroupPosition.p.y - LandHeight ) else GroupHeight = GroupHeight + GroupPosition.p.y end end return GroupHeight / GroupCount end return nil end --- --- Returns the initial size of the DCS Group. -- If some of the DCS Units of the DCS Group are destroyed, the initial size of the DCS Group is unchanged. -- @param #GROUP self -- @return #number The DCS Group initial size. function GROUP:GetInitialSize() self:F3( { self.GroupName } ) local DCSGroup = self:GetDCSObject() if DCSGroup then local GroupInitialSize = DCSGroup:getInitialSize() self:T3( GroupInitialSize ) return GroupInitialSize end return nil end --- Returns the DCS Units of the DCS Group. -- @param #GROUP self -- @return #table The DCS Units. function GROUP:GetDCSUnits() self:F2( { self.GroupName } ) local DCSGroup = self:GetDCSObject() if DCSGroup then local DCSUnits = DCSGroup:getUnits() self:T3( DCSUnits ) return DCSUnits end return nil end --- Activates a late activated GROUP. -- @param #GROUP self -- @param #number delay Delay in seconds, before the group is activated. -- @return #GROUP self function GROUP:Activate(delay) self:F2( { self.GroupName } ) if delay and delay>0 then self:ScheduleOnce(delay, GROUP.Activate, self) else trigger.action.activateGroup( self:GetDCSObject() ) end return self end --- Gets the type name of the group. -- @param #GROUP self -- @return #string The type name of the group. function GROUP:GetTypeName() self:F2( self.GroupName ) local DCSGroup = self:GetDCSObject() if DCSGroup then local GroupTypeName = DCSGroup:getUnit(1):getTypeName() self:T3( GroupTypeName ) return( GroupTypeName ) end return nil end --- [AIRPLANE] Get the NATO reporting name (platform, e.g. "Flanker") of a GROUP (note - first unit the group). "Bogey" if not found. Currently airplanes only! --@param #GROUP self --@return #string NatoReportingName or "Bogey" if unknown. function GROUP:GetNatoReportingName() self:F2( self.GroupName ) local DCSGroup = self:GetDCSObject() if DCSGroup then local GroupTypeName = DCSGroup:getUnit(1):getTypeName() self:T3( GroupTypeName ) return UTILS.GetReportingName(GroupTypeName) end return "Bogey" end --- Gets the player name of the group. -- @param #GROUP self -- @return #string The player name of the group. function GROUP:GetPlayerName() self:F2( self.GroupName ) local DCSGroup = self:GetDCSObject() if DCSGroup then local PlayerName = DCSGroup:getUnit(1):getPlayerName() self:T3( PlayerName ) return( PlayerName ) end return nil end --- Gets the CallSign of the first DCS Unit of the DCS Group. -- @param #GROUP self -- @return #string The CallSign of the first DCS Unit of the DCS Group. function GROUP:GetCallsign() self:F2( self.GroupName ) local DCSGroup = self:GetDCSObject() if DCSGroup then local GroupCallSign = DCSGroup:getUnit(1):getCallsign() self:T3( GroupCallSign ) return GroupCallSign end BASE:E( { "Cannot GetCallsign", Positionable = self, Alive = self:IsAlive() } ) return nil end --- Returns the current point (Vec2 vector) of the first DCS Unit in the DCS Group. -- @param #GROUP self -- @return DCS#Vec2 Current Vec2 point of the first DCS Unit of the DCS Group. function GROUP:GetVec2() local Unit=self:GetUnit(1) if Unit then local vec2=Unit:GetVec2() return vec2 end end --- Returns the current Vec3 vector of the first Unit in the GROUP. -- @param #GROUP self -- @return DCS#Vec3 Current Vec3 of the first Unit of the GROUP or nil if cannot be found. function GROUP:GetVec3() -- Get first unit. local unit=self:GetUnit(1) if unit then local vec3=unit:GetVec3() return vec3 end self:E("ERROR: Cannot get Vec3 of group "..tostring(self.GroupName)) return nil end --- Returns the average Vec3 vector of the Units in the GROUP. -- @param #GROUP self -- @return DCS#Vec3 Current Vec3 of the GROUP or nil if cannot be found. function GROUP:GetAverageVec3() local units = self:GetUnits() or {} -- Init. local x=0 ; local y=0 ; local z=0 ; local n=0 -- Loop over all units. for _,unit in pairs(units) do local vec3=nil --DCS#Vec3 if unit and unit:IsAlive() then vec3 = unit:GetVec3() end if vec3 then -- Sum up posits. x=x+vec3.x y=y+vec3.y z=z+vec3.z -- Increase counter. n=n+1 end end if n>0 then -- Average. local Vec3={x=x/n, y=y/n, z=z/n} --DCS#Vec3 return Vec3 end return nil end --- Returns a POINT_VEC2 object indicating the point in 2D of the first UNIT of the GROUP within the mission. -- @param #GROUP self -- @return Core.Point#POINT_VEC2 The 2D point vector of the first DCS Unit of the GROUP. -- @return #nil The first UNIT is not existing or alive. function GROUP:GetPointVec2() self:F2(self.GroupName) local FirstUnit = self:GetUnit(1) if FirstUnit then local FirstUnitPointVec2 = FirstUnit:GetPointVec2() self:T3(FirstUnitPointVec2) return FirstUnitPointVec2 end BASE:E( { "Cannot GetPointVec2", Group = self, Alive = self:IsAlive() } ) return nil end --- Returns a COORDINATE object indicating the average position of the GROUP within the mission. -- @param Wrapper.Group#GROUP self -- @return Core.Point#COORDINATE The COORDINATE of the GROUP. function GROUP:GetAverageCoordinate() local vec3 = self:GetAverageVec3() if vec3 then local coord = COORDINATE:NewFromVec3(vec3) local Heading = self:GetHeading() coord.Heading = Heading else BASE:E( { "Cannot GetAverageCoordinate", Group = self, Alive = self:IsAlive() } ) return nil end end --- Returns a COORDINATE object indicating the point of the first UNIT of the GROUP within the mission. -- @param Wrapper.Group#GROUP self -- @return Core.Point#COORDINATE The COORDINATE of the GROUP. function GROUP:GetCoordinate() local Units = self:GetUnits() or {} for _,_unit in pairs(Units) do local FirstUnit = _unit -- Wrapper.Unit#UNIT if FirstUnit then local FirstUnitCoordinate = FirstUnit:GetCoordinate() if FirstUnitCoordinate then local Heading = self:GetHeading() FirstUnitCoordinate.Heading = Heading return FirstUnitCoordinate end end end BASE:E( { "Cannot GetCoordinate", Group = self, Alive = self:IsAlive() } ) end --- Returns a random @{DCS#Vec3} vector (point in 3D of the UNIT within the mission) within a range around the first UNIT of the GROUP. -- @param #GROUP self -- @param #number Radius Radius in meters. -- @return DCS#Vec3 The random 3D point vector around the first UNIT of the GROUP or #nil The GROUP is invalid or empty. -- @usage -- -- If Radius is ignored, returns the DCS#Vec3 of first UNIT of the GROUP function GROUP:GetRandomVec3(Radius) self:F2(self.GroupName) local FirstUnit = self:GetUnit(1) if FirstUnit then local FirstUnitRandomPointVec3 = FirstUnit:GetRandomVec3(Radius) self:T3(FirstUnitRandomPointVec3) return FirstUnitRandomPointVec3 end BASE:E( { "Cannot GetRandomVec3", Group = self, Alive = self:IsAlive() } ) return nil end --- Returns the mean heading of every UNIT in the GROUP in degrees -- @param #GROUP self -- @return #number Mean heading of the GROUP in degrees or #nil The first UNIT is not existing or alive. function GROUP:GetHeading() self:F2(self.GroupName) self:F2(self.GroupName) local GroupSize = self:GetSize() local HeadingAccumulator = 0 local n=0 local Units = self:GetUnits() if GroupSize then for _,unit in pairs(Units) do if unit and unit:IsAlive() then HeadingAccumulator = HeadingAccumulator + unit:GetHeading() n=n+1 end end return math.floor(HeadingAccumulator / n) end BASE:E( { "Cannot GetHeading", Group = self, Alive = self:IsAlive() } ) return nil end --- Return the fuel state and unit reference for the unit with the least -- amount of fuel in the group. -- @param #GROUP self -- @return #number The fuel state of the unit with the least amount of fuel. -- @return Wrapper.Unit#UNIT reference to #Unit object for further processing. function GROUP:GetFuelMin() self:F3(self.ControllableName) if not self:GetDCSObject() then BASE:E( { "Cannot GetFuel", Group = self, Alive = self:IsAlive() } ) return 0 end local min = 65535 -- some sufficiently large number to init with local unit = nil local tmp = nil for UnitID, UnitData in pairs( self:GetUnits() ) do if UnitData and UnitData:IsAlive() then tmp = UnitData:GetFuel() if tmp < min then min = tmp unit = UnitData end end end return min, unit end --- Returns relative amount of fuel (from 0.0 to 1.0) the group has in its -- internal tanks. If there are additional fuel tanks the value may be -- greater than 1.0. -- @param #GROUP self -- @return #number The relative amount of fuel (from 0.0 to 1.0). -- @return #nil The GROUP is not existing or alive. function GROUP:GetFuelAvg() self:F( self.ControllableName ) local DCSControllable = self:GetDCSObject() if DCSControllable then local GroupSize = self:GetSize() local TotalFuel = 0 for UnitID, UnitData in pairs( self:GetUnits() ) do local Unit = UnitData -- Wrapper.Unit#UNIT local UnitFuel = Unit:GetFuel() or 0 self:F( { Fuel = UnitFuel } ) TotalFuel = TotalFuel + UnitFuel end local GroupFuel = TotalFuel / GroupSize return GroupFuel end BASE:E( { "Cannot GetFuel", Group = self, Alive = self:IsAlive() } ) return 0 end --- Returns relative amount of fuel (from 0.0 to 1.0) the group has in its internal tanks. If there are additional fuel tanks the value may be greater than 1.0. -- @param #GROUP self -- @return #number The relative amount of fuel (from 0.0 to 1.0). -- @return #nil The GROUP is not existing or alive. function GROUP:GetFuel() return self:GetFuelAvg() end --- Get the number of shells, rockets, bombs and missiles the whole group currently has. -- @param #GROUP self -- @return #number Total amount of ammo the group has left. This is the sum of shells, rockets, bombs and missiles of all units. -- @return #number Number of shells left. -- @return #number Number of rockets left. -- @return #number Number of bombs left. -- @return #number Number of missiles left. -- @return #number Number of artillery shells left (with explosive mass, included in shells; shells can also be machine gun ammo) function GROUP:GetAmmunition() self:F( self.ControllableName ) local DCSControllable = self:GetDCSObject() local Ntot=0 local Nshells=0 local Nrockets=0 local Nmissiles=0 local Nbombs=0 local Narti=0 if DCSControllable then -- Loop over units. for UnitID, UnitData in pairs( self:GetUnits() ) do local Unit = UnitData -- Wrapper.Unit#UNIT -- Get ammo of the unit local ntot, nshells, nrockets, nbombs, nmissiles, narti = Unit:GetAmmunition() Ntot=Ntot+ntot Nshells=Nshells+nshells Nrockets=Nrockets+nrockets Nmissiles=Nmissiles+nmissiles Nbombs=Nbombs+nbombs Narti=Narti+narti end end return Ntot, Nshells, Nrockets, Nbombs, Nmissiles, Narti end do -- Is Zone methods --- Check if any unit of a group is inside a @{Core.Zone}. -- @param #GROUP self -- @param Core.Zone#ZONE_BASE Zone The zone to test. -- @return #boolean Returns `true` if *at least one unit* is inside the zone or `false` if *no* unit is inside. function GROUP:IsInZone( Zone ) if self:IsAlive() then for UnitID, UnitData in pairs(self:GetUnits()) do local Unit = UnitData -- Wrapper.Unit#UNIT local vec2 = nil if Unit then -- Get 2D vector. That's all we need for the zone check. vec2=Unit:GetVec2() end if vec2 and Zone:IsVec2InZone(vec2) then return true -- At least one unit is in the zone. That is enough. end end return false end return nil end --- Returns true if all units of the group are within a @{Core.Zone}. -- @param #GROUP self -- @param Core.Zone#ZONE_BASE Zone The zone to test. -- @return #boolean Returns true if the Group is completely within the @{Core.Zone#ZONE_BASE} function GROUP:IsCompletelyInZone( Zone ) self:F2( { self.GroupName, Zone } ) if not self:IsAlive() then return false end for UnitID, UnitData in pairs( self:GetUnits() ) do local Unit = UnitData -- Wrapper.Unit#UNIT if Zone:IsVec3InZone( Unit:GetVec3() ) then else return false end end return true end --- Returns true if some but NOT ALL units of the group are within a @{Core.Zone}. -- @param #GROUP self -- @param Core.Zone#ZONE_BASE Zone The zone to test. -- @return #boolean Returns true if the Group is partially within the @{Core.Zone#ZONE_BASE} function GROUP:IsPartlyInZone( Zone ) self:F2( { self.GroupName, Zone } ) local IsOneUnitInZone = false local IsOneUnitOutsideZone = false if not self:IsAlive() then return false end for UnitID, UnitData in pairs( self:GetUnits() ) do local Unit = UnitData -- Wrapper.Unit#UNIT if Zone:IsVec3InZone( Unit:GetVec3() ) then IsOneUnitInZone = true else IsOneUnitOutsideZone = true end end if IsOneUnitInZone and IsOneUnitOutsideZone then return true else return false end end --- Returns true if part or all units of the group are within a @{Core.Zone}. -- @param #GROUP self -- @param Core.Zone#ZONE_BASE Zone The zone to test. -- @return #boolean Returns true if the Group is partially or completely within the @{Core.Zone#ZONE_BASE}. function GROUP:IsPartlyOrCompletelyInZone( Zone ) return self:IsPartlyInZone(Zone) or self:IsCompletelyInZone(Zone) end --- Returns true if none of the group units of the group are within a @{Core.Zone}. -- @param #GROUP self -- @param Core.Zone#ZONE_BASE Zone The zone to test. -- @return #boolean Returns true if the Group is not within the @{Core.Zone#ZONE_BASE} function GROUP:IsNotInZone( Zone ) self:F2( { self.GroupName, Zone } ) if not self:IsAlive() then return true end for UnitID, UnitData in pairs( self:GetUnits() ) do local Unit = UnitData -- Wrapper.Unit#UNIT if Zone:IsVec3InZone( Unit:GetVec3() ) then return false end end return true end --- Returns true if any units of the group are within a @{Core.Zone}. -- @param #GROUP self -- @param Core.Zone#ZONE_BASE Zone The zone to test. -- @return #boolean Returns true if any unit of the Group is within the @{Core.Zone#ZONE_BASE} function GROUP:IsAnyInZone( Zone ) if not self:IsAlive() then return false end for UnitID, UnitData in pairs( self:GetUnits() ) do local Unit = UnitData -- Wrapper.Unit#UNIT if Zone:IsVec3InZone( Unit:GetVec3() ) then return true end end return false end --- Returns the number of UNITs that are in the @{Core.Zone} -- @param #GROUP self -- @param Core.Zone#ZONE_BASE Zone The zone to test. -- @return #number The number of UNITs that are in the @{Core.Zone} function GROUP:CountInZone( Zone ) self:F2( {self.GroupName, Zone} ) local Count = 0 if not self:IsAlive() then return Count end for UnitID, UnitData in pairs( self:GetUnits() ) do local Unit = UnitData -- Wrapper.Unit#UNIT if Zone:IsVec3InZone( Unit:GetVec3() ) then Count = Count + 1 end end return Count end --- Returns if the group is of an air category. -- If the group is a helicopter or a plane, then this method will return true, otherwise false. -- @param #GROUP self -- @return #boolean Air category evaluation result. function GROUP:IsAir() self:F2( self.GroupName ) local DCSGroup = self:GetDCSObject() if DCSGroup then local IsAirResult = DCSGroup:getCategory() == Group.Category.AIRPLANE or DCSGroup:getCategory() == Group.Category.HELICOPTER self:T3( IsAirResult ) return IsAirResult end return nil end --- Returns if the DCS Group contains Helicopters. -- @param #GROUP self -- @return #boolean true if DCS Group contains Helicopters. function GROUP:IsHelicopter() self:F2( self.GroupName ) local DCSGroup = self:GetDCSObject() if DCSGroup then local GroupCategory = DCSGroup:getCategory() self:T2( GroupCategory ) return GroupCategory == Group.Category.HELICOPTER end return nil end --- Returns if the DCS Group contains AirPlanes. -- @param #GROUP self -- @return #boolean true if DCS Group contains AirPlanes. function GROUP:IsAirPlane() self:F2() local DCSGroup = self:GetDCSObject() if DCSGroup then local GroupCategory = DCSGroup:getCategory() self:T2( GroupCategory ) return GroupCategory == Group.Category.AIRPLANE end return nil end --- Returns if the DCS Group contains Ground troops. -- @param #GROUP self -- @return #boolean true if DCS Group contains Ground troops. function GROUP:IsGround() self:F2() local DCSGroup = self:GetDCSObject() if DCSGroup then local GroupCategory = DCSGroup:getCategory() self:T2( GroupCategory ) return GroupCategory == Group.Category.GROUND end return nil end --- Returns if the DCS Group contains Ships. -- @param #GROUP self -- @return #boolean true if DCS Group contains Ships. function GROUP:IsShip() self:F2() local DCSGroup = self:GetDCSObject() if DCSGroup then local GroupCategory = DCSGroup:getCategory() self:T2( GroupCategory ) return GroupCategory == Group.Category.SHIP end return nil end --- Returns if all units of the group are on the ground or landed. -- If all units of this group are on the ground, this function will return true, otherwise false. -- @param #GROUP self -- @return #boolean All units on the ground result. function GROUP:AllOnGround() self:F2() local DCSGroup = self:GetDCSObject() if DCSGroup then local AllOnGroundResult = true for Index, UnitData in pairs( DCSGroup:getUnits() ) do if UnitData:inAir() then AllOnGroundResult = false end end self:T3( AllOnGroundResult ) return AllOnGroundResult end return nil end end do -- AI methods --- Turns the AI On or Off for the GROUP. -- @param #GROUP self -- @param #boolean AIOnOff The value true turns the AI On, the value false turns the AI Off. -- @return #GROUP The GROUP. function GROUP:SetAIOnOff( AIOnOff ) local DCSGroup = self:GetDCSObject() -- DCS#Group if DCSGroup then local DCSController = DCSGroup:getController() -- DCS#Controller if DCSController then DCSController:setOnOff( AIOnOff ) return self end end return nil end --- Turns the AI On for the GROUP. -- @param #GROUP self -- @return #GROUP The GROUP. function GROUP:SetAIOn() return self:SetAIOnOff( true ) end --- Turns the AI Off for the GROUP. -- @param #GROUP self -- @return #GROUP The GROUP. function GROUP:SetAIOff() return self:SetAIOnOff( false ) end end --- Returns the current maximum velocity of the group. -- Each unit within the group gets evaluated, and the maximum velocity (= the unit which is going the fastest) is returned. -- @param #GROUP self -- @return #number Maximum velocity found. function GROUP:GetMaxVelocity() self:F2() local DCSGroup = self:GetDCSObject() if DCSGroup then local GroupVelocityMax = 0 for Index, UnitData in pairs( DCSGroup:getUnits() ) do local UnitVelocityVec3 = UnitData:getVelocity() local UnitVelocity = math.abs( UnitVelocityVec3.x ) + math.abs( UnitVelocityVec3.y ) + math.abs( UnitVelocityVec3.z ) if UnitVelocity > GroupVelocityMax then GroupVelocityMax = UnitVelocity end end return GroupVelocityMax end return nil end --- Returns the current minimum height of the group. -- Each unit within the group gets evaluated, and the minimum height (= the unit which is the lowest elevated) is returned. -- @param #GROUP self -- @return #number Minimum height found. function GROUP:GetMinHeight() self:F2() local DCSGroup = self:GetDCSObject() if DCSGroup then local GroupHeightMin = 999999999 for Index, UnitData in pairs( DCSGroup:getUnits() ) do local UnitData = UnitData -- DCS#Unit local UnitHeight = UnitData:getPoint() if UnitHeight < GroupHeightMin then GroupHeightMin = UnitHeight end end return GroupHeightMin end return nil end --- Returns the current maximum height of the group. -- Each unit within the group gets evaluated, and the maximum height (= the unit which is the highest elevated) is returned. -- @param #GROUP self -- @return #number Maximum height found. function GROUP:GetMaxHeight() self:F2() local DCSGroup = self:GetDCSObject() if DCSGroup then local GroupHeightMax = -999999999 for Index, UnitData in pairs( DCSGroup:getUnits() ) do local UnitData = UnitData -- DCS#Unit local UnitHeight = UnitData:getPoint() if UnitHeight > GroupHeightMax then GroupHeightMax = UnitHeight end end return GroupHeightMax end return nil end -- RESPAWNING --- Returns the group template from the global _DATABASE object (an instance of @{Core.Database#DATABASE}). -- @param #GROUP self -- @return #table function GROUP:GetTemplate() local GroupName = self:GetName() return UTILS.DeepCopy( _DATABASE:GetGroupTemplate( GroupName ) ) end --- Returns the group template route.points[] (the waypoints) from the global _DATABASE object (an instance of @{Core.Database#DATABASE}). -- @param #GROUP self -- @return #table function GROUP:GetTemplateRoutePoints() local GroupName = self:GetName() return UTILS.DeepCopy( _DATABASE:GetGroupTemplate( GroupName ).route.points ) end --- Sets the controlled status in a Template. -- @param #GROUP self -- @param #boolean Controlled true is controlled, false is uncontrolled. -- @return #table function GROUP:SetTemplateControlled( Template, Controlled ) Template.uncontrolled = not Controlled return Template end --- Sets the CountryID of the group in a Template. -- @param #GROUP self -- @param DCS#country.id CountryID The country ID. -- @return #table function GROUP:SetTemplateCountry( Template, CountryID ) Template.CountryID = CountryID return Template end --- Sets the CoalitionID of the group in a Template. -- @param #GROUP self -- @param DCS#coalition.side CoalitionID The coalition ID. -- @return #table function GROUP:SetTemplateCoalition( Template, CoalitionID ) Template.CoalitionID = CoalitionID return Template end --- Set the heading for the units in degrees within the respawned group. -- @param #GROUP self -- @param #number Heading The heading in meters. -- @return #GROUP self function GROUP:InitHeading( Heading ) self.InitRespawnHeading = Heading return self end --- Set the height for the units in meters for the respawned group. (This is applicable for air units). -- @param #GROUP self -- @param #number Height The height in meters. -- @return #GROUP self function GROUP:InitHeight( Height ) self.InitRespawnHeight = Height return self end --- Set the respawn @{Core.Zone} for the respawned group. -- @param #GROUP self -- @param Core.Zone#ZONE Zone The zone in meters. -- @return #GROUP self function GROUP:InitZone( Zone ) self.InitRespawnZone = Zone return self end --- Randomize the positions of the units of the respawned group within the @{Core.Zone}. -- When a Respawn happens, the units of the group will be placed at random positions within the Zone (selected). -- @param #GROUP self -- @param #boolean PositionZone true will randomize the positions within the Zone. -- @return #GROUP self function GROUP:InitRandomizePositionZone( PositionZone ) self.InitRespawnRandomizePositionZone = PositionZone self.InitRespawnRandomizePositionInner = nil self.InitRespawnRandomizePositionOuter = nil return self end --- Randomize the positions of the units of the respawned group in a circle band. -- When a Respawn happens, the units of the group will be positioned at random places within the Outer and Inner radius. -- Thus, a band is created around the respawn location where the units will be placed at random positions. -- @param #GROUP self -- @param #boolean OuterRadius Outer band in meters from the center. -- @param #boolean InnerRadius Inner band in meters from the center. -- @return #GROUP self function GROUP:InitRandomizePositionRadius( OuterRadius, InnerRadius ) self.InitRespawnRandomizePositionZone = nil self.InitRespawnRandomizePositionOuter = OuterRadius self.InitRespawnRandomizePositionInner = InnerRadius return self end --- Set respawn coordinate. -- @param #GROUP self -- @param Core.Point#COORDINATE coordinate Coordinate where the group should be respawned. -- @return #GROUP self function GROUP:InitCoordinate(coordinate) self:F({coordinate=coordinate}) self.InitCoord=coordinate return self end --- Sets the radio comms on or off when the group is respawned. Same as checking/unchecking the COMM box in the mission editor. -- @param #GROUP self -- @param #boolean switch If true (or nil), enables the radio comms. If false, disables the radio for the spawned group. -- @return #GROUP self function GROUP:InitRadioCommsOnOff(switch) self:F({switch=switch}) if switch==true or switch==nil then self.InitRespawnRadio=true else self.InitRespawnRadio=false end return self end --- Sets the radio frequency of the group when it is respawned. -- @param #GROUP self -- @param #number frequency The frequency in MHz. -- @return #GROUP self function GROUP:InitRadioFrequency(frequency) self:F({frequency=frequency}) self.InitRespawnFreq=frequency return self end --- Set radio modulation when the group is respawned. Default is AM. -- @param #GROUP self -- @param #string modulation Either "FM" or "AM". If no value is given, modulation is set to AM. -- @return #GROUP self function GROUP:InitRadioModulation(modulation) self:F({modulation=modulation}) if modulation and modulation:lower()=="fm" then self.InitRespawnModu=radio.modulation.FM else self.InitRespawnModu=radio.modulation.AM end return self end --- Sets the modex (tail number) of the first unit of the group. If more units are in the group, the number is increased with every unit. -- @param #GROUP self -- @param #string modex Tail number of the first unit. -- @return #GROUP self function GROUP:InitModex(modex) self:F({modex=modex}) if modex then self.InitRespawnModex=tonumber(modex) end return self end --- Respawn the @{Wrapper.Group} at a @{Point}. -- The method will setup the new group template according the Init(Respawn) settings provided for the group. -- These settings can be provided by calling the relevant Init...() methods of the Group. -- -- - @{#GROUP.InitHeading}: Set the heading for the units in degrees within the respawned group. -- - @{#GROUP.InitHeight}: Set the height for the units in meters for the respawned group. (This is applicable for air units). -- - @{#GROUP.InitRandomizeHeading}: Randomize the headings for the units within the respawned group. -- - @{#GROUP.InitZone}: Set the respawn @{Core.Zone} for the respawned group. -- - @{#GROUP.InitRandomizeZones}: Randomize the respawn @{Core.Zone} between one of the @{Core.Zone}s given for the respawned group. -- - @{#GROUP.InitRandomizePositionZone}: Randomize the positions of the units of the respawned group within the @{Core.Zone}. -- - @{#GROUP.InitRandomizePositionRadius}: Randomize the positions of the units of the respawned group in a circle band. -- - @{#GROUP.InitRandomizeTemplates}: Randomize the Template for the respawned group. -- -- -- Notes: -- -- - When InitZone or InitRandomizeZones is not used, the position of the respawned group will be its current position. -- - The current alive group will always be destroyed and respawned using the template definition. -- -- @param Wrapper.Group#GROUP self -- @param #table Template (optional) The template of the Group retrieved with GROUP:GetTemplate(). If the template is not provided, the template will be retrieved of the group itself. -- @param #boolean Reset Reset positions if TRUE. -- @return Wrapper.Group#GROUP self function GROUP:Respawn( Template, Reset ) -- Given template or get old. Template = Template or self:GetTemplate() -- Get correct heading. local function _Heading(course) local h if course<=180 then h=math.rad(course) else h=-math.rad(360-course) end return h end -- First check if group is alive. if self:IsAlive() then -- Respawn zone. local Zone = self.InitRespawnZone -- Core.Zone#ZONE -- Zone position or current group position. local Vec3 = Zone and Zone:GetVec3() or self:GetVec3() -- From point of the template. local From = { x = Template.x, y = Template.y } -- X, Y Template.x = Vec3.x Template.y = Vec3.z --Template.x = nil --Template.y = nil -- Debug number of units. self:F( #Template.units ) -- Reset position etc? if Reset == true then -- Loop over units in group. for UnitID, UnitData in pairs( self:GetUnits() ) do local GroupUnit = UnitData -- Wrapper.Unit#UNIT self:F(GroupUnit:GetName()) if GroupUnit:IsAlive() then self:I("FF Alive") -- Get unit position vector. local GroupUnitVec3 = GroupUnit:GetVec3() -- Check if respawn zone is set. if Zone then if self.InitRespawnRandomizePositionZone then GroupUnitVec3 = Zone:GetRandomVec3() else if self.InitRespawnRandomizePositionInner and self.InitRespawnRandomizePositionOuter then GroupUnitVec3 = POINT_VEC3:NewFromVec2( From ):GetRandomPointVec3InRadius( self.InitRespawnRandomizePositionsOuter, self.InitRespawnRandomizePositionsInner ) else GroupUnitVec3 = Zone:GetVec3() end end end -- Coordinate where the group should be respawned. if self.InitCoord then GroupUnitVec3=self.InitCoord:GetVec3() end -- Altitude Template.units[UnitID].alt = self.InitRespawnHeight and self.InitRespawnHeight or GroupUnitVec3.y -- Unit position. Why not simply take the current positon? if Zone then Template.units[UnitID].x = ( Template.units[UnitID].x - From.x ) + GroupUnitVec3.x -- Keep the original x position of the template and translate to the new position. Template.units[UnitID].y = ( Template.units[UnitID].y - From.y ) + GroupUnitVec3.z -- Keep the original z position of the template and translate to the new position. else Template.units[UnitID].x=GroupUnitVec3.x Template.units[UnitID].y=GroupUnitVec3.z end -- Set heading. Template.units[UnitID].heading = _Heading(self.InitRespawnHeading and self.InitRespawnHeading or GroupUnit:GetHeading()) Template.units[UnitID].psi = -Template.units[UnitID].heading -- Debug. self:F( { UnitID, Template.units[UnitID], Template.units[UnitID] } ) end end elseif Reset==false then -- Reset=false or nil -- Loop over template units. for UnitID, TemplateUnitData in pairs( Template.units ) do self:F( "Reset" ) -- Position from template. local GroupUnitVec3 = { x = TemplateUnitData.x, y = TemplateUnitData.alt, z = TemplateUnitData.y } -- Respawn zone position. if Zone then if self.InitRespawnRandomizePositionZone then GroupUnitVec3 = Zone:GetRandomVec3() else if self.InitRespawnRandomizePositionInner and self.InitRespawnRandomizePositionOuter then GroupUnitVec3 = POINT_VEC3:NewFromVec2( From ):GetRandomPointVec3InRadius( self.InitRespawnRandomizePositionsOuter, self.InitRespawnRandomizePositionsInner ) else GroupUnitVec3 = Zone:GetVec3() end end end -- Coordinate where the group should be respawned. if self.InitCoord then GroupUnitVec3=self.InitCoord:GetVec3() end -- Set altitude. Template.units[UnitID].alt = self.InitRespawnHeight and self.InitRespawnHeight or GroupUnitVec3.y -- Unit position. Template.units[UnitID].x = ( Template.units[UnitID].x - From.x ) + GroupUnitVec3.x -- Keep the original x position of the template and translate to the new position. Template.units[UnitID].y = ( Template.units[UnitID].y - From.y ) + GroupUnitVec3.z -- Keep the original z position of the template and translate to the new position. -- Heading Template.units[UnitID].heading = self.InitRespawnHeading and self.InitRespawnHeading or TemplateUnitData.heading -- Debug. self:F( { UnitID, Template.units[UnitID], Template.units[UnitID] } ) end else local units=self:GetUnits() -- Loop over template units. for UnitID, Unit in pairs(Template.units) do for _,_unit in pairs(units) do local unit=_unit --Wrapper.Unit#UNIT if unit:GetName()==Unit.name then local coord=unit:GetCoordinate() local heading=unit:GetHeading() Unit.x=coord.x Unit.y=coord.z Unit.alt=coord.y Unit.heading=math.rad(heading) Unit.psi=-Unit.heading end end end end end -- Set tail number. if self.InitRespawnModex then for UnitID=1,#Template.units do Template.units[UnitID].onboard_num=string.format("%03d", self.InitRespawnModex+(UnitID-1)) end end -- Set radio frequency and modulation. if self.InitRespawnRadio then Template.communication=self.InitRespawnRadio end if self.InitRespawnFreq then Template.frequency=self.InitRespawnFreq end if self.InitRespawnModu then Template.modulation=self.InitRespawnModu end -- Destroy old group. Dont trigger any dead/crash events since this is a respawn. self:Destroy(false) self:T({Template=Template}) -- Spawn new group. _DATABASE:Spawn(Template) -- Reset events. self:ResetEvents() return self end --- Respawn a group at an airbase. -- Note that the group has to be on parking spots at the airbase already in order for this to work. -- So each unit of the group is respawned at exactly the same parking spot as it currently occupies. -- @param Wrapper.Group#GROUP self -- @param #table SpawnTemplate (Optional) The spawn template for the group. If no template is given it is exacted from the group. -- @param Core.Spawn#SPAWN.Takeoff Takeoff (Optional) Takeoff type. Sould be either SPAWN.Takeoff.Cold or SPAWN.Takeoff.Hot. Default is SPAWN.Takeoff.Hot. -- @param #boolean Uncontrolled (Optional) If true, spawn in uncontrolled state. -- @return Wrapper.Group#GROUP Group spawned at airbase or nil if group could not be spawned. function GROUP:RespawnAtCurrentAirbase(SpawnTemplate, Takeoff, Uncontrolled) -- R2.4 self:F2( { SpawnTemplate, Takeoff, Uncontrolled} ) if self and self:IsAlive() then -- Get closest airbase. Should be the one we are currently on. local airbase=self:GetCoordinate():GetClosestAirbase() if airbase then self:F2("Closest airbase = "..airbase:GetName()) else self:E("ERROR: could not find closest airbase!") return nil end -- Takeoff type. Default hot. Takeoff = Takeoff or SPAWN.Takeoff.Hot -- Coordinate of the airbase. local AirbaseCoord=airbase:GetCoordinate() -- Spawn template. SpawnTemplate = SpawnTemplate or self:GetTemplate() if SpawnTemplate then local SpawnPoint = SpawnTemplate.route.points[1] -- These are only for ships. SpawnPoint.linkUnit = nil SpawnPoint.helipadId = nil SpawnPoint.airdromeId = nil -- Aibase id and category. local AirbaseID = airbase:GetID() local AirbaseCategory = airbase:GetAirbaseCategory() if AirbaseCategory == Airbase.Category.SHIP or AirbaseCategory == Airbase.Category.HELIPAD then SpawnPoint.linkUnit = AirbaseID SpawnPoint.helipadId = AirbaseID elseif AirbaseCategory == Airbase.Category.AIRDROME then SpawnPoint.airdromeId = AirbaseID end SpawnPoint.type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type SpawnPoint.action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action -- Get the units of the group. local units=self:GetUnits() local x local y for UnitID=1,#units do local unit=units[UnitID] --Wrapper.Unit#UNIT -- Get closest parking spot of current unit. Note that we look for occupied spots since the unit is currently sitting on it! local Parkingspot, TermialID, Distance=unit:GetCoordinate():GetClosestParkingSpot(airbase) --Parkingspot:MarkToAll("parking spot") self:T2(string.format("Closest parking spot distance = %s, terminal ID=%s", tostring(Distance), tostring(TermialID))) -- Get unit coordinates for respawning position. local uc=unit:GetCoordinate() --uc:MarkToAll(string.format("re-spawnplace %s terminal %d", unit:GetName(), TermialID)) SpawnTemplate.units[UnitID].x = uc.x --Parkingspot.x SpawnTemplate.units[UnitID].y = uc.z --Parkingspot.z SpawnTemplate.units[UnitID].alt = uc.y --Parkingspot.y SpawnTemplate.units[UnitID].parking = TermialID SpawnTemplate.units[UnitID].parking_id = nil --SpawnTemplate.units[UnitID].unitId=nil end --SpawnTemplate.groupId=nil SpawnPoint.x = SpawnTemplate.units[1].x --x --AirbaseCoord.x SpawnPoint.y = SpawnTemplate.units[1].y --y --AirbaseCoord.z SpawnPoint.alt = SpawnTemplate.units[1].alt --AirbaseCoord:GetLandHeight() SpawnTemplate.x = SpawnTemplate.units[1].x --x --AirbaseCoord.x SpawnTemplate.y = SpawnTemplate.units[1].y --y --AirbaseCoord.z -- Set uncontrolled state. SpawnTemplate.uncontrolled=Uncontrolled -- Set radio frequency and modulation. if self.InitRespawnRadio then SpawnTemplate.communication=self.InitRespawnRadio end if self.InitRespawnFreq then SpawnTemplate.frequency=self.InitRespawnFreq end if self.InitRespawnModu then SpawnTemplate.modulation=self.InitRespawnModu end -- Destroy old group. self:Destroy(false) -- Spawn new group. _DATABASE:Spawn(SpawnTemplate) -- Reset events. self:ResetEvents() return self end else self:E("WARNING: GROUP is not alive!") end return nil end --- Return the mission template of the group. -- @param #GROUP self -- @return #table The MissionTemplate function GROUP:GetTaskMission() self:F2( self.GroupName ) return routines.utils.deepCopy( _DATABASE.Templates.Groups[self.GroupName].Template ) end --- Return the mission route of the group. -- @param #GROUP self -- @return #table The mission route defined by points. function GROUP:GetTaskRoute() self:F2( self.GroupName ) return routines.utils.deepCopy( _DATABASE.Templates.Groups[self.GroupName].Template.route.points ) end --- Return the route of a group by using the global _DATABASE object (an instance of @{Core.Database#DATABASE}). -- @param #GROUP self -- @param #number Begin The route point from where the copy will start. The base route point is 0. -- @param #number End The route point where the copy will end. The End point is the last point - the End point. The last point has base 0. -- @param #boolean Randomize Randomization of the route, when true. -- @param #number Radius When randomization is on, the randomization is within the radius. function GROUP:CopyRoute( Begin, End, Randomize, Radius ) self:F2( { Begin, End } ) local Points = {} -- Could be a Spawned Group local GroupName = string.match( self:GetName(), ".*#" ) if GroupName then GroupName = GroupName:sub( 1, -2 ) else GroupName = self:GetName() end self:T3( { GroupName } ) local Template = _DATABASE.Templates.Groups[GroupName].Template if Template then if not Begin then Begin = 0 end if not End then End = 0 end for TPointID = Begin + 1, #Template.route.points - End do if Template.route.points[TPointID] then Points[#Points+1] = routines.utils.deepCopy( Template.route.points[TPointID] ) if Randomize then if not Radius then Radius = 500 end Points[#Points].x = Points[#Points].x + math.random( Radius * -1, Radius ) Points[#Points].y = Points[#Points].y + math.random( Radius * -1, Radius ) end end end return Points else error( "Template not found for Group : " .. GroupName ) end return nil end --- Calculate the maxium A2G threat level of the Group. -- @param #GROUP self -- @return #number Number between 0 and 10. function GROUP:CalculateThreatLevelA2G() local MaxThreatLevelA2G = 0 for UnitName, UnitData in pairs( self:GetUnits() ) do local ThreatUnit = UnitData -- Wrapper.Unit#UNIT local ThreatLevelA2G = ThreatUnit:GetThreatLevel() if ThreatLevelA2G > MaxThreatLevelA2G then MaxThreatLevelA2G = ThreatLevelA2G end end self:T3( MaxThreatLevelA2G ) return MaxThreatLevelA2G end --- Get threat level of the group. -- @param #GROUP self -- @return #number Max threat level (a number between 0 and 10). function GROUP:GetThreatLevel() local threatlevelMax = 0 for UnitName, UnitData in pairs(self:GetUnits()) do local ThreatUnit = UnitData -- Wrapper.Unit#UNIT local threatlevel = ThreatUnit:GetThreatLevel() if threatlevel > threatlevelMax then threatlevelMax=threatlevel end end return threatlevelMax end --- Returns true if the first unit of the GROUP is in the air. -- @param Wrapper.Group#GROUP self -- @return #boolean true if in the first unit of the group is in the air or #nil if the GROUP is not existing or not alive. function GROUP:InAir() self:F2( self.GroupName ) local DCSGroup = self:GetDCSObject() if DCSGroup then local DCSUnit = DCSGroup:getUnit(1) if DCSUnit then local GroupInAir = DCSGroup:getUnit(1):inAir() self:T3( GroupInAir ) return GroupInAir end end return nil end --- Checks whether any unit (or optionally) all units of a group is(are) airbore or not. -- @param Wrapper.Group#GROUP self -- @param #boolean AllUnits (Optional) If true, check whether all units of the group are airborne. -- @return #boolean True if at least one (optionally all) unit(s) is(are) airborne or false otherwise. Nil if no unit exists or is alive. function GROUP:IsAirborne(AllUnits) self:F2( self.GroupName ) -- Get all units of the group. local units=self:GetUnits() if units then if AllUnits then --- We want to know if ALL units are airborne. for _,_unit in pairs(units) do local unit=_unit --Wrapper.Unit#UNIT if unit then -- Unit in air or not. local inair=unit:InAir() -- At least one unit is not in air. if not inair then return false end end end -- All units are in air. return true else --- We want to know if ANY unit is airborne. for _,_unit in pairs(units) do local unit=_unit --Wrapper.Unit#UNIT if unit then -- Unit in air or not. local inair=unit:InAir() if inair then -- At least one unit is in air. return true end end -- No unit is in air. return false end end end return nil end --- Returns the DCS descriptor table of the nth unit of the group. -- @param #GROUP self -- @param #number n (Optional) The number of the unit for which the dscriptor is returned. -- @return DCS#Object.Desc The descriptor of the first unit of the group or #nil if the group does not exist any more. function GROUP:GetDCSDesc(n) -- Default. n=n or 1 local unit=self:GetUnit(n) if unit and unit:IsAlive()~=nil then local desc=unit:GetDesc() return desc end return nil end --- Get the generalized attribute of a self. -- Note that for a heterogenious self, the attribute is determined from the attribute of the first unit! -- @param #GROUP self -- @return #string Generalized attribute of the self. function GROUP:GetAttribute() -- Default local attribute=GROUP.Attribute.OTHER_UNKNOWN --#GROUP.Attribute if self then ----------- --- Air --- ----------- -- Planes local transportplane=self:HasAttribute("Transports") and self:HasAttribute("Planes") local awacs=self:HasAttribute("AWACS") local fighter=self:HasAttribute("Fighters") or self:HasAttribute("Interceptors") or self:HasAttribute("Multirole fighters") or (self:HasAttribute("Bombers") and not self:HasAttribute("Strategic bombers")) local bomber=self:HasAttribute("Strategic bombers") local tanker=self:HasAttribute("Tankers") local uav=self:HasAttribute("UAVs") -- Helicopters local transporthelo=self:HasAttribute("Transport helicopters") local attackhelicopter=self:HasAttribute("Attack helicopters") -------------- --- Ground --- -------------- -- Ground local apc=self:HasAttribute("APC") local truck=self:HasAttribute("Trucks") and self:GetCategory()==Group.Category.GROUND local infantry=self:HasAttribute("Infantry") local artillery=self:HasAttribute("Artillery") local tank=self:HasAttribute("Old Tanks") or self:HasAttribute("Modern Tanks") or self:HasAttribute("Tanks") local aaa=self:HasAttribute("AAA") and (not self:HasAttribute("SAM elements")) local ewr=self:HasAttribute("EWR") local ifv=self:HasAttribute("IFV") local sam=self:HasAttribute("SAM elements") or self:HasAttribute("Optical Tracker") -- Train local train=self:GetCategory()==Group.Category.TRAIN ------------- --- Naval --- ------------- -- Ships local aircraftcarrier=self:HasAttribute("Aircraft Carriers") local warship=self:HasAttribute("Heavy armed ships") local armedship=self:HasAttribute("Armed ships") local unarmedship=self:HasAttribute("Unarmed ships") -- Define attribute. Order of attack is important. if fighter then attribute=GROUP.Attribute.AIR_FIGHTER elseif bomber then attribute=GROUP.Attribute.AIR_BOMBER elseif awacs then attribute=GROUP.Attribute.AIR_AWACS elseif transportplane then attribute=GROUP.Attribute.AIR_TRANSPORTPLANE elseif tanker then attribute=GROUP.Attribute.AIR_TANKER -- helos elseif attackhelicopter then attribute=GROUP.Attribute.AIR_ATTACKHELO elseif transporthelo then attribute=GROUP.Attribute.AIR_TRANSPORTHELO elseif uav then attribute=GROUP.Attribute.AIR_UAV -- ground - order of attack elseif ewr then attribute=GROUP.Attribute.GROUND_EWR elseif sam then attribute=GROUP.Attribute.GROUND_SAM elseif aaa then attribute=GROUP.Attribute.GROUND_AAA elseif artillery then attribute=GROUP.Attribute.GROUND_ARTILLERY elseif tank then attribute=GROUP.Attribute.GROUND_TANK elseif ifv then attribute=GROUP.Attribute.GROUND_IFV elseif apc then attribute=GROUP.Attribute.GROUND_APC elseif infantry then attribute=GROUP.Attribute.GROUND_INFANTRY elseif truck then attribute=GROUP.Attribute.GROUND_TRUCK elseif train then attribute=GROUP.Attribute.GROUND_TRAIN -- ships elseif aircraftcarrier then attribute=GROUP.Attribute.NAVAL_AIRCRAFTCARRIER elseif warship then attribute=GROUP.Attribute.NAVAL_WARSHIP elseif armedship then attribute=GROUP.Attribute.NAVAL_ARMEDSHIP elseif unarmedship then attribute=GROUP.Attribute.NAVAL_UNARMEDSHIP else if self:IsGround() then attribute=GROUP.Attribute.GROUND_OTHER elseif self:IsShip() then attribute=GROUP.Attribute.NAVAL_OTHER elseif self:IsAir() then attribute=GROUP.Attribute.AIR_OTHER else attribute=GROUP.Attribute.OTHER_UNKNOWN end end end return attribute end do -- Route methods --- (AIR) Return the Group to an @{Wrapper.Airbase#AIRBASE}. -- The following things are to be taken into account: -- -- * The group is respawned to achieve the RTB, there may be side artefacts as a result of this. (Like weapons suddenly come back). -- * A group consisting out of more than one unit, may rejoin formation when respawned. -- * A speed can be given in km/h. If no speed is specified, the maximum speed of the first unit will be taken to return to base. -- * When there is no @{Wrapper.Airbase} object specified, the group will return to the home base if the route of the group is pinned at take-off or at landing to a base. -- * When there is no @{Wrapper.Airbase} object specified and the group route is not pinned to any airbase, it will return to the nearest airbase. -- -- @param #GROUP self -- @param Wrapper.Airbase#AIRBASE RTBAirbase (optional) The @{Wrapper.Airbase} to return to. If blank, the controllable will return to the nearest friendly airbase. -- @param #number Speed (optional) The Speed, if no Speed is given, 80% of maximum Speed of the group is selected. -- @return #GROUP self function GROUP:RouteRTB( RTBAirbase, Speed ) self:F( { RTBAirbase:GetName(), Speed } ) local DCSGroup = self:GetDCSObject() if DCSGroup then if RTBAirbase then -- If speed is not given take 80% of max speed. local Speed=Speed or self:GetSpeedMax()*0.8 -- Curent (from) waypoint. local coord=self:GetCoordinate() local PointFrom=coord:WaypointAirTurningPoint(nil, Speed) -- Airbase coordinate. --local PointAirbase=RTBAirbase:GetCoordinate():SetAltitude(coord.y):WaypointAirTurningPoint(nil ,Speed) -- Landing waypoint. More general than prev version since it should also work with FAPRS and ships. local PointLanding=RTBAirbase:GetCoordinate():WaypointAirLanding(Speed, RTBAirbase) -- Waypoint table. local Points={PointFrom, PointLanding} --local Points={PointFrom, PointAirbase, PointLanding} -- Debug info. self:T3(Points) -- Get group template. local Template=self:GetTemplate() -- Set route points. Template.route.points=Points -- Respawn the group. self:Respawn(Template, true) -- Route the group or this will not work. self:Route(Points) else -- Clear all tasks. self:ClearTasks() end end return self end end function GROUP:OnReSpawn( ReSpawnFunction ) self.ReSpawnFunction = ReSpawnFunction end do -- Event Handling --- Subscribe to a DCS Event. -- @param #GROUP self -- @param Core.Event#EVENTS Event -- @param #function EventFunction (optional) The function to be called when the event occurs for the GROUP. -- @return #GROUP function GROUP:HandleEvent( Event, EventFunction, ... ) self:EventDispatcher():OnEventForGroup( self:GetName(), EventFunction, self, Event, ... ) return self end --- UnSubscribe to a DCS event. -- @param #GROUP self -- @param Core.Event#EVENTS Event -- @return #GROUP function GROUP:UnHandleEvent( Event ) self:EventDispatcher():RemoveEvent( self, Event ) return self end --- Reset the subscriptions. -- @param #GROUP self -- @return #GROUP function GROUP:ResetEvents() self:EventDispatcher():Reset( self ) for UnitID, UnitData in pairs( self:GetUnits() ) do UnitData:ResetEvents() end return self end end do -- Players --- Get player names -- @param #GROUP self -- @return #table The group has players, an array of player names is returned. -- @return #nil The group has no players function GROUP:GetPlayerNames() local HasPlayers = false local PlayerNames = {} local Units = self:GetUnits() for UnitID, UnitData in pairs( Units ) do local Unit = UnitData -- Wrapper.Unit#UNIT local PlayerName = Unit:GetPlayerName() if PlayerName and PlayerName ~= "" then PlayerNames = PlayerNames or {} table.insert( PlayerNames, PlayerName ) HasPlayers = true end end if HasPlayers == true then self:F2( PlayerNames ) return PlayerNames end return nil end --- Get the active player count in the group. -- @param #GROUP self -- @return #number The amount of players. function GROUP:GetPlayerCount() local PlayerCount = 0 local Units = self:GetUnits() for UnitID, UnitData in pairs( Units or {} ) do local Unit = UnitData -- Wrapper.Unit#UNIT local PlayerName = Unit:GetPlayerName() if PlayerName and PlayerName ~= "" then PlayerCount = PlayerCount + 1 end end return PlayerCount end end --- GROUND - Switch on/off radar emissions for the group. -- @param #GROUP self -- @param #boolean switch If true, emission is enabled. If false, emission is disabled. -- @return #GROUP self function GROUP:EnableEmission(switch) self:F2( self.GroupName ) local switch = switch or false local DCSUnit = self:GetDCSObject() if DCSUnit then DCSUnit:enableEmission(switch) end return self end --- Switch on/off invisible flag for the group. -- @param #GROUP self -- @param #boolean switch If true, emission is enabled. If false, emission is disabled. -- @return #GROUP self function GROUP:SetCommandInvisible(switch) self:F2( self.GroupName ) if switch==nil then switch=false end local SetInvisible = {id = 'SetInvisible', params = {value = switch}} self:SetCommand(SetInvisible) return self end --- Switch on/off immortal flag for the group. -- @param #GROUP self -- @param #boolean switch If true, emission is enabled. If false, emission is disabled. -- @return #GROUP self function GROUP:SetCommandImmortal(switch) self:F2( self.GroupName ) if switch==nil then switch=false end local SetImmortal = {id = 'SetImmortal', params = {value = switch}} self:SetCommand(SetImmortal) return self end --- Get skill from Group. Effectively gets the skill from Unit 1 as the group holds no skill value. -- @param #GROUP self -- @return #string Skill String of skill name. function GROUP:GetSkill() self:F2( self.GroupName ) local unit = self:GetUnit(1) local name = unit:GetName() local skill = _DATABASE.Templates.Units[name].Template.skill or "Random" return skill end --- Get the unit in the group with the highest threat level, which is still alive. -- @param #GROUP self -- @return Wrapper.Unit#UNIT The most dangerous unit in the group. -- @return #number Threat level of the unit. function GROUP:GetHighestThreat() -- Get units of the group. local units=self:GetUnits() if units then local threat=nil ; local maxtl=0 for _,_unit in pairs(units or {}) do local unit=_unit --Wrapper.Unit#UNIT if unit and unit:IsAlive() then -- Threat level of group. local tl=unit:GetThreatLevel() -- Check if greater the current threat. if tl>maxtl then maxtl=tl threat=unit end end end return threat, maxtl end return nil, nil end --- Get TTS friendly, optionally customized callsign mainly for **player groups**. A customized callsign is taken from the #GROUP name, after an optional '#' sign, e.g. "Aerial 1-1#Ghostrider" resulting in "Ghostrider 9", or, -- if that isn't available, from the playername, as set in the mission editor main screen under Logbook, after an optional '|' sign (actually, more of a personal call sign), e.g. "Apple|Moose" results in "Moose 9 1". Options see below. -- @param #GROUP self -- @param #boolean ShortCallsign Return a shortened customized callsign, i.e. "Ghostrider 9" and not "Ghostrider 9 1" -- @param #boolean Keepnumber (Player only) Return customized callsign, incl optional numbers at the end, e.g. "Aerial 1-1#Ghostrider 109" results in "Ghostrider 109", if you want to e.g. use historical US Navy Callsigns -- @param #table CallsignTranslations Table to translate between DCS standard callsigns and bespoke ones. Does not apply if using customized -- callsigns from playername or group name. -- @return #string Callsign -- @usage -- -- Set Custom CAP Flight Callsigns for use with TTS -- mygroup:GetCustomCallSign(true,false,{ -- Devil = 'Bengal', -- Snake = 'Winder', -- Colt = 'Camelot', -- Enfield = 'Victory', -- Uzi = 'Evil Eye' -- }) -- -- results in this outcome if the group has Callsign "Enfield 9 1" on the 1st #UNIT of the group: -- -- 'Victory 9' -- -- function GROUP:GetCustomCallSign(ShortCallsign,Keepnumber,CallsignTranslations) --self:I("GetCustomCallSign") local callsign = "Ghost 1" if self:IsAlive() then local IsPlayer = self:IsPlayer() local shortcallsign = self:GetCallsign() or "unknown91" -- e.g.Uzi91, but we want Uzi 9 1 local callsignroot = string.match(shortcallsign, '(%a+)') or "Ghost" -- Uzi --self:I("CallSign = " .. callsignroot) local groupname = self:GetName() local callnumber = string.match(shortcallsign, "(%d+)$" ) or "91" -- 91 local callnumbermajor = string.char(string.byte(callnumber,1)) -- 9 local callnumberminor = string.char(string.byte(callnumber,2)) -- 1 local personalized = false if IsPlayer and string.find(groupname,"#") then -- personalized flight name in group naming if Keepnumber then shortcallsign = string.match(groupname,"#(.+)") or "Ghost 111" -- Ghostrider 219 else shortcallsign = string.match(groupname,"#%s*([%a]+)") or "Ghost" -- Ghostrider end personalized = true elseif IsPlayer and string.find(self:GetPlayerName(),"|") then -- personalized flight name in group naming shortcallsign = string.match(self:GetPlayerName(),"|%s*([%a]+)") or string.match(self:GetPlayerName(),"|%s*([%d]+)") or "Ghost" -- Ghostrider personalized = true end if (not personalized) and CallsignTranslations and CallsignTranslations[callsignroot] then callsignroot = CallsignTranslations[callsignroot] end if personalized then -- player personalized callsign -- remove trailing/leading spaces shortcallsign=string.gsub(shortcallsign,"^%s*","") shortcallsign=string.gsub(shortcallsign,"%s*$","") if Keepnumber then return shortcallsign -- Ghostrider 219 elseif ShortCallsign then callsign = shortcallsign.." "..callnumbermajor -- Ghostrider 9 else callsign = shortcallsign.." "..callnumbermajor.." "..callnumberminor -- Ghostrider 9 1 end return callsign end -- AI or not personalized if ShortCallsign then callsign = callsignroot.." "..callnumbermajor -- Uzi/Victory 9 else callsign = callsignroot.." "..callnumbermajor.." "..callnumberminor -- Uzi/Victory 9 1 end --self:I("Generated Callsign = " .. callsign) end return callsign end --- -- @param #GROUP self -- @param Wrapper.Group#GROUP CarrierGroup. -- @param #number Speed Speed in knots. -- @param #boolean ToKIAS If true, adjust speed to altitude (KIAS). -- @param #number Altitude Altitude the tanker orbits at in feet. -- @param #number Delay (optional) Set the task after this many seconds. Defaults to one. -- @param #number LastWaypoint (optional) Waypoint number of carrier group that when reached, ends the recovery tanker task. -- @return #GROUP self function GROUP:SetAsRecoveryTanker(CarrierGroup,Speed,ToKIAS,Altitude,Delay,LastWaypoint) local speed = ToKIAS == true and UTILS.KnotsToAltKIAS(Speed,Altitude) or Speed speed = UTILS.KnotsToMps(speed) local alt = UTILS.FeetToMeters(Altitude) local delay = Delay or 1 local task = self:TaskRecoveryTanker(CarrierGroup,speed,alt,LastWaypoint) self:SetTask(task,delay) local tankertask = self:EnRouteTaskTanker() self:PushTask(tankertask,delay+2) return self end