diff --git a/Moose Development/Moose/AI/AI_A2A.lua b/Moose Development/Moose/AI/AI_A2A.lua index c96fa4e43..2b3269b74 100644 --- a/Moose Development/Moose/AI/AI_A2A.lua +++ b/Moose Development/Moose/AI/AI_A2A.lua @@ -7,7 +7,7 @@ -- === -- -- @module AI.AI_A2A --- @image AI_Air_To_Air_Dispatching.JPG +-- @image MOOSE.JPG --BASE:TraceClass("AI_A2A") @@ -489,7 +489,7 @@ function AI_A2A:onafterStatus() not self:Is( "Fuel" ) and not self:Is( "Damaged" ) and not self:Is( "Home" ) then - if self.IdleCount >= 2 then + if self.IdleCount >= 3 then if Damage ~= InitialLife then self:Damaged() else diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index a884daf88..9b9370bb3 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -274,7 +274,7 @@ do -- AI_A2A_DISPATCHER -- A2ADispatcher_Red = AI_A2A_DISPATCHER:New( EWR_Red ) -- A2ADispatcher_Blue = AI_A2A_DISPATCHER:New( EWR_Blue ) -- - -- ### 2. Define the detected **target grouping radius**: + -- ### 1.2. Define the detected **target grouping radius**: -- -- The target grouping radius is a property of the Detection object, that was passed to the AI\_A2A\_DISPATCHER object, but can be changed. -- The grouping radius should not be too small, but also depends on the types of planes and the era of the simulation. diff --git a/Moose Development/Moose/AI/AI_A2A_Patrol.lua b/Moose Development/Moose/AI/AI_A2A_Patrol.lua index 7b44c14fb..7e57c24d0 100644 --- a/Moose Development/Moose/AI/AI_A2A_Patrol.lua +++ b/Moose Development/Moose/AI/AI_A2A_Patrol.lua @@ -345,7 +345,7 @@ function AI_A2A_PATROL:onafterRoute( AIPatrol, From, Event, To ) AIPatrol:OptionROEReturnFire() AIPatrol:OptionROTEvadeFire() - AIPatrol:Route( PatrolRoute, 0.5 ) + AIPatrol:Route( PatrolRoute, 0.5) end end diff --git a/Moose Development/Moose/AI/AI_A2G.lua b/Moose Development/Moose/AI/AI_A2G.lua new file mode 100644 index 000000000..b80eb8796 --- /dev/null +++ b/Moose Development/Moose/AI/AI_A2G.lua @@ -0,0 +1,69 @@ +--- **AI** -- Models the process of air to ground operations for airplanes and helicopters. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_A2G +-- @image AI_Air_To_Ground_Dispatching.JPG + +--- @type AI_A2G +-- @extends AI.AI_Air#AI_AIR + +--- The AI_A2G class implements the core functions to operate an AI @{Wrapper.Group} A2G tasking. +-- +-- +-- # 1) AI_A2G constructor +-- +-- * @{#AI_A2G.New}(): Creates a new AI_A2G object. +-- +-- # 2) AI_A2G is a Finite State Machine. +-- +-- This section must be read as follows. Each of the rows indicate a state transition, triggered through an event, and with an ending state of the event was executed. +-- The first column is the **From** state, the second column the **Event**, and the third column the **To** state. +-- +-- So, each of the rows have the following structure. +-- +-- * **From** => **Event** => **To** +-- +-- Important to know is that an event can only be executed if the **current state** is the **From** state. +-- This, when an **Event** that is being triggered has a **From** state that is equal to the **Current** state of the state machine, the event will be executed, +-- and the resulting state will be the **To** state. +-- +-- These are the different possible state transitions of this state machine implementation: +-- +-- * Idle => Start => Monitoring +-- +-- ## 2.1) AI_A2G States. +-- +-- * **Idle**: The process is idle. +-- +-- ## 2.2) AI_A2G Events. +-- +-- * **Start**: Start the transport process. +-- * **Stop**: Stop the transport process. +-- * **Monitor**: Monitor and take action. +-- +-- @field #AI_A2G +AI_A2G = { + ClassName = "AI_A2G", +} + +--- Creates a new AI_A2G process. +-- @param #AI_A2G self +-- @param Wrapper.Group#GROUP AIGroup The group object to receive the A2G Process. +-- @return #AI_A2G +function AI_A2G:New( AIGroup ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_AIR:New( AIGroup ) ) -- #AI_A2G + + self:SetFuelThreshold( .2, 60 ) + self:SetDamageThreshold( 0.95 ) + self:SetDisengageRadius( 70000 ) + + return self +end + diff --git a/Moose Development/Moose/AI/AI_A2G_BAI.lua b/Moose Development/Moose/AI/AI_A2G_BAI.lua new file mode 100644 index 000000000..efd48c397 --- /dev/null +++ b/Moose Development/Moose/AI/AI_A2G_BAI.lua @@ -0,0 +1,162 @@ +--- **AI** -- Models the process of air to ground BAI engagement for airplanes and helicopters. +-- +-- This is a class used in the @{AI_A2G_Dispatcher}. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_A2G_BAI +-- @image AI_Air_To_Ground_Engage.JPG + + + +--- @type AI_A2G_BAI +-- @extends AI.AI_A2A_Engage#AI_A2A_Engage + + +--- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders. +-- +-- === +-- +-- @field #AI_A2G_BAI +AI_A2G_BAI = { + ClassName = "AI_A2G_BAI", +} + + + +--- Creates a new AI_A2G_BAI object +-- @param #AI_A2G_BAI self +-- @param Wrapper.Group#GROUP AIGroup +-- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. +-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @return #AI_A2G_BAI +function AI_A2G_BAI:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_A2G_PATROL:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_A2G_BAI + + local RTBSpeedMax = AIGroup:GetSpeedMax() or 9999 + + self:SetRTBSpeed( RTBSpeedMax * 0.50, RTBSpeedMax * 0.75 ) + + return self +end + + +--- @param #AI_A2G_BAI self +-- @param Wrapper.Group#GROUP DefenderGroup The GroupGroup managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_BAI:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit ) + + self:F( { DefenderGroup, From, Event, To, AttackSetUnit} ) + + local DefenderGroupName = DefenderGroup:GetName() + + self.AttackSetUnit = AttackSetUnit or self.AttackSetUnit -- Core.Set#SET_UNIT + + local AttackCount = self.AttackSetUnit:Count() + + if AttackCount > 0 then + + if DefenderGroup:IsAlive() then + + local EngageAltitude = math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) + local EngageSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) + + -- Determine the distance to the target. + -- If it is less than 10km, then attack without a route. + -- Otherwise perform a route attack. + + local DefenderCoord = DefenderGroup:GetPointVec3() + DefenderCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. + + local TargetCoord = self.AttackSetUnit:GetFirst():GetPointVec3() + TargetCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. + + local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) + + local EngageRoute = {} + + --- Calculate the target route point. + + local FromWP = DefenderCoord:WaypointAir( + self.PatrolAltType or "RADIO", + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + EngageSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = FromWP + + self:SetTargetDistance( TargetCoord ) -- For RTB status check + + local FromEngageAngle = TargetCoord:GetAngleDegrees( TargetCoord:GetDirectionVec3( DefenderCoord ) ) + local EngageDistance = ( DefenderGroup:IsHelicopter() and 5000 ) or ( DefenderGroup:IsAirPlane() and 10000 ) + + --- Create a route point of type air. + local ToWP = TargetCoord:Translate( EngageDistance, FromEngageAngle ):WaypointAir( + self.PatrolAltType or "RADIO", + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + EngageSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = ToWP + + local AttackTasks = {} + + self.AttackSetUnit.AttackIndex = self.AttackSetUnit.AttackIndex and self.AttackSetUnit.AttackIndex + 1 or 1 + + local AttackSetUnitPerThreatLevel = self.AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) + + local AttackUnit = AttackSetUnitPerThreatLevel[self.AttackSetUnit.AttackIndex] + + if not AttackUnit then + self.AttackSetUnit.AttackIndex = 1 + AttackUnit = AttackSetUnitPerThreatLevel[self.AttackSetUnit.AttackIndex] + end + + if AttackUnit then + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + self:T( { "BAI Unit:", AttackUnit:GetName() } ) + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, false, false, nil, nil, EngageAltitude ) + end + end + + if #AttackTasks == 0 then + self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:Return() + self:__RTB( self.TaskDelay ) + else + DefenderGroup:OptionROEOpenFire() + DefenderGroup:OptionROTEvadeFire() + DefenderGroup:OptionKeepWeaponsOnThreat() + + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) + EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) + end + + DefenderGroup:Route( EngageRoute, self.TaskDelay ) + end + else + self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:Return() + self:__RTB( self.TaskDelay ) + end +end diff --git a/Moose Development/Moose/AI/AI_A2G_CAS.lua b/Moose Development/Moose/AI/AI_A2G_CAS.lua new file mode 100644 index 000000000..8d483f317 --- /dev/null +++ b/Moose Development/Moose/AI/AI_A2G_CAS.lua @@ -0,0 +1,159 @@ +--- **AI** -- Models the process of air to ground engagement for airplanes and helicopters. +-- +-- This is a class used in the @{AI_A2G_Dispatcher}. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_A2G_CAS +-- @image AI_Air_To_Ground_Engage.JPG + + + +--- @type AI_A2G_CAS +-- @extends AI.AI_A2G_Patrol#AI_A2G_PATROL + + +--- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders. +-- +-- === +-- +-- @field #AI_A2G_CAS +AI_A2G_CAS = { + ClassName = "AI_A2G_CAS", +} + + + +--- Creates a new AI_A2G_CAS object +-- @param #AI_A2G_CAS self +-- @param Wrapper.Group#GROUP AIGroup +-- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. +-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @return #AI_A2G_CAS +function AI_A2G_CAS:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_A2G_PATROL:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_A2G_CAS + + local RTBSpeedMax = AIGroup:GetSpeedMax() or 9999 + + self:SetRTBSpeed( RTBSpeedMax * 0.50, RTBSpeedMax * 0.75 ) + + return self +end + + +--- @param #AI_A2G_CAS self +-- @param Wrapper.Group#GROUP DefenderGroup The GroupGroup managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_CAS:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit ) + + self:F( { DefenderGroup, From, Event, To, AttackSetUnit} ) + + local DefenderGroupName = DefenderGroup:GetName() + + self.AttackSetUnit = AttackSetUnit or self.AttackSetUnit -- Core.Set#SET_UNIT + + local AttackCount = self.AttackSetUnit:Count() + + if AttackCount > 0 then + + if DefenderGroup:IsAlive() then + + local EngageAltitude = math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) + local EngageSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) + + local DefenderCoord = DefenderGroup:GetPointVec3() + DefenderCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. + + local TargetCoord = self.AttackSetUnit:GetFirst():GetPointVec3() + TargetCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. + + local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) + + local EngageRoute = {} + + --- Calculate the target route point. + + local FromWP = DefenderCoord:WaypointAir( + self.PatrolAltType or "RADIO", + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + EngageSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = FromWP + + self:SetTargetDistance( TargetCoord ) -- For RTB status check + + local FromEngageAngle = TargetCoord:GetAngleDegrees( TargetCoord:GetDirectionVec3( DefenderCoord ) ) + local EngageDistance = ( DefenderGroup:IsHelicopter() and 5000 ) or ( DefenderGroup:IsAirPlane() and 10000 ) + + --- Create a route point of type air. + local ToWP = TargetCoord:Translate( EngageDistance, FromEngageAngle, true ):WaypointAir( + self.PatrolAltType or "RADIO", + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + EngageSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = ToWP + + local AttackTasks = {} + + self.AttackSetUnit.AttackIndex = self.AttackSetUnit.AttackIndex and self.AttackSetUnit.AttackIndex + 1 or 1 + + local AttackSetUnitPerThreatLevel = self.AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) + + local AttackUnit = AttackSetUnitPerThreatLevel[self.AttackSetUnit.AttackIndex] + + if not AttackUnit then + self.AttackSetUnit.AttackIndex = 1 + AttackUnit = AttackSetUnitPerThreatLevel[self.AttackSetUnit.AttackIndex] + end + + if AttackUnit then + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + self:F( { "CAS Unit:", AttackUnit:GetName() } ) + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, false, false, nil, nil, EngageAltitude ) + end + end + + if #AttackTasks == 0 then + self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:Return() + self:__RTB( self.TaskDelay ) + else + DefenderGroup:OptionROEOpenFire() + DefenderGroup:OptionROTEvadeFire() + DefenderGroup:OptionKeepWeaponsOnThreat() + + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) + EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) + end + + DefenderGroup:Route( EngageRoute, self.TaskDelay ) + end + else + self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:Return() + self:__RTB( self.TaskDelay ) + end +end + diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua new file mode 100644 index 000000000..94beddcbc --- /dev/null +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -0,0 +1,4726 @@ +--- **AI** - Create an automated A2G defense system based on a detection network of reconnaissance vehicles and air units, coordinating SEAD, BAI and CAP operations. +-- +-- === +-- +-- Features: +-- +-- * Setup quickly an A2G defense system for a coalition. +-- * Setup multiple defense zones to defend specif points in your battlefield. +-- * Setup (SEAD) suppression of air defenses to enhance the control of enemy airspace. +-- * Setup (CAS) Controlled Air Support to attack approach enemy ground units. +-- * Setup (BAI) Battleground Air Interdiction to attack detected remote enemy ground units and targets. +-- * Define and use a detection network setup by recce. +-- * Define defense squadrons at airbases, farps and carriers. +-- * Enable airbases for A2G defenses. +-- * Add different planes and helicopter templates to different squadrons. +-- * Assign squadrons to execute a specific engagement type depending on threat level of the detected ground enemy unit composition. +-- * Add multiple squadrons to different airbases, farps or carriers. +-- * Define different ranges to engage upon. +-- * Establish an automatic in air refuel process for planes using refuel tankers. +-- * Setup default settings for all squadrons and A2G defenses. +-- * Setup specific settings for specific squadrons. +-- +-- === +-- +-- ## Missions: +-- +-- [AID-A2G - AI A2G Dispatching](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching/AID-A2G%20-%20AI%20A2G%20Dispatching) +-- +-- === +-- +-- ## YouTube Channel: +-- +-- [DCS WORLD - MOOSE - A2G GCICAP - Build an automatic A2G Defense System](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0S4KMNUUJpaUs6zZHjLKNx) +-- +-- === +-- +-- # QUICK START GUIDE +-- +-- The following class is available to model an A2G defense system. +-- +-- AI_A2G_DISPATCHER is the main A2G defense class that models the A2G defense system. +-- +-- Before you start using the AI_A2G_DISPATCHER, ask youself the following questions. +-- +-- +-- ## 1. Which coalition am I modeling an A2G defense system for? blue or red? +-- +-- One AI_A2G_DISPATCHER object can create a defense system for **one coalition**, which is blue or red. +-- If you want to create a **mutual defense system**, for both blue and red, then you need to instantiate **two** AI_A2G_DISPATCHER **objects**, +-- each governing their defense system for one coalition. +-- +-- +-- ## 2. Which type of detection will I setup? Grouping based per AREA, per TYPE or per UNIT? (Later others will follow). +-- +-- The MOOSE framework leverages the @{Functional.Detection} classes to perform the reconnaissance, detecting enemy units +-- and reporting them to the head quarters. +-- Several types of @{Functional.Detection} classes exist, and the most common characteristics of these classes is that they: +-- +-- * Perform detections from multiple recce as one co-operating entity. +-- * Communicate with a @{Tasking.CommandCenter}, which consolidates each detection. +-- * Groups detections based on a method (per area, per type or per unit). +-- * Communicates detections. +-- +-- +-- ## 3. Which recce units can be used as part of the detection system? Only ground based, or also airborne? +-- +-- Depending on the type of mission you want to achieve, different types of units can be engaged to perform ground enemy targets reconnaissance. +-- Ground recce (FAC) are very useful units to determine the position of enemy ground targets when they spread out over the battlefield at strategic positions. +-- Using their varying detection technology, and especially those ground units which have spotting technology, can be extremely effective at +-- detecting targets at great range. The terrain elevation characteristics are a big tool in making ground recce to be more effective. +-- Unfortunately, they lack sometimes the visibility to detect targets at greater range, or when scenery is preventing line of sight. +-- If you succeed to position recce at higher level terrain providing a broad and far overview of the lower terrain in the distance, then +-- the recce will be very effective at detecting approaching enemy targets. Therefore, always use the terrain very carefully! +-- +-- Airborne recce (AFAC) are also very effective. The are capable of patrolling at a functional detection altitude, +-- having an overview of the whole battlefield. However, airborne recce can be vulnerable to air to ground attacks, +-- so you need air superiority to make them effective. +-- Airborne recce will also have varying ground detection technology, which plays a big role in the effectiveness of the reconnaissance. +-- Certain helicopter or plane types have ground searching radars or advanced ground scanning technology, and are very effective +-- compared to air units having only visual detection capabilities. +-- For example, for the red coalition, the Mi-28N and the Su-34; and for the blue side, the reaper, are such effective airborne recce units. +-- +-- Typically, don't want these recce units to engage with the enemy, you want to keep them at position. Therefore, it is a good practice +-- to set the ROE for these recce to hold weapons, and make them invisible from the enemy. +-- +-- It is not possible to perform a recce function as a player (unit). +-- +-- +-- ## 4. How do the defenses decide **when and where to engage** on approaching enemy units? +-- +-- The A2G dispacher needs you to setup (various) defense coordinates, which are strategic positions in the battle field to be defended. +-- Any ground based enemy approaching within the proximity of such a defense point, may trigger for a defensive action by friendly air units. +-- +-- There are 2 important parameters that play a role in the defensive decision making: defensiveness and reactivity. +-- +-- The A2G dispatcher provides various parameters to setup the **defensiveness**, +-- which models the decision **when** a defender will engage with the approaching enemy. +-- Defensiveness is calculated by a probability distribution model when to trigger a defense action, +-- depending on the distance of the enemy unit from the defense coordinates, and a **defensiveness factor**. +-- +-- The other parameter considered for defensive action is **where the enemy is located**, thus the distance from a defense coordinate, +-- which we call the **reactive distance**. By default, the reactive distance is set to 60km, but can be changed by the mission designer +-- using the available method explained further below. +-- The combination of the defensiveness and reactivity results in a model that, the closer the attacker is to the defense point, +-- the higher the probability will be that a defense action will be launched! +-- +-- +-- ## 5. Are defense coordinates and defense reactivity the only parameters? +-- +-- No, depending on the target type, and the threat level of the target, the probability of defense will be higher. +-- In other words, when a SAM-10 radar emitter is detected, its probabilty for defense will be much higher than when a BMP-1 vehicle is +-- detected, even when both enemies are at the same distance from a defense coordinate. +-- This will ensure optimal defenses, SEAD tasks will be launched much more quicker against engaging radar emitters, to ensure air superiority. +-- Approaching main battle tanks will be engaged much faster, than a group of approaching trucks. +-- +-- +-- ## 6. Which Squadrons will I create and which name will I give each Squadron? +-- +-- The A2G defense system works with **Squadrons**. Each Squadron must be given a unique name, that forms the **key** to the squadron. +-- Several options and activities can be set per Squadron. A free format name can be given, but always ensure that the name is meaningfull +-- for your mission, and remember that squadron names are used for communication to the players of your mission. +-- +-- There are mainly 3 types of defenses: **SEAD**, **CAS** and **BAI**. +-- +-- Suppression of Air Defenses (SEAD) are effective agains radar emitters. Close Air Support (CAS) is launched when the enemy is close near friendly units. +-- Battleground Air Interdiction (BAI) tasks are launched when there are no friendlies around. +-- +-- Depending on the defense type, different payloads will be needed. See further points on squadron definition. +-- +-- +-- ## 7. Where will the Squadrons be located? On Airbases? On Carrier Ships? On Farps? +-- +-- Squadrons are placed at the **home base** on an **airfield**, **carrier** or **farp**. +-- Carefully plan where each Squadron will be located as part of the defense system required for mission effective defenses. +-- If the home base of the squadron is too far from assumed enemy positions, then the defenses will be too late. +-- The home bases must be **behind** enemy lines, you want to prevent your home bases to be engaged by enemies! +-- Depending on the units applied for defenses, the home base can be further or closer to the enemies. +-- Any airbase, farp or carrier can act as the launching platform for A2G defenses. +-- Carefully plan which airbases will take part in the coalition. Color each airbase **in the color of the coalition**, using the mission editor, +-- or your air units will not return for landing at the airbase! +-- +-- +-- ## 8. Which helicopter or plane models will I assign for each Squadron? Do I need one plane model or more plane models per squadron? +-- +-- Per Squadron, one or multiple helicopter or plane models can be allocated as **Templates**. +-- These are late activated groups with one airplane or helicopter that start with a specific name, called the **template prefix**. +-- The A2G defense system will select from the given templates a random template to spawn a new plane (group). +-- +-- A squadron will perform specific task types (SEAD, CAS or BAI). So, squadrons will require specific templates for the +-- task types it will perform. A squadron executing SEAD defenses, will require a payload with long range anti-radar seeking missiles. +-- +-- +-- ## 9. Which payloads, skills and skins will these plane models have? +-- +-- Per Squadron, even if you have one plane model, you can still allocate multiple templates of one plane model, +-- each having different payloads, skills and skins. +-- The A2G defense system will select from the given templates a random template to spawn a new plane (group). +-- +-- +-- ## 10. How to squadrons engage in a defensive action? +-- +-- There are two ways how squadrons engage and execute your A2G defenses. +-- Squadrons can start the defense directly from the airbase, farp or carrier. When a squadron launches a defensive group, that group +-- will start directly from the airbase. The other way is to launch early on in the mission a patrolling mechanism. +-- Squadrons will launch air units to patrol in specific zone(s), so that when ground enemy targets are detected, that the airborne +-- A2G defenses can come immediately into action. +-- +-- +-- ## 11. For each Squadron doing a patrol, which zone types will I create? +-- +-- Per zone, evaluate whether you want: +-- +-- * simple trigger zones +-- * polygon zones +-- * moving zones +-- +-- Depending on the type of zone selected, a different @{Zone} object needs to be created from a ZONE_ class. +-- +-- +-- ## 12. Are moving defense coordinates possible? +-- +-- Yes, different COORDINATE types are possible to be used. +-- The COORDINATE_UNIT will help you to specify a defense coodinate that is attached to a moving unit. +-- +-- +-- ## 13. How much defense coordinates do I need to create? +-- +-- It depends, but the idea is to define only the necessary defense points that drive your mission. +-- If you define too much defense points, the performance of your mission may decrease. Per defense point defined, +-- all the possible enemies are evaluated. Note that each defense coordinate has a reach depending on the size of the defense radius. +-- The default defense radius is about 60km, and depending on the defense reactivity, defenses will be launched when the enemy is at +-- close or greater distance from the defense coordinate. +-- +-- +-- ## 14. For each Squadron doing patrols, what are the time intervals and patrol amounts to be performed? +-- +-- For each patrol: +-- +-- * **How many** patrol you want to have airborne at the same time? +-- * **How frequent** you want the defense mechanism to check whether to start a new patrol? +-- +-- other considerations: +-- +-- * **How far** is the patrol area from the engagement "hot zone". You want to ensure that the enemy is reached on time! +-- * **How safe** is the patrol area taking into account air superiority. Is it well defended, are there nearby A2A bases? +-- +-- +-- ## 15. For each Squadron, which takeoff method will I use? +-- +-- For each Squadron, evaluate which takeoff method will be used: +-- +-- * Straight from the air +-- * From the runway +-- * From a parking spot with running engines +-- * From a parking spot with cold engines +-- +-- **The default takeoff method is staight in the air.** +-- This takeoff method is the most useful if you want to avoid airplane clutter at airbases! +-- But it is the least realistic one! +-- +-- +-- ## 16. For each Squadron, which landing method will I use? +-- +-- For each Squadron, evaluate which landing method will be used: +-- +-- * Despawn near the airbase when returning +-- * Despawn after landing on the runway +-- * Despawn after engine shutdown after landing +-- +-- **The default landing method is despawn when near the airbase when returning.** +-- This landing method is the most useful if you want to avoid airplane clutter at airbases! +-- But it is the least realistic one! +-- +-- +-- ## 19. For each Squadron, which **defense overhead** will I use? +-- +-- For each Squadron, depending on the helicopter or airplane type (modern, old) and payload, which overhead is required to provide any defense? +-- +-- In other words, if **X** enemy ground units are detected, how many **Y** defense helicpters or airplanes need to engage (per squadron)? +-- The **Y** is dependent on the type of airplane (era), payload, fuel levels, skills etc. +-- But the most important factor is the payload, which is the amount of A2G weapons the defense can carry to attack the enemy ground units. +-- For example, a Ka-50 can carry 16 vikrs, that means, that it potentially can destroy at least 8 ground units without a reload of ammunication. +-- That means, that one defender can destroy more enemy ground units. +-- Thus, the overhead is a **factor** that will calculate dynamically how many **Y** defenses will be required based on **X** attackers detected. +-- +-- **The default overhead is 1. A smaller value than 1, like 0.25 will decrease the overhead to a 1 / 4 ratio, meaning, +-- one defender for each 4 detected ground enemy units. ** +-- +-- +-- ## 19. For each Squadron, which grouping will I use? +-- +-- When multiple targets are detected, how will defenses be grouped when multiple defense air units are spawned for multiple enemy ground units? +-- Per one, two, three, four? +-- +-- **The default grouping is 1. That means, that each spawned defender will act individually.** +-- But you can specify a number between 1 and 4, so that the defenders will act as a group. +-- +-- === +-- +-- ### Author: **FlightControl** rework of GCICAP + introduction of new concepts (squadrons). +-- +-- @module AI.AI_A2G_Dispatcher +-- @image AI_Air_To_Ground_Dispatching.JPG + + + +do -- AI_A2G_DISPATCHER + + --- AI_A2G_DISPATCHER class. + -- @type AI_A2G_DISPATCHER + -- @extends Tasking.DetectionManager#DETECTION_MANAGER + + --- Create an automated A2G defense system based on a detection network of reconnaissance vehicles and air units, coordinating SEAD, BAI and CAP operations. + -- + -- === + -- + -- When your mission is in the need to take control of the AI to automate and setup a process of air to ground defenses, this is the module you need. + -- The defense system work through the definition of defense coordinates, which are points in your friendly area within the battle field, that your mission need to have defended. + -- Multiple defense coordinates can be setup. Defense coordinates can be strategic or tactical positions or references to strategic units or scenery. + -- The A2G dispatcher will evaluate every x seconds the tactical situation around each defense coordinate. When a defense coordinate + -- is under threat, it will communicate through the command center that defensive actions need to be taken and will launch groups of air units for defense. + -- The level of threat to the defense coordinate varyies upon the strength and types of the enemy units, the distance to the defense point, and the defensiveness parameters. + -- Defensive actions are taken through probability, but the closer and the more threat the enemy poses to the defense coordinate, the faster it will be attacked by friendly A2G units. + -- + -- Please study carefully the underlying explanations how to setup and use this module, as it has many features. + -- It also requires a little study to ensure that you get a good understanding of the defense mechanisms, to ensure a strong + -- defense for your missions. + -- + -- === + -- + -- # USAGE GUIDE + -- + -- ## 1. AI\_A2G\_DISPATCHER constructor: + -- + -- ![Banner Image](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_DISPATCHER-ME_1.JPG) + -- + -- + -- The @{#AI_A2G_DISPATCHER.New}() method creates a new AI_A2G_DISPATCHER instance. + -- + -- ### 1.1. Define the **reconnaissance network**: + -- + -- As part of the AI_A2G_DISPATCHER :New() constructor, a reconnaissance network must be given as the first parameter. + -- A reconnaissance network is provide through an instance of a @{Functional.Detection} network. + -- The most effective reconnaissance for the A2G dispatcher would be to use the @{Functional.Detection#DETECTION_AREAS} object. + -- + -- An reconnaissance network, is used to detect enemy ground targets, potentially group them into areas, and to understand the position, level of threat of the enemy. + -- + -- ![Banner Image](..\Presentations\AI_A2G_DISPATCHER\Dia5.JPG) + -- + -- As explained in the introduction, depending on the type of mission you want to achieve, different types of units can be applied to detect ground enemy targets. + -- Ground based units are very useful to act as a reconnaissance, but they lack sometimes the visibility to detect targets at greater range. + -- Recce are very useful to acquire the position of enemy ground targets when spread out over the battlefield at strategic positions. + -- Ground units also have varying detectors, and especially the ground units which have laser guiding missiles can be extremely effective at + -- detecting targets at great range. The terrain elevation characteristics are a big tool in making ground recce to be more effective. + -- If you succeed to position recce at higher level terrain providing a broad and far overview of the lower terrain in the distance, then + -- the recce will be very effective at detecting approaching enemy targets. Therefore, always use the terrain very carefully! + -- + -- Beside ground level units to use for reconnaissance, air units are also very effective. The are capable of patrolling at great speed + -- covering a large terrain. However, airborne recce can be vulnerable to air to ground attacks, and you need air superiority to make then + -- effective. Also the instruments available at the air units play a big role in the effectiveness of the reconnaissance. + -- Air units which have ground detection capabilities will be much more effective than air units with only visual detection capabilities. + -- For the red coalition, the Mi-28N and for the blue side, the reaper are such effective reconnaissance airborne units. + -- + -- Reconnaissance networks are **dynamically constructed**, that is, they form part of the @{Functional.Detection} instance that is given as the first parameter to the A2G dispatcher. + -- By defining in a **smart way the names or name prefixes of the reconnaissance groups**, these groups will be **automatically added or removed** to or from the reconnaissance network, + -- when these groups are spawned in or destroyed during the ongoing battle. + -- By spawning in dynamically additional recce, you can ensure that there is sufficient reconnaissance coverage so the defense mechanism is continuously + -- alerted of new enemy ground targets. + -- + -- The following example defens a new reconnaissance network using a @{Functional.Detection#DETECTION_AREAS} object. + -- + -- -- Define a SET_GROUP object that builds a collection of groups that define the recce network. + -- -- Here we build the network with all the groups that have a name starting with CCCP Recce. + -- DetectionSetGroup = SET_GROUP:New() -- Defene a set of group objects, caled DetectionSetGroup. + -- + -- DetectionSetGroup:FilterPrefixes( { "CCCP Recce" } ) -- The DetectionSetGroup will search for groups that start with the name "CCCP Recce". + -- + -- -- This command will start the dynamic filtering, so when groups spawn in or are destroyed, + -- -- which have a group name starting with "CCCP Recce", then these will be automatically added or removed from the set. + -- DetectionSetGroup:FilterStart() + -- + -- -- This command defines the reconnaissance network. + -- -- It will group any detected ground enemy targets within a radius of 1km. + -- -- It uses the DetectionSetGroup, which defines the set of reconnaissance groups to detect for enemy ground targets. + -- Detection = DETECTION_AREAS:New( DetectionSetGroup, 1000 ) + -- + -- -- Setup the A2A dispatcher, and initialize it. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- + -- The above example creates a SET_GROUP instance, and stores this in the variable (object) **DetectionSetGroup**. + -- **DetectionSetGroup** is then being configured to filter all active groups with a group name starting with `"CCCP Recce"` to be included in the set. + -- **DetectionSetGroup** is then calling `FilterStart()`, which is starting the dynamic filtering or inclusion of these groups. + -- Note that any destroy or new spawn of a group having a name, starting with the above prefix, will be removed or added to the set. + -- + -- Then a new detection object is created from the class `DETECTION_AREAS`. A grouping radius of 1000 meters (1km) is choosen. + -- + -- The `Detection` object is then passed to the @{#AI_A2G_DISPATCHER.New}() method to indicate the reconnaissance network + -- configuration and setup the A2G defense detection mechanism. + -- + -- ### 1.2. Setup the A2G dispatcher for both a red and blue coalition. + -- + -- Following the above described procedure, you'll need to create for each coalition an separate detection network, and a separate A2G dispatcher. + -- Ensure that while doing so, that you name the objects differently both for red and blue coalition. + -- + -- For example like this for the red coalition: + -- + -- DetectionRed = DETECTION_AREAS:New( DetectionSetGroupRed, 1000 ) + -- A2GDispatcherRed = AI_A2G_DISPATCHER:New( DetectionRed ) + -- + -- And for the blue coalition: + -- + -- DetectionBlue = DETECTION_AREAS:New( DetectionSetGroupBlue, 1000 ) + -- A2GDispatcherBlue = AI_A2G_DISPATCHER:New( DetectionBlue ) + -- + -- + -- Note: Also the SET_GROUP objects should be created for each coalition separately, containing each red and blue recce respectively! + -- + -- ### 1.3. Define the enemy ground target **grouping radius**, in case you use DETECTION_AREAS: + -- + -- The target grouping radius is a property of the DETECTION_AREAS class, that was passed to the AI_A2G_DISPATCHER:New() method, + -- but can be changed. The grouping radius should not be too small, but also depends on the types of ground forces and the way you want your mission to evolve. + -- A large radius will mean large groups of enemy ground targets, while making smaller groups will result in a more fragmented defense system. + -- Typically I suggest a grouping radius of 1km. This is the right balance to create efficient defenses. + -- + -- Note that detected targets are constantly re-grouped, that is, when certain detected enemy ground units are moving further than the group radius, + -- then these units will become a separate area being detected. This may result in additional defenses being started by the dispatcher! + -- So don't make this value too small! Again, I advise about 1km or 1000 meters. + -- + -- ## 2. Setup (a) **Defense Coordinate(s)**. + -- + -- As explained above, defense coordinates are the center of your defense operations. + -- The more threat to the defense coordinate, the higher it is likely a defensive action will be launched. + -- + -- Find below an example how to add defense coordinates: + -- + -- -- Add defense coordinates. + -- A2GDispatcher:AddDefenseCoordinate( "HQ", GROUP:FindByName( "HQ" ):GetCoordinate() ) + -- + -- In this example, the coordinate of a group called `"HQ"` is retrieved, using `:GetCoordinate()` + -- This returns a COORDINATE object, pointing to the first unit within the GROUP object. + -- + -- The method @{#AI_A2G_DISPATCHER.AddDefenseCoordinate}() adds a new defense coordinate to the `A2GDispatcher` object. + -- The first parameter is the key of the defense coordinate, the second the coordinate itself. + -- + -- Later, a COORDINATE_UNIT will be added to the framework, which can be used to assign "moving" coordinates to an A2G dispatcher. + -- + -- **REMEMBER!** + -- + -- - **Defense coordinates are the center of the A2G dispatcher defense system!** + -- - **You can define more defense coordinates to defend a larger area.** + -- - **Detected enemy ground targets are not immediately engaged, but are engaged with a reactivity or probability calculation!** + -- + -- But, there is more to it ... + -- + -- + -- ### 2.1. The **Defense Radius**. + -- + -- The defense radius defines the maximum radius that a defense will be initiated around each defense coordinate. + -- So even when there are targets further away than the defense radius, then these targets won't be engaged upon. + -- By default, the defense radius is set to 100km (100.000 meters), but can be changed using the @{#AI_A2G_DISPATCHER.SetDefenseRadius}() method. + -- Note that the defense radius influences the defense reactivity also! The larger the defense radius, the more reactive the defenses will be. + -- + -- For example: + -- + -- A2GDispatcher:SetDefenseRadius( 30000 ) + -- + -- This defines an A2G dispatcher which will engage on enemy ground targets within 30km radius around the defense coordinate. + -- Note that the defense radius **applies to all defense coordinates** defined within the A2G dispatcher. + -- + -- ### 2.2. The **Defense Reactivity**. + -- + -- There are 5 levels that can be configured to tweak the defense reactivity. As explained above, the threat to a defense coordinate is + -- also determined by the distance of the enemy ground target to the defense coordinate. + -- If you want to have a **low** defense reactivity, that is, the probability that an A2G defense will engage to the enemy ground target, then + -- use the @{#AI_A2G_DISPATCHER.SetDefenseReactivityLow}() method. For medium and high reactivity, use the methods + -- @{#AI_A2G_DISPATCHER.SetDefenseReactivityMedium}() and @{#AI_A2G_DISPATCHER.SetDefenseReactivityHigh}() respectively. + -- + -- Note that the reactivity of defenses is always in relation to the Defense Radius! the shorter the distance, + -- the less reactive the defenses will be in terms of distance to enemy ground targets! + -- + -- For example: + -- + -- A2GDispatcher:SetDefenseReactivityHigh() + -- + -- This defines an A2G dispatcher with high defense reactivity. + -- + -- ## 3. **Squadrons**. + -- + -- The A2G dispatcher works with **Squadrons**, that need to be defined using the different methods available. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetSquadron}() to **setup a new squadron** active at an airfield, farp or carrier, + -- while defining which helicopter or plane **templates** are being used by the squadron and how many **resources** are available. + -- + -- **Multiple squadrons** can be defined within one A2G dispatcher, each having specific defense tasks and defense parameter settings! + -- + -- Squadrons: + -- + -- * Have name (string) that is the identifier or **key** of the squadron. + -- * Have specific helicopter or plane **templates**. + -- * Are located at **one** airbase, farp or carrier. + -- * Optionally have a **limited set of resources**. The default is that squadrons have **unlimited resources**. + -- + -- The name of the squadron given acts as the **squadron key** in all `A2GDispatcher:SetSquadron...()` or `A2GDispatcher:GetSquadron...()` methods. + -- + -- Additionally, squadrons have specific configuration options to: + -- + -- * Control how new helicopters or aircraft are taking off from the airfield, farp or carrier (in the air, cold, hot, at the runway). + -- * Control how returning helicopters or aircraft are landing at the airfield, farp or carrier (in the air near the airbase, after landing, after engine shutdown). + -- * Control the **grouping** of new helicopters or aircraft spawned at the airfield, farp or carrier. If there is more than one helicopter or aircraft to be spawned, these may be grouped. + -- * Control the **overhead** or defensive strength of the squadron. Depending on the types of helicopters, planes, amount of resources and payload (weapon configuration) chosen, + -- the mission designer can choose to increase or reduce the amount of planes spawned. + -- + -- The method @{#AI_A2G_DISPATCHER.SetSquadron}() defines for you a new squadron. + -- The provided parameters are the squadron name, airbase name and a list of template prefixe, and a number that indicates the amount of resources. + -- + -- For example, this defines 3 new squadrons: + -- + -- A2GDispatcher:SetSquadron( "Maykop SEAD", AIRBASE.Caucasus.Maykop_Khanskaya, { "CCCP KA-50" }, 10 ) + -- A2GDispatcher:SetSquadron( "Maykop CAS", "CAS", { "CCCP KA-50" }, 10 ) + -- A2GDispatcher:SetSquadron( "Maykop BAI", "BAI", { "CCCP KA-50" }, 10 ) + -- + -- The latter 2 will depart from FARPs, which bare the name `"CAS"` and `"BAI"`. + -- + -- + -- ### 3.1. Squadrons **Tasking**. + -- + -- Squadrons can be commanded to execute 3 types of tasks, as explained above: + -- + -- - SEAD: Suppression of Air Defenses, which are ground targets that have medium or long range radar emitters. + -- - CAS : Close Air Support, when there are enemy ground targets close to friendly units. + -- - BAI : Battlefield Air Interdiction, which are targets further away from the frond-line. + -- + -- You need to configure each squadron which task types you want it to perform. Read on ... + -- + -- ### 3.2. Squadrons enemy ground target **engagement types**. + -- + -- There are two ways how targets can be engaged: directly **on call** from the airfield, farp or carrier, or through a **patrol**. + -- + -- Patrols are extremely handy, as these will airborne your helicopters or airplanes in advance. They will patrol in defined zones outlined, + -- and will engage with the targets once commanded. If the patrol zone is close enough to the enemy ground targets, then the time required + -- to engage is heavily minimized! + -- + -- However; patrols come with a side effect: since your resources are airborne, they will be vulnerable to incoming air attacks from the enemy. + -- + -- The mission designer needs to carefully balance the need for patrols or the need for engagement on call from the airfields. + -- + -- ### 3.3. Squadron **on call** engagement. + -- + -- So to make squadrons engage targets from the airfields, use the following methods: + -- + -- - For SEAD, use the @{#AI_A2G_DISPATCHER.SetSquadronSead}() method. + -- - For CAS, use the @{#AI_A2G_DISPATCHER.SetSquadronCas}() method. + -- - For BAI, use the @{#AI_A2G_DISPATCHER.SetSquadronBai}() method. + -- + -- Note that for the tasks, specific helicopter or airplane templates are required to be used, which you can configure using your mission editor. + -- Especially the payload (weapons configuration) is important to get right. + -- + -- For example, the following will define for the squadrons different tasks: + -- + -- A2GDispatcher:SetSquadron( "Maykop SEAD", AIRBASE.Caucasus.Maykop_Khanskaya, { "CCCP KA-50 SEAD" }, 10 ) + -- A2GDispatcher:SetSquadronSead( "Maykop SEAD", 120, 250 ) + -- + -- A2GDispatcher:SetSquadron( "Maykop CAS", "CAS", { "CCCP KA-50 CAS" }, 10 ) + -- A2GDispatcher:SetSquadronCas( "Maykop CAS", 120, 250 ) + -- + -- A2GDispatcher:SetSquadron( "Maykop BAI", "BAI", { "CCCP KA-50 BAI" }, 10 ) + -- A2GDispatcher:SetSquadronBai( "Maykop BAI", 120, 250 ) + -- + -- ### 3.4. Squadron **on patrol engagement**. + -- + -- Squadrons can be setup to patrol in the air near the engagement hot zone. + -- When needed, the A2G defense units will be close to the battle area, and can engage quickly. + -- + -- So to make squadrons engage targets from a patrol zone, use the following methods: + -- + -- - For SEAD, use the @{#AI_A2G_DISPATCHER.SetSquadronSeadPatrol}() method. + -- - For CAS, use the @{#AI_A2G_DISPATCHER.SetSquadronCasPatrol}() method. + -- - For BAI, use the @{#AI_A2G_DISPATCHER.SetSquadronBaiPatrol}() method. + -- + -- Because a patrol requires more parameters, the following methods must be used to fine-tune the patrols for each squadron. + -- + -- - For SEAD, use the @{#AI_A2G_DISPATCHER.SetSquadronSeadPatrolInterval}() method. + -- - For CAS, use the @{#AI_A2G_DISPATCHER.SetSquadronCasPatrolInterval}() method. + -- - For BAI, use the @{#AI_A2G_DISPATCHER.SetSquadronBaiPatrolInterval}() method. + -- + -- Here an example to setup patrols of various task types: + -- + -- A2GDispatcher:SetSquadron( "Maykop SEAD", AIRBASE.Caucasus.Maykop_Khanskaya, { "CCCP KA-50 SEAD" }, 10 ) + -- A2GDispatcher:SetSquadronSeadPatrol( "Maykop SEAD", PatrolZone, 300, 500, 50, 80, 250, 300 ) + -- A2GDispatcher:SetSquadronPatrolInterval( "Maykop SEAD", 2, 30, 60, 1, "SEAD" ) + -- + -- A2GDispatcher:SetSquadron( "Maykop CAS", "CAS", { "CCCP KA-50 CAS" }, 10 ) + -- A2GDispatcher:SetSquadronCasPatrol( "Maykop CAS", PatrolZone, 600, 700, 50, 80, 250, 300 ) + -- A2GDispatcher:SetSquadronPatrolInterval( "Maykop CAS", 2, 30, 60, 1, "CAS" ) + -- + -- A2GDispatcher:SetSquadron( "Maykop BAI", "BAI", { "CCCP KA-50 BAI" }, 10 ) + -- A2GDispatcher:SetSquadronBaiPatrol( "Maykop BAI", PatrolZone, 800, 900, 50, 80, 250, 300 ) + -- A2GDispatcher:SetSquadronPatrolInterval( "Maykop BAI", 2, 30, 60, 1, "BAI" ) + -- + -- + -- ### 3.5. Set squadron take-off methods + -- + -- Use the various SetSquadronTakeoff... methods to control how squadrons are taking-off from the home airfield, FARP or ship. + -- + -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoff}() is the generic configuration method to control takeoff from the air, hot, cold or from the runway. See the method for further details. + -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffInAir}() will spawn new aircraft from the squadron directly in the air. + -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffFromParkingCold}() will spawn new aircraft in without running engines at a parking spot at the airfield. + -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffFromParkingHot}() will spawn new aircraft in with running engines at a parking spot at the airfield. + -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffFromRunway}() will spawn new aircraft at the runway at the airfield. + -- + -- **The default landing method is to spawn new aircraft directly in the air.** + -- + -- Use these methods to fine-tune for specific airfields that are known to create bottlenecks, or have reduced airbase efficiency. + -- The more and the longer aircraft need to taxi at an airfield, the more risk there is that: + -- + -- * aircraft will stop waiting for each other or for a landing aircraft before takeoff. + -- * aircraft may get into a "dead-lock" situation, where two aircraft are blocking each other. + -- * aircraft may collide at the airbase. + -- * aircraft may be awaiting the landing of a plane currently in the air, but never lands ... + -- + -- Currently within the DCS engine, the airfield traffic coordination is erroneous and contains a lot of bugs. + -- If you experience while testing problems with aircraft take-off or landing, please use one of the above methods as a solution to workaround these issues! + -- + -- This example sets the default takeoff method to be from the runway. + -- And for a couple of squadrons overrides this default method. + -- + -- -- Setup the Takeoff methods + -- + -- -- The default takeoff + -- A2ADispatcher:SetDefaultTakeOffFromRunway() + -- + -- -- The individual takeoff per squadron + -- A2ADispatcher:SetSquadronTakeoff( "Mineralnye", AI_A2G_DISPATCHER.Takeoff.Air ) + -- A2ADispatcher:SetSquadronTakeoffInAir( "Sochi" ) + -- A2ADispatcher:SetSquadronTakeoffFromRunway( "Mozdok" ) + -- A2ADispatcher:SetSquadronTakeoffFromParkingCold( "Maykop" ) + -- A2ADispatcher:SetSquadronTakeoffFromParkingHot( "Novo" ) + -- + -- + -- ### 3.5.1. Set Squadron takeoff altitude when spawning new aircraft in the air. + -- + -- In the case of the @{#AI_A2G_DISPATCHER.SetSquadronTakeoffInAir}() there is also an other parameter that can be applied. + -- That is modifying or setting the **altitude** from where planes spawn in the air. + -- Use the method @{#AI_A2G_DISPATCHER.SetSquadronTakeoffInAirAltitude}() to set the altitude for a specific squadron. + -- The default takeoff altitude can be modified or set using the method @{#AI_A2G_DISPATCHER.SetSquadronTakeoffInAirAltitude}(). + -- As part of the method @{#AI_A2G_DISPATCHER.SetSquadronTakeoffInAir}() a parameter can be specified to set the takeoff altitude. + -- If this parameter is not specified, then the default altitude will be used for the squadron. + -- + -- ### 3.5.2. Set Squadron takeoff interval. + -- + -- The different types of available airfields have different amounts of available launching platforms: + -- + -- - Airbases typically have a lot of platforms. + -- - FARPs have 4 platforms. + -- - Ships have 2 to 4 platforms. + -- + -- Depending on the demand of requested takeoffs by the A2G dispatcher, an airfield can become overloaded. Too many aircraft need to be taken + -- off at the same time, which will result in clutter as described above. In order to better control this behaviour, a takeoff scheduler is implemented, + -- which can be used to control how many aircraft are ordered for takeoff between specific time intervals. + -- The takeff intervals can be specified per squadron, which make sense, as each squadron have a "home" airfield. + -- + -- For this purpose, the method @{#AI_A2G_DISPATCHER.SetSquadronTakeOffInterval}() can be used to specify the takeoff intervals of + -- aircraft groups per squadron to avoid cluttering of aircraft at airbases. + -- This is especially useful for FARPs and ships. Each takeoff dispatch is queued by the dispatcher and when the interval time + -- has been reached, a new group will be spawned or activated for takeoff. + -- + -- The interval needs to be estimated, and depends on the time needed for the aircraft group to actually depart from the launch platform, and + -- the way how the aircraft are starting up. Cold starts take the longest duration, hot starts a few seconds, and runway takeoff also a few seconds for FARPs and ships. + -- + -- See the underlying example: + -- + -- -- Imagine a squadron launched from a FARP, with a grouping of 4. + -- -- Aircraft will cold start from the FARP, and thus, a maximum of 4 aircraft can be launched at the same time. + -- -- Additionally, depending on the group composition of the aircraft, defending units will be ordered for takeoff together. + -- -- It takes about 3 to 4 minutes to takeoff helicopters from FARPs in cold start. + -- A2ADispatcher:SetSquadronTakeOffInterval( "Mineralnye", 60 * 4 ) + -- + -- + -- ### 3.6. Set squadron landing methods + -- + -- In analogy with takeoff, the landing methods are to control how squadrons land at the airfield: + -- + -- * @{#AI_A2G_DISPATCHER.SetSquadronLanding}() is the generic configuration method to control landing, namely despawn the aircraft near the airfield in the air, right after landing, or at engine shutdown. + -- * @{#AI_A2G_DISPATCHER.SetSquadronLandingNearAirbase}() will despawn the returning aircraft in the air when near the airfield. + -- * @{#AI_A2G_DISPATCHER.SetSquadronLandingAtRunway}() will despawn the returning aircraft directly after landing at the runway. + -- * @{#AI_A2G_DISPATCHER.SetSquadronLandingAtEngineShutdown}() will despawn the returning aircraft when the aircraft has returned to its parking spot and has turned off its engines. + -- + -- You can use these methods to minimize the airbase coodination overhead and to increase the airbase efficiency. + -- When there are lots of aircraft returning for landing, at the same airbase, the takeoff process will be halted, which can cause a complete failure of the + -- A2A defense system, as no new CAP or GCI planes can takeoff. + -- Note that the method @{#AI_A2G_DISPATCHER.SetSquadronLandingNearAirbase}() will only work for returning aircraft, not for damaged or out of fuel aircraft. + -- Damaged or out-of-fuel aircraft are returning to the nearest friendly airbase and will land, and are out of control from ground control. + -- + -- This example defines the default landing method to be at the runway. + -- And for a couple of squadrons overrides this default method. + -- + -- -- Setup the Landing methods + -- + -- -- The default landing method + -- A2ADispatcher:SetDefaultLandingAtRunway() + -- + -- -- The individual landing per squadron + -- A2ADispatcher:SetSquadronLandingAtRunway( "Mineralnye" ) + -- A2ADispatcher:SetSquadronLandingNearAirbase( "Sochi" ) + -- A2ADispatcher:SetSquadronLandingAtEngineShutdown( "Mozdok" ) + -- A2ADispatcher:SetSquadronLandingNearAirbase( "Maykop" ) + -- A2ADispatcher:SetSquadronLanding( "Novo", AI_A2G_DISPATCHER.Landing.AtRunway ) + -- + -- + -- ### 3.7. Set squadron **grouping**. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetSquadronGrouping}() to set the grouping of aircraft when spawned in. + -- + -- ![Banner Image](..\Presentations\AI_A2G_DISPATCHER\Dia12.JPG) + -- + -- In the case of **on call** engagement, the @{#AI_A2G_DISPATCHER.SetSquadronGrouping}() method has additional behaviour. + -- When there aren't enough patrol flights airborne, a on call will be initiated for the remaining + -- targets to be engaged. Depending on the grouping parameter, the spawned flights for on call aircraft are grouped into this setting. + -- For example with a group setting of 2, if 3 targets are detected and cannot be engaged by the available patrols or any airborne flight, + -- an additional on call flight needs to be started. + -- + -- The **grouping value is set for a Squadron**, and can be **dynamically adjusted** during mission execution, so to adjust the defense flights grouping when the tactical situation changes. + -- + -- ### 3.8. Set the squadron **overhead** to balance the effectiveness of the A2G defenses. + -- + -- The effectiveness can be set with the **overhead parameter**. This is a number that is used to calculate the amount of Units that dispatching command will allocate to GCI in surplus of detected amount of units. + -- The **default value** of the overhead parameter is 1.0, which means **equal balance**. + -- + -- ![Banner Image](..\Presentations\AI_A2G_DISPATCHER\Dia11.JPG) + -- + -- However, depending on the (type of) aircraft (strength and payload) in the squadron and the amount of resources available, this parameter can be changed. + -- + -- The @{#AI_A2G_DISPATCHER.SetSquadronOverhead}() method can be used to tweak the defense strength, + -- taking into account the plane types of the squadron. + -- + -- For example, a A-10C with full long-distance A2G missiles payload, may still be less effective than a Su-23 with short range A2G missiles... + -- So in this case, one may want to use the @{#AI_A2G_DISPATCHER.SetOverhead}() method to allocate more defending planes as the amount of detected attacking ground units. + -- The overhead must be given as a decimal value with 1 as the neutral value, which means that overhead values: + -- + -- * Higher than 1.0, for example 1.5, will increase the defense unit amounts. For 4 attacking ground units detected, 6 aircraft will be spawned. + -- * Lower than 1, for example 0.75, will decrease the defense unit amounts. For 4 attacking ground units detected, only 3 aircraft will be spawned. + -- + -- The amount of defending units is calculated by multiplying the amount of detected attacking ground units as part of the detected group + -- multiplied by the overhead parameter, and rounded up to the smallest integer. + -- + -- Typically, for A2G defenses, values small than 1 will be used. Here are some good values for a couple of aircraft to support CAS operations: + -- + -- - A-10C: 0.15 + -- - Su-34: 0.15 + -- - A-10A: 0.25 + -- - SU-25T: 0.10 + -- + -- So generically, the amount of missiles that an aircraft can take will determine its attacking effectiveness. The longer the range of the missiles, + -- the less risk that the defender may be destroyed by the enemy, thus, the less aircraft needs to be activated in a defense. + -- + -- The **overhead value is set for a Squadron**, and can be **dynamically adjusted** during mission execution, so to adjust the defense overhead when the tactical situation changes. + -- + -- ### 3.8. Set the squadron **engage limit**. + -- + -- To limit the amount of aircraft to defend against a large group of intruders, an **engage limit** can be defined per squadron. + -- This limit will avoid an extensive amount of aircraft to engage with the enemy if the attacking ground forces are enormous. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetSquadronEngageLimit}() to limit the amount of aircraft that will engage with the enemy, per squadron. + -- + -- ## 4. Set the **fuel treshold**. + -- + -- When aircraft get **out of fuel** to a certain %-tage, which is by default **15% (0.15)**, there are two possible actions that can be taken: + -- - The aircraft will go RTB, and will be replaced with a new aircraft if possible. + -- - The aircraft will refuel at a tanker, if a tanker has been specified for the squadron. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetSquadronFuelThreshold}() to set the **squadron fuel treshold** of the aircraft for all squadrons. + -- + -- ## 6. Other configuration options + -- + -- ### 6.1. Set a tactical display panel. + -- + -- Every 30 seconds, a tactical display panel can be shown that illustrates what the status is of the different groups controlled by AI_A2G_DISPATCHER. + -- Use the method @{#AI_A2G_DISPATCHER.SetTacticalDisplay}() to switch on the tactical display panel. The default will not show this panel. + -- Note that there may be some performance impact if this panel is shown. + -- + -- ## 10. Default settings. + -- + -- Default settings configure the standard behaviour of the squadrons. + -- This section a good overview of the different parameters that setup the behaviour of **ALL** the squadrons by default. + -- Note that default behaviour can be tweaked, and thus, this will change the behaviour of all the squadrons. + -- Unless there is a specific behaviour set for a specific squadron, the default configured behaviour will be followed. + -- + -- ## 10.1. Default **takeoff** behaviour. + -- + -- The default takeoff behaviour is set to **in the air**, which means that new spawned aircraft will be spawned directly in the air above the airbase by default. + -- + -- **The default takeoff method can be set for ALL squadrons that don't have an individual takeoff method configured.** + -- + -- * @{#AI_A2G_DISPATCHER.SetDefaultTakeoff}() is the generic configuration method to control takeoff by default from the air, hot, cold or from the runway. See the method for further details. + -- * @{#AI_A2G_DISPATCHER.SetDefaultTakeoffInAir}() will spawn by default new aircraft from the squadron directly in the air. + -- * @{#AI_A2G_DISPATCHER.SetDefaultTakeoffFromParkingCold}() will spawn by default new aircraft in without running engines at a parking spot at the airfield. + -- * @{#AI_A2G_DISPATCHER.SetDefaultTakeoffFromParkingHot}() will spawn by default new aircraft in with running engines at a parking spot at the airfield. + -- * @{#AI_A2G_DISPATCHER.SetDefaultTakeoffFromRunway}() will spawn by default new aircraft at the runway at the airfield. + -- + -- ## 10.2. Default landing behaviour. + -- + -- The default landing behaviour is set to **near the airbase**, which means that returning airplanes will be despawned directly in the air by default. + -- + -- The default landing method can be set for ALL squadrons that don't have an individual landing method configured. + -- + -- * @{#AI_A2G_DISPATCHER.SetDefaultLanding}() is the generic configuration method to control by default landing, namely despawn the aircraft near the airfield in the air, right after landing, or at engine shutdown. + -- * @{#AI_A2G_DISPATCHER.SetDefaultLandingNearAirbase}() will despawn by default the returning aircraft in the air when near the airfield. + -- * @{#AI_A2G_DISPATCHER.SetDefaultLandingAtRunway}() will despawn by default the returning aircraft directly after landing at the runway. + -- * @{#AI_A2G_DISPATCHER.SetDefaultLandingAtEngineShutdown}() will despawn by default the returning aircraft when the aircraft has returned to its parking spot and has turned off its engines. + -- + -- ## 10.3. Default **overhead**. + -- + -- The default overhead is set to **0.25**. That essentially means that for each 4 ground enemies there will be 1 aircraft dispatched. + -- + -- The default overhead value can be set for ALL squadrons that don't have an individual overhead value configured. + -- + -- Use the @{#AI_A2G_DISPATCHER.SetDefaultOverhead}() method can be used to set the default overhead or defense strength for ALL squadrons. + -- + -- ## 10.4. Default **grouping**. + -- + -- The default grouping is set to **one airplane**. That essentially means that there won't be any grouping applied by default. + -- + -- The default grouping value can be set for ALL squadrons that don't have an individual grouping value configured. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultGrouping}() to set the **default grouping** of spawned airplanes for all squadrons. + -- + -- ## 10.5. Default RTB fuel treshold. + -- + -- When an airplane gets **out of fuel** to a certain %-tage, which is **15% (0.15)**, it will go RTB, and will be replaced with a new airplane when applicable. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultFuelThreshold}() to set the **default fuel treshold** of spawned airplanes for all squadrons. + -- + -- ## 10.6. Default RTB damage treshold. + -- + -- When an airplane is **damaged** to a certain %-tage, which is **40% (0.40)**, it will go RTB, and will be replaced with a new airplane when applicable. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultDamageThreshold}() to set the **default damage treshold** of spawned airplanes for all squadrons. + -- + -- ## 10.7. Default settings for **patrol**. + -- + -- ### 10.7.1. Default **patrol time Interval**. + -- + -- Patrol dispatching is time event driven, and will evaluate in random time intervals if a new patrol needs to be dispatched. + -- + -- The default patrol time interval is between **180** and **600** seconds. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultPatrolTimeInterval}() to set the **default patrol time interval** of dispatched aircraft for ALL squadrons. + -- + -- Note that you can still change the patrol limit and patrol time intervals for each patrol individually using + -- the @{#AI_A2G_DISPATCHER.SetSquadronPatrolTimeInterval}() method. + -- + -- ### 10.7.2. Default **patrol limit**. + -- + -- Multiple patrol can be airborne at the same time for one squadron, which is controlled by the **patrol limit**. + -- The **default patrol limit** is 1 patrol per squadron to be airborne at the same time. + -- Note that the default patrol limit is used when a squadron patrol is defined, and cannot be changed afterwards. + -- So, ensure that you set the default patrol limit **before** you define or setup the squadron patrol. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultPatrolTimeInterval}() to set the **default patrol time interval** of dispatched aircraft patrols for all squadrons. + -- Note that you can still change the patrol limit and patrol time intervals for each patrol individually using + -- the @{#AI_A2G_DISPATCHER.SetSquadronPatrolTimeInterval}() method. + -- + -- ## 10.7.3. Default tanker for refuelling when executing CAP. + -- + -- Instead of sending CAP to RTB when out of fuel, you can let CAP refuel in mid air using a tanker. + -- This greatly increases the efficiency of your CAP operations. + -- + -- In the mission editor, setup a group with task Refuelling. A tanker unit of the correct coalition will be automatically selected. + -- Then, use the method @{#AI_A2G_DISPATCHER.SetDefaultTanker}() to set the tanker for the dispatcher. + -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultFuelThreshold}() to set the %-tage left in the defender airplane tanks when a refuel action is needed. + -- + -- When the tanker specified is alive and in the air, the tanker will be used for refuelling. + -- + -- For example, the following setup will set the default refuel tanker to "Tanker": + -- + -- ![Banner Image](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_DISPATCHER-ME_11.JPG) + -- + -- -- Define the CAP + -- A2ADispatcher:SetSquadron( "Sochi", AIRBASE.Caucasus.Sochi_Adler, { "SQ CCCP SU-34" }, 20 ) + -- A2ADispatcher:SetSquadronCap( "Sochi", ZONE:New( "PatrolZone" ), 4000, 8000, 600, 800, 1000, 1300 ) + -- A2ADispatcher:SetSquadronCapInterval("Sochi", 2, 30, 600, 1 ) + -- A2ADispatcher:SetSquadronGci( "Sochi", 900, 1200 ) + -- + -- -- Set the default tanker for refuelling to "Tanker", when the default fuel treshold has reached 90% fuel left. + -- A2ADispatcher:SetDefaultFuelThreshold( 0.9 ) + -- A2ADispatcher:SetDefaultTanker( "Tanker" ) + -- + -- ## 10.8. Default settings for GCI. + -- + -- ## 10.8.1. Optimal intercept point calculation. + -- + -- When intruders are detected, the intrusion path of the attackers can be monitored by the EWR. + -- Although defender planes might be on standby at the airbase, it can still take some time to get the defenses up in the air if there aren't any defenses airborne. + -- This time can easily take 2 to 3 minutes, and even then the defenders still need to fly towards the target, which takes also time. + -- + -- Therefore, an optimal **intercept point** is calculated which takes a couple of parameters: + -- + -- * The average bearing of the intruders for an amount of seconds. + -- * The average speed of the intruders for an amount of seconds. + -- * An assumed time it takes to get planes operational at the airbase. + -- + -- The **intercept point** will determine: + -- + -- * If there are any friendlies close to engage the target. These can be defenders performing CAP or defenders in RTB. + -- * The optimal airbase from where defenders will takeoff for GCI. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetIntercept}() to modify the assumed intercept delay time to calculate a valid interception. + -- + -- ## 10.8.2. Default Disengage Radius. + -- + -- The radius to **disengage any target** when the **distance** of the defender to the **home base** is larger than the specified meters. + -- The default Disengage Radius is **300km** (300000 meters). Note that the Disengage Radius is applicable to ALL squadrons! + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetDisengageRadius}() to modify the default Disengage Radius to another distance setting. + -- + -- ## 11. Airbase capture: + -- + -- Different squadrons can be located at one airbase. + -- If the airbase gets captured, that is, when there is an enemy unit near the airbase, and there aren't anymore friendlies at the airbase, the airbase will change coalition ownership. + -- As a result, the GCI and CAP will stop! + -- However, the squadron will still stay alive. Any airplane that is airborne will continue its operations until all airborne airplanes + -- of the squadron will be destroyed. This to keep consistency of air operations not to confuse the players. + -- + -- + -- + -- + -- @field #AI_A2G_DISPATCHER + AI_A2G_DISPATCHER = { + ClassName = "AI_A2G_DISPATCHER", + Detection = nil, + } + + --- Definition of a Squadron. + -- @type AI_A2G_DISPATCHER.Squadron + -- @field #string Name The Squadron name. + -- @field Wrapper.Airbase#AIRBASE Airbase The home airbase. + -- @field #string AirbaseName The name of the home airbase. + -- @field Core.Spawn#SPAWN Spawn The spawning object. + -- @field #number ResourceCount The number of resources available. + -- @field #list<#string> TemplatePrefixes The list of template prefixes. + -- @field #boolean Captured true if the squadron is captured. + -- @field #number Overhead The overhead for the squadron. + + + --- List of defense coordinates. + -- @type AI_A2G_DISPATCHER.DefenseCoordinates + -- @map <#string,Core.Point#COORDINATE> A list of all defense coordinates mapped per defense coordinate name. + + --- @field #AI_A2G_DISPATCHER.DefenseCoordinates DefenseCoordinates + AI_A2G_DISPATCHER.DefenseCoordinates = {} + + --- Enumerator for spawns at airbases + -- @type AI_A2G_DISPATCHER.Takeoff + -- @extends Wrapper.Group#GROUP.Takeoff + + --- @field #AI_A2G_DISPATCHER.Takeoff Takeoff + AI_A2G_DISPATCHER.Takeoff = GROUP.Takeoff + + --- Defnes Landing location. + -- @field Landing + AI_A2G_DISPATCHER.Landing = { + NearAirbase = 1, + AtRunway = 2, + AtEngineShutdown = 3, + } + + --- A defense queue item description + -- @type AI_A2G_DISPATCHER.DefenseQueueItem + -- @field Squadron + -- @field #AI_A2G_DISPATCHER.Squadron DefenderSquadron The squadron in the queue. + -- @field DefendersNeeded + -- @field Defense + -- @field DefenseTaskType + -- @field Functional.Detection#DETECTION_BASE AttackerDetection + -- @field DefenderGrouping + -- @field #string SquadronName The name of the squadron. + + --- Queue of planned defenses to be launched. + -- This queue exists because defenses must be launched on FARPS, or in the air, or on an airbase, or on carriers. + -- And some of these platforms have very limited amount of "launching" platforms. + -- Therefore, this queue concept is introduced that queues each defender request. + -- Depending on the location of the launching site, the queued defenders will be launched at varying time intervals. + -- This guarantees that launched defenders are also directly existing ... + -- @type AI_A2G_DISPATCHER.DefenseQueue + -- @list<#AI_A2G_DISPATCHER.DefenseQueueItem> DefenseQueueItem A list of all defenses being queued ... + + --- @field #AI_A2G_DISPATCHER.DefenseQueue DefenseQueue + AI_A2G_DISPATCHER.DefenseQueue = {} + + + + + --- AI_A2G_DISPATCHER constructor. + -- This is defining the A2G DISPATCHER for one coaliton. + -- The Dispatcher works with a @{Functional.Detection#DETECTION_BASE} object that is taking of the detection of targets using the EWR units. + -- The Detection object is polymorphic, depending on the type of detection object choosen, the detection will work differently. + -- @param #AI_A2G_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE Detection The DETECTION object that will detects targets using the the Early Warning Radar network. + -- @return #AI_A2G_DISPATCHER self + -- @usage + -- + -- -- Setup the Detection, using DETECTION_AREAS. + -- -- First define the SET of GROUPs that are defining the EWR network. + -- -- Here with prefixes DF CCCP AWACS, DF CCCP EWR. + -- DetectionSetGroup = SET_GROUP:New() + -- DetectionSetGroup:FilterPrefixes( { "DF CCCP AWACS", "DF CCCP EWR" } ) + -- DetectionSetGroup:FilterStart() + -- + -- -- Define the DETECTION_AREAS, using the DetectionSetGroup, with a 30km grouping radius. + -- Detection = DETECTION_AREAS:New( DetectionSetGroup, 30000 ) + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) -- + -- + function AI_A2G_DISPATCHER:New( Detection ) + + -- Inherits from DETECTION_MANAGER + local self = BASE:Inherit( self, DETECTION_MANAGER:New( nil, Detection ) ) -- #AI_A2G_DISPATCHER + + self.Detection = Detection -- Functional.Detection#DETECTION_AREAS + + self.Detection:FilterCategories( Unit.Category.GROUND_UNIT ) + + -- This table models the DefenderSquadron templates. + self.DefenderSquadrons = {} -- The Defender Squadrons. + self.DefenderSpawns = {} + self.DefenderTasks = {} -- The Defenders Tasks. + self.DefenderDefault = {} -- The Defender Default Settings over all Squadrons. + + -- TODO: Check detection through radar. +-- self.Detection:FilterCategories( { Unit.Category.GROUND } ) +-- self.Detection:InitDetectRadar( false ) +-- self.Detection:InitDetectVisual( true ) +-- self.Detection:SetRefreshTimeInterval( 30 ) + + self:SetDefenseRadius() + self:SetIntercept( 300 ) -- A default intercept delay time of 300 seconds. + self:SetDisengageRadius( 300000 ) -- The default Disengage Radius is 300 km. + + self:SetDefaultTakeoff( AI_A2G_DISPATCHER.Takeoff.Air ) + self:SetDefaultTakeoffInAirAltitude( 500 ) -- Default takeoff is 500 meters above the ground. + self:SetDefaultLanding( AI_A2G_DISPATCHER.Landing.NearAirbase ) + self:SetDefaultOverhead( 1 ) + self:SetDefaultGrouping( 1 ) + self:SetDefaultFuelThreshold( 0.15, 0 ) -- 15% of fuel remaining in the tank will trigger the airplane to return to base or refuel. + self:SetDefaultDamageThreshold( 0.4 ) -- When 40% of damage, go RTB. + self:SetDefaultPatrolTimeInterval( 180, 600 ) -- Between 180 and 600 seconds. + self:SetDefaultPatrolLimit( 1 ) -- Maximum one Patrol per squadron. + + + self:AddTransition( "Started", "Assign", "Started" ) + + --- OnAfter Transition Handler for Event Assign. + -- @function [parent=#AI_A2G_DISPATCHER] OnAfterAssign + -- @param #AI_A2G_DISPATCHER self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @param Tasking.Task_A2G#AI_A2G Task + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param #string PlayerName + + self:AddTransition( "*", "Patrol", "*" ) + + --- Patrol Handler OnBefore for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] OnBeforePatrol + -- @param #AI_A2G_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Patrol Handler OnAfter for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] OnAfterPatrol + -- @param #AI_A2G_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Patrol Trigger for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] Patrol + -- @param #AI_A2G_DISPATCHER self + + --- Patrol Asynchronous Trigger for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] __Patrol + -- @param #AI_A2G_DISPATCHER self + -- @param #number Delay + + self:AddTransition( "*", "Defend", "*" ) + + --- Defend Handler OnBefore for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] OnBeforeDefend + -- @param #AI_A2G_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Defend Handler OnAfter for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] OnAfterDefend + -- @param #AI_A2G_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Defend Trigger for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] Defend + -- @param #AI_A2G_DISPATCHER self + + --- Defend Asynchronous Trigger for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] __Defend + -- @param #AI_A2G_DISPATCHER self + -- @param #number Delay + + self:AddTransition( "*", "Engage", "*" ) + + --- Engage Handler OnBefore for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] OnBeforeEngage + -- @param #AI_A2G_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Engage Handler OnAfter for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] OnAfterEngage + -- @param #AI_A2G_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Engage Trigger for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] Engage + -- @param #AI_A2G_DISPATCHER self + + --- Engage Asynchronous Trigger for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] __Engage + -- @param #AI_A2G_DISPATCHER self + -- @param #number Delay + + + -- Subscribe to the CRASH event so that when planes are shot + -- by a Unit from the dispatcher, they will be removed from the detection... + -- This will avoid the detection to still "know" the shot unit until the next detection. + -- Otherwise, a new defense or engage may happen for an already shot plane! + + + self:HandleEvent( EVENTS.Crash, self.OnEventCrashOrDead ) + self:HandleEvent( EVENTS.Dead, self.OnEventCrashOrDead ) + --self:HandleEvent( EVENTS.RemoveUnit, self.OnEventCrashOrDead ) + + + self:HandleEvent( EVENTS.Land ) + self:HandleEvent( EVENTS.EngineShutdown ) + + -- Handle the situation where the airbases are captured. + self:HandleEvent( EVENTS.BaseCaptured ) + + self:SetTacticalDisplay( false ) + + self.DefenderPatrolIndex = 0 + + self:SetDefenseReactivityMedium() + + self.TakeoffScheduleID = self:ScheduleRepeat( 10, 10, 0, nil, self.ResourceTakeoff, self ) + + self:__Start( 5 ) + + return self + end + + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:onafterStart( From, Event, To ) + + self:GetParent( self ).onafterStart( self, From, Event, To ) + + -- Spawn the resources. + for SquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do + DefenderSquadron.Resource = {} + for Resource = 1, DefenderSquadron.ResourceCount or 0 do + self:ResourcePark( DefenderSquadron ) + end + end + end + + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ResourcePark( DefenderSquadron ) + local TemplateID = math.random( 1, #DefenderSquadron.Spawn ) + local Spawn = DefenderSquadron.Spawn[ TemplateID ] -- Core.Spawn#SPAWN + Spawn:InitGrouping( 1 ) + local SpawnGroup + if self:IsSquadronVisible( DefenderSquadron.Name ) then + SpawnGroup = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, SPAWN.Takeoff.Cold ) + local GroupName = SpawnGroup:GetName() + DefenderSquadron.Resources = DefenderSquadron.Resources or {} + DefenderSquadron.Resources[TemplateID] = DefenderSquadron.Resources[TemplateID] or {} + DefenderSquadron.Resources[TemplateID][GroupName] = {} + DefenderSquadron.Resources[TemplateID][GroupName] = SpawnGroup + end + end + + + --- @param #AI_A2G_DISPATCHER self + -- @param Core.Event#EVENTDATA EventData + function AI_A2G_DISPATCHER:OnEventBaseCaptured( EventData ) + + local AirbaseName = EventData.PlaceName -- The name of the airbase that was captured. + + self:I( "Captured " .. AirbaseName ) + + -- Now search for all squadrons located at the airbase, and sanatize them. + for SquadronName, Squadron in pairs( self.DefenderSquadrons ) do + if Squadron.AirbaseName == AirbaseName then + Squadron.ResourceCount = -999 -- The base has been captured, and the resources are eliminated. No more spawning. + Squadron.Captured = true + self:I( "Squadron " .. SquadronName .. " captured." ) + end + end + end + + --- @param #AI_A2G_DISPATCHER self + -- @param Core.Event#EVENTDATA EventData + function AI_A2G_DISPATCHER:OnEventCrashOrDead( EventData ) + self.Detection:ForgetDetectedUnit( EventData.IniUnitName ) + end + + --- @param #AI_A2G_DISPATCHER self + -- @param Core.Event#EVENTDATA EventData + function AI_A2G_DISPATCHER:OnEventLand( EventData ) + self:F( "Landed" ) + local DefenderUnit = EventData.IniUnit + local Defender = EventData.IniGroup + local Squadron = self:GetSquadronFromDefender( Defender ) + if Squadron then + self:F( { SquadronName = Squadron.Name } ) + local LandingMethod = self:GetSquadronLanding( Squadron.Name ) + + if LandingMethod == AI_A2G_DISPATCHER.Landing.AtRunway then + local DefenderSize = Defender:GetSize() + if DefenderSize == 1 then + self:RemoveDefenderFromSquadron( Squadron, Defender ) + end + DefenderUnit:Destroy() + self:ResourcePark( Squadron, Defender ) + return + end + if DefenderUnit:GetLife() ~= DefenderUnit:GetLife0() then + -- Damaged units cannot be repaired anymore. + DefenderUnit:Destroy() + return + end + end + end + + --- @param #AI_A2G_DISPATCHER self + -- @param Core.Event#EVENTDATA EventData + function AI_A2G_DISPATCHER:OnEventEngineShutdown( EventData ) + local DefenderUnit = EventData.IniUnit + local Defender = EventData.IniGroup + local Squadron = self:GetSquadronFromDefender( Defender ) + if Squadron then + self:F( { SquadronName = Squadron.Name } ) + local LandingMethod = self:GetSquadronLanding( Squadron.Name ) + if LandingMethod == AI_A2G_DISPATCHER.Landing.AtEngineShutdown and + not DefenderUnit:InAir() then + local DefenderSize = Defender:GetSize() + if DefenderSize == 1 then + self:RemoveDefenderFromSquadron( Squadron, Defender ) + end + DefenderUnit:Destroy() + self:ResourcePark( Squadron, Defender ) + end + end + end + + do -- Manage the defensive behaviour + + --- @param #AI_A2G_DISPATCHER self + -- @param #string DefenseCoordinateName The name of the coordinate to be defended by A2G defenses. + -- @param Core.Point#COORDINATE DefenseCoordinate The coordinate to be defended by A2G defenses. + function AI_A2G_DISPATCHER:AddDefenseCoordinate( DefenseCoordinateName, DefenseCoordinate ) + self.DefenseCoordinates[DefenseCoordinateName] = DefenseCoordinate + end + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:SetDefenseReactivityLow() + self.DefenseReactivity = 0.05 + end + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:SetDefenseReactivityMedium() + self.DefenseReactivity = 0.15 + end + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:SetDefenseReactivityHigh() + self.DefenseReactivity = 0.5 + end + + end + + --- Define the radius to engage any target by airborne friendlies, which are executing cap or returning from an defense mission. + -- If there is a target area detected and reported, then any friendlies that are airborne near this target area, + -- will be commanded to (re-)engage that target when available (if no other tasks were commanded). + -- + -- For example, if 100000 is given as a value, then any friendly that is airborne within 100km from the detected target, + -- will be considered to receive the command to engage that target area. + -- + -- You need to evaluate the value of this parameter carefully: + -- + -- * If too small, more defense missions may be triggered upon detected target areas. + -- * If too large, any airborne cap may not be able to reach the detected target area in time, because it is too far. + -- + -- **Use the method @{#AI_A2G_DISPATCHER.SetEngageRadius}() to modify the default Engage Radius for ALL squadrons.** + -- + -- Demonstration Mission: [AID-019 - AI_A2G - Engage Range Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-019%20-%20AI_A2G%20-%20Engage%20Range%20Test) + -- + -- @param #AI_A2G_DISPATCHER self + -- @param #number EngageRadius (Optional, Default = 100000) The radius to report friendlies near the target. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Set 50km as the radius to engage any target by airborne friendlies. + -- A2GDispatcher:SetEngageRadius( 50000 ) + -- + -- -- Set 100km as the radius to engage any target by airborne friendlies. + -- A2GDispatcher:SetEngageRadius() -- 100000 is the default value. + -- + function AI_A2G_DISPATCHER:SetEngageRadius( EngageRadius ) + + --self.Detection:SetFriendliesRange( EngageRadius or 100000 ) + + return self + end + + --- Define the radius to disengage any target when the distance to the home base is larger than the specified meters. + -- @param #AI_A2G_DISPATCHER self + -- @param #number DisengageRadius (Optional, Default = 300000) The radius to disengage a target when too far from the home base. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Set 50km as the Disengage Radius. + -- A2GDispatcher:SetDisengageRadius( 50000 ) + -- + -- -- Set 100km as the Disengage Radius. + -- A2GDispatcher:SetDisngageRadius() -- 300000 is the default value. + -- + function AI_A2G_DISPATCHER:SetDisengageRadius( DisengageRadius ) + + self.DisengageRadius = DisengageRadius or 300000 + + return self + end + + + --- Define the defense radius to check if a target can be engaged by a squadron group for SEAD, CAS or BAI for defense. + -- When targets are detected that are still really far off, you don't want the AI_A2G_DISPATCHER to launch defenders, as they might need to travel too far. + -- You want it to wait until a certain defend radius is reached, which is calculated as: + -- 1. the **distance of the closest airbase to target**, being smaller than the **Defend Radius**. + -- 2. the **distance to any defense reference point**. + -- + -- The **default** defense radius is defined as **400000** or **40km**. Override the default defense radius when the era of the warfare is early, or, + -- when you don't want to let the AI_A2G_DISPATCHER react immediately when a certain border or area is not being crossed. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetDefendRadius}() to set a specific defend radius for all squadrons, + -- **the Defense Radius is defined for ALL squadrons which are operational.** + -- + -- @param #AI_A2G_DISPATCHER self + -- @param #number DefenseRadius (Optional, Default = 200000) The defense radius to engage detected targets from the nearest capable and available squadron airbase. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Set 100km as the radius to defend from detected targets from the nearest airbase. + -- A2GDispatcher:SetDefendRadius( 100000 ) + -- + -- -- Set 200km as the radius to defend. + -- A2GDispatcher:SetDefendRadius() -- 200000 is the default value. + -- + function AI_A2G_DISPATCHER:SetDefenseRadius( DefenseRadius ) + + self.DefenseRadius = DefenseRadius or 100000 + + self.Detection:SetAcceptRange( self.DefenseRadius ) + + return self + end + + + + --- Define a border area to simulate a **cold war** scenario. + -- A **cold war** is one where Patrol aircraft patrol their territory but will not attack enemy aircraft or launch GCI aircraft unless enemy aircraft enter their territory. In other words the EWR may detect an enemy aircraft but will only send aircraft to attack it if it crosses the border. + -- A **hot war** is one where Patrol aircraft will intercept any detected enemy aircraft and GCI aircraft will launch against detected enemy aircraft without regard for territory. In other words if the ground radar can detect the enemy aircraft then it will send Patrol and GCI aircraft to attack it. + -- If it's a cold war then the **borders of red and blue territory** need to be defined using a @{zone} object derived from @{Core.Zone#ZONE_BASE}. This method needs to be used for this. + -- If a hot war is chosen then **no borders** actually need to be defined using the helicopter units other than it makes it easier sometimes for the mission maker to envisage where the red and blue territories roughly are. In a hot war the borders are effectively defined by the ground based radar coverage of a coalition. Set the noborders parameter to 1 + -- @param #AI_A2G_DISPATCHER self + -- @param Core.Zone#ZONE_BASE BorderZone An object derived from ZONE_BASE, or a list of objects derived from ZONE_BASE. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Set one ZONE_POLYGON object as the border for the A2G dispatcher. + -- local BorderZone = ZONE_POLYGON( "CCCP Border", GROUP:FindByName( "CCCP Border" ) ) -- The GROUP object is a late activate helicopter unit. + -- A2GDispatcher:SetBorderZone( BorderZone ) + -- + -- or + -- + -- -- Set two ZONE_POLYGON objects as the border for the A2G dispatcher. + -- local BorderZone1 = ZONE_POLYGON( "CCCP Border1", GROUP:FindByName( "CCCP Border1" ) ) -- The GROUP object is a late activate helicopter unit. + -- local BorderZone2 = ZONE_POLYGON( "CCCP Border2", GROUP:FindByName( "CCCP Border2" ) ) -- The GROUP object is a late activate helicopter unit. + -- A2GDispatcher:SetBorderZone( { BorderZone1, BorderZone2 } ) + -- + -- + function AI_A2G_DISPATCHER:SetBorderZone( BorderZone ) + + self.Detection:SetAcceptZones( BorderZone ) + + return self + end + + --- Display a tactical report every 30 seconds about which aircraft are: + -- * Patrolling + -- * Engaging + -- * Returning + -- * Damaged + -- * Out of Fuel + -- * ... + -- @param #AI_A2G_DISPATCHER self + -- @param #boolean TacticalDisplay Provide a value of **true** to display every 30 seconds a tactical overview. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the Tactical Display for debug mode. + -- A2GDispatcher:SetTacticalDisplay( true ) + -- + function AI_A2G_DISPATCHER:SetTacticalDisplay( TacticalDisplay ) + + self.TacticalDisplay = TacticalDisplay + + return self + end + + + --- Set the default damage treshold when defenders will RTB. + -- The default damage treshold is by default set to 40%, which means that when the airplane is 40% damaged, it will go RTB. + -- @param #AI_A2G_DISPATCHER self + -- @param #number DamageThreshold A decimal number between 0 and 1, that expresses the %-tage of the damage treshold before going RTB. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default damage treshold. + -- A2GDispatcher:SetDefaultDamageThreshold( 0.90 ) -- Go RTB when the airplane 90% damaged. + -- + function AI_A2G_DISPATCHER:SetDefaultDamageThreshold( DamageThreshold ) + + self.DefenderDefault.DamageThreshold = DamageThreshold + + return self + end + + + --- Set the default Patrol time interval for squadrons, which will be used to determine a random Patrol timing. + -- The default Patrol time interval is between 180 and 600 seconds. + -- @param #AI_A2G_DISPATCHER self + -- @param #number PatrolMinSeconds The minimum amount of seconds for the random time interval. + -- @param #number PatrolMaxSeconds The maximum amount of seconds for the random time interval. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default Patrol time interval. + -- A2GDispatcher:SetDefaultPatrolTimeInterval( 300, 1200 ) -- Between 300 and 1200 seconds. + -- + function AI_A2G_DISPATCHER:SetDefaultPatrolTimeInterval( PatrolMinSeconds, PatrolMaxSeconds ) + + self.DefenderDefault.PatrolMinSeconds = PatrolMinSeconds + self.DefenderDefault.PatrolMaxSeconds = PatrolMaxSeconds + + return self + end + + + --- Set the default Patrol limit for squadrons, which will be used to determine how many Patrol can be airborne at the same time for the squadron. + -- The default Patrol limit is 1 Patrol, which means one Patrol group being spawned. + -- @param #AI_A2G_DISPATCHER self + -- @param #number PatrolLimit The maximum amount of Patrol that can be airborne at the same time for the squadron. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default Patrol limit. + -- A2GDispatcher:SetDefaultPatrolLimit( 2 ) -- Maximum 2 Patrol per squadron. + -- + function AI_A2G_DISPATCHER:SetDefaultPatrolLimit( PatrolLimit ) + + self.DefenderDefault.PatrolLimit = PatrolLimit + + return self + end + + + --- Set the default engage limit for squadrons, which will be used to determine how many air units will engage at the same time with the enemy. + -- The default eatrol limit is 1, which means one eatrol group maximum per squadron. + -- @param #AI_A2G_DISPATCHER self + -- @param #number EngageLimit The maximum engages that can be done at the same time per squadron. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default Patrol limit. + -- A2GDispatcher:SetDefaultEngageLimit( 2 ) -- Maximum 2 engagements with the enemy per squadron. + -- + function AI_A2G_DISPATCHER:SetDefaultEngageLimit( EngageLimit ) + + self.DefenderDefault.EngageLimit = EngageLimit + + return self + end + + + function AI_A2G_DISPATCHER:SetIntercept( InterceptDelay ) + + self.DefenderDefault.InterceptDelay = InterceptDelay + + local Detection = self.Detection -- Functional.Detection#DETECTION_AREAS + Detection:SetIntercept( true, InterceptDelay ) + + return self + end + + + --- Calculates which defender friendlies are nearby the area, to help protect the area. + -- @param #AI_A2G_DISPATCHER self + -- @param DetectedItem + -- @return #table A list of the defender friendlies nearby, sorted by distance. + function AI_A2G_DISPATCHER:GetDefenderFriendliesNearBy( DetectedItem ) + +-- local DefenderFriendliesNearBy = self.Detection:GetFriendliesDistance( DetectedItem ) + + local DefenderFriendliesNearBy = {} + + local DetectionCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem ) + + local ScanZone = ZONE_RADIUS:New( "ScanZone", DetectionCoordinate:GetVec2(), self.DefenseRadius ) + + ScanZone:Scan( Object.Category.UNIT, { Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } ) + + local DefenderUnits = ScanZone:GetScannedUnits() + + for DefenderUnitID, DefenderUnit in pairs( DefenderUnits ) do + local DefenderUnit = UNIT:FindByName( DefenderUnit:getName() ) + + DefenderFriendliesNearBy[#DefenderFriendliesNearBy+1] = DefenderUnit + end + + + return DefenderFriendliesNearBy + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:GetDefenderTasks() + return self.DefenderTasks or {} + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:GetDefenderTask( Defender ) + return self.DefenderTasks[Defender] + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:GetDefenderTaskFsm( Defender ) + return self:GetDefenderTask( Defender ).Fsm + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:GetDefenderTaskTarget( Defender ) + return self:GetDefenderTask( Defender ).Target + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:GetDefenderTaskSquadronName( Defender ) + return self:GetDefenderTask( Defender ).SquadronName + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ClearDefenderTask( Defender ) + if Defender:IsAlive() and self.DefenderTasks[Defender] then + local Target = self.DefenderTasks[Defender].Target + local Message = "Clearing (" .. self.DefenderTasks[Defender].Type .. ") " + Message = Message .. Defender:GetName() + if Target then + Message = Message .. ( Target and ( " from " .. Target.Index .. " [" .. Target.Set:Count() .. "]" ) ) or "" + end + self:F( { Target = Message } ) + end + self.DefenderTasks[Defender] = nil + return self + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ClearDefenderTaskTarget( Defender ) + + local DefenderTask = self:GetDefenderTask( Defender ) + + if Defender:IsAlive() and DefenderTask then + local Target = DefenderTask.Target + local Message = "Clearing (" .. DefenderTask.Type .. ") " + Message = Message .. Defender:GetName() + if Target then + Message = Message .. ( Target and ( " from " .. Target.Index .. " [" .. Target.Set:Count() .. "]" ) ) or "" + end + self:F( { Target = Message } ) + end + if Defender and DefenderTask and DefenderTask.Target then + DefenderTask.Target = nil + end +-- if Defender and DefenderTask then +-- if DefenderTask.Fsm:Is( "Fuel" ) +-- or DefenderTask.Fsm:Is( "LostControl") +-- or DefenderTask.Fsm:Is( "Damaged" ) then +-- self:ClearDefenderTask( Defender ) +-- end +-- end + return self + end + + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:SetDefenderTask( SquadronName, Defender, Type, Fsm, Target, Size ) + + self:F( { SquadronName = SquadronName, Defender = Defender:GetName() } ) + + self.DefenderTasks[Defender] = self.DefenderTasks[Defender] or {} + self.DefenderTasks[Defender].Type = Type + self.DefenderTasks[Defender].Fsm = Fsm + self.DefenderTasks[Defender].SquadronName = SquadronName + self.DefenderTasks[Defender].Size = Size + + if Target then + self:SetDefenderTaskTarget( Defender, Target ) + end + return self + end + + + --- + -- @param #AI_A2G_DISPATCHER self + -- @param Wrapper.Group#GROUP AIGroup + function AI_A2G_DISPATCHER:SetDefenderTaskTarget( Defender, AttackerDetection ) + + local Message = "(" .. self.DefenderTasks[Defender].Type .. ") " + Message = Message .. Defender:GetName() + Message = Message .. ( AttackerDetection and ( " target " .. AttackerDetection.Index .. " [" .. AttackerDetection.Set:Count() .. "]" ) ) or "" + self:F( { AttackerDetection = Message } ) + if AttackerDetection then + self.DefenderTasks[Defender].Target = AttackerDetection + end + return self + end + + + --- This is the main method to define Squadrons programmatically. + -- Squadrons: + -- + -- * Have a **name or key** that is the identifier or key of the squadron. + -- * Have **specific plane types** defined by **templates**. + -- * Are **located at one specific airbase**. Multiple squadrons can be located at one airbase through. + -- * Optionally have a limited set of **resources**. The default is that squadrons have unlimited resources. + -- + -- The name of the squadron given acts as the **squadron key** in the AI\_A2G\_DISPATCHER:Squadron...() methods. + -- + -- Additionally, squadrons have specific configuration options to: + -- + -- * Control how new aircraft are **taking off** from the airfield (in the air, cold, hot, at the runway). + -- * Control how returning aircraft are **landing** at the airfield (in the air near the airbase, after landing, after engine shutdown). + -- * Control the **grouping** of new aircraft spawned at the airfield. If there is more than one aircraft to be spawned, these may be grouped. + -- * Control the **overhead** or defensive strength of the squadron. Depending on the types of planes and amount of resources, the mission designer can choose to increase or reduce the amount of planes spawned. + -- + -- For performance and bug workaround reasons within DCS, squadrons have different methods to spawn new aircraft or land returning or damaged aircraft. + -- + -- @param #AI_A2G_DISPATCHER self + -- + -- @param #string SquadronName A string (text) that defines the squadron identifier or the key of the Squadron. + -- It can be any name, for example `"104th Squadron"` or `"SQ SQUADRON1"`, whatever. + -- As long as you remember that this name becomes the identifier of your squadron you have defined. + -- You need to use this name in other methods too! + -- + -- @param #string AirbaseName The airbase name where you want to have the squadron located. + -- You need to specify here EXACTLY the name of the airbase as you see it in the mission editor. + -- Examples are `"Batumi"` or `"Tbilisi-Lochini"`. + -- EXACTLY the airbase name, between quotes `""`. + -- To ease the airbase naming when using the LDT editor and IntelliSense, the @{Wrapper.Airbase#AIRBASE} class contains enumerations of the airbases of each map. + -- + -- * Caucasus: @{Wrapper.Airbase#AIRBASE.Caucaus} + -- * Nevada or NTTR: @{Wrapper.Airbase#AIRBASE.Nevada} + -- * Normandy: @{Wrapper.Airbase#AIRBASE.Normandy} + -- + -- @param #string TemplatePrefixes A string or an array of strings specifying the **prefix names of the templates** (not going to explain what is templates here again). + -- Examples are `{ "104th", "105th" }` or `"104th"` or `"Template 1"` or `"BLUE PLANES"`. + -- Just remember that your template (groups late activated) need to start with the prefix you have specified in your code. + -- If you have only one prefix name for a squadron, you don't need to use the `{ }`, otherwise you need to use the brackets. + -- + -- @param #number ResourceCount (optional) A number that specifies how many resources are in stock of the squadron. If not specified, the squadron will have infinite resources available. + -- + -- @usage + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- @usage + -- -- This will create squadron "Squadron1" at "Batumi" airbase, and will use plane types "SQ1" and has 40 planes in stock... + -- A2GDispatcher:SetSquadron( "Squadron1", "Batumi", "SQ1", 40 ) + -- + -- @usage + -- -- This will create squadron "Sq 1" at "Batumi" airbase, and will use plane types "Mig-29" and "Su-27" and has 20 planes in stock... + -- -- Note that in this implementation, the A2G dispatcher will select a random plane type when a new plane (group) needs to be spawned for defenses. + -- -- Note the usage of the {} for the airplane templates list. + -- A2GDispatcher:SetSquadron( "Sq 1", "Batumi", { "Mig-29", "Su-27" }, 40 ) + -- + -- @usage + -- -- This will create 2 squadrons "104th" and "23th" at "Batumi" airbase, and will use plane types "Mig-29" and "Su-27" respectively and each squadron has 10 planes in stock... + -- A2GDispatcher:SetSquadron( "104th", "Batumi", "Mig-29", 10 ) + -- A2GDispatcher:SetSquadron( "23th", "Batumi", "Su-27", 10 ) + -- + -- @usage + -- -- This is an example like the previous, but now with infinite resources. + -- -- The ResourceCount parameter is not given in the SetSquadron method. + -- A2GDispatcher:SetSquadron( "104th", "Batumi", "Mig-29" ) + -- A2GDispatcher:SetSquadron( "23th", "Batumi", "Su-27" ) + -- + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadron( SquadronName, AirbaseName, TemplatePrefixes, ResourceCount ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + local DefenderSquadron = self.DefenderSquadrons[SquadronName] + + DefenderSquadron.Name = SquadronName + DefenderSquadron.Airbase = AIRBASE:FindByName( AirbaseName ) + DefenderSquadron.AirbaseName = DefenderSquadron.Airbase:GetName() + if not DefenderSquadron.Airbase then + error( "Cannot find airbase with name:" .. AirbaseName ) + end + + DefenderSquadron.Spawn = {} + if type( TemplatePrefixes ) == "string" then + local SpawnTemplate = TemplatePrefixes + self.DefenderSpawns[SpawnTemplate] = self.DefenderSpawns[SpawnTemplate] or SPAWN:New( SpawnTemplate ) -- :InitCleanUp( 180 ) + DefenderSquadron.Spawn[1] = self.DefenderSpawns[SpawnTemplate] + else + for TemplateID, SpawnTemplate in pairs( TemplatePrefixes ) do + self.DefenderSpawns[SpawnTemplate] = self.DefenderSpawns[SpawnTemplate] or SPAWN:New( SpawnTemplate ) -- :InitCleanUp( 180 ) + DefenderSquadron.Spawn[#DefenderSquadron.Spawn+1] = self.DefenderSpawns[SpawnTemplate] + end + end + DefenderSquadron.ResourceCount = ResourceCount + DefenderSquadron.TemplatePrefixes = TemplatePrefixes + DefenderSquadron.Captured = false -- Not captured. This flag will be set to true, when the airbase where the squadron is located, is captured. + + self:SetSquadronTakeoffInterval( SquadronName, 0 ) + + self:F( { Squadron = {SquadronName, AirbaseName, TemplatePrefixes, ResourceCount } } ) + + return self + end + + --- Get an item from the Squadron table. + -- @param #AI_A2G_DISPATCHER self + -- @return #table + function AI_A2G_DISPATCHER:GetSquadron( SquadronName ) + + local DefenderSquadron = self.DefenderSquadrons[SquadronName] + + if not DefenderSquadron then + error( "Unknown Squadron:" .. SquadronName ) + end + + return DefenderSquadron + end + + + --- Set the Squadron visible before startup of the dispatcher. + -- All planes will be spawned as uncontrolled on the parking spot. + -- They will lock the parking spot. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Set the Squadron visible before startup of dispatcher. + -- A2GDispatcher:SetSquadronVisible( "Mineralnye" ) + -- + function AI_A2G_DISPATCHER:SetSquadronVisible( SquadronName ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.Uncontrolled = true + self:SetSquadronTakeoffFromParkingCold( SquadronName ) + self:SetSquadronLandingAtEngineShutdown( SquadronName ) + + for SpawnTemplate, DefenderSpawn in pairs( self.DefenderSpawns ) do + DefenderSpawn:InitUnControlled() + end + + end + + --- Check if the Squadron is visible before startup of the dispatcher. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #bool true if visible. + -- @usage + -- + -- -- Set the Squadron visible before startup of dispatcher. + -- local IsVisible = A2GDispatcher:IsSquadronVisible( "Mineralnye" ) + -- + function AI_A2G_DISPATCHER:IsSquadronVisible( SquadronName ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + if DefenderSquadron then + return DefenderSquadron.Uncontrolled == true + end + + return nil + + end + + --- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number TakeoffInterval Only Takeoff new units each specified interval in seconds in 10 seconds steps. + -- @usage + -- + -- -- Set the Squadron Takeoff interval every 60 seconds for squadron "SQ50", which is good for a FARP cold start. + -- A2GDispatcher:SetSquadronTakeoffInterval( "SQ50", 60 ) + -- + function AI_A2G_DISPATCHER:SetSquadronTakeoffInterval( SquadronName, TakeoffInterval ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + if DefenderSquadron then + DefenderSquadron.TakeoffInterval = TakeoffInterval or 0 + DefenderSquadron.TakeoffTime = 0 + end + + end + + + + --- Set the squadron patrol parameters for a specific task type. + -- Mission designers should not use this method, instead use the below methods. This method is used by the below methods. + -- + -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadPatrolInterval} for SEAD tasks. + -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadPatrolInterval} for CAS tasks. + -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadPatrolInterval} for BAI tasks. + -- + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that a Patrol is a group, so can consist out of 1 to 4 airplanes. The default is 1 Patrol group. + -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new Patrol will be spawned. The default is 180 seconds. + -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new Patrol will be spawned. The default is 600 seconds. + -- @param #number Probability Is not in use, you can skip this parameter. + -- @param #string DefenseTaskType Should contain "SEAD", "CAS" or "BAI". + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronSeadPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- A2GDispatcher:SetSquadronPatrolInterval( "Mineralnye", 2, 30, 60, 1, "SEAD" ) + -- + function AI_A2G_DISPATCHER:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, DefenseTaskType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + local Patrol = DefenderSquadron[DefenseTaskType] + if Patrol then + Patrol.LowInterval = LowInterval or 180 + Patrol.HighInterval = HighInterval or 600 + Patrol.Probability = Probability or 1 + Patrol.PatrolLimit = PatrolLimit or 1 + Patrol.Scheduler = Patrol.Scheduler or SCHEDULER:New( self ) + local Scheduler = Patrol.Scheduler -- Core.Scheduler#SCHEDULER + local ScheduleID = Patrol.ScheduleID + local Variance = ( Patrol.HighInterval - Patrol.LowInterval ) / 2 + local Repeat = Patrol.LowInterval + Variance + local Randomization = Variance / Repeat + local Start = math.random( 1, Patrol.HighInterval ) + + if ScheduleID then + Scheduler:Stop( ScheduleID ) + end + + Patrol.ScheduleID = Scheduler:Schedule( self, self.SchedulerPatrol, { SquadronName }, Start, Repeat, Randomization ) + else + error( "This squadron does not exist:" .. SquadronName ) + end + + end + + + + --- Set the squadron Patrol parameters for SEAD tasks. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that a Patrol is a group, so can consist out of 1 to 4 airplanes. The default is 1 Patrol group. + -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new Patrol will be spawned. The default is 180 seconds. + -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new Patrol will be spawned. The default is 600 seconds. + -- @param #number Probability Is not in use, you can skip this parameter. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronSeadPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- A2GDispatcher:SetSquadronSeadPatrolInterval( "Mineralnye", 2, 30, 60, 1 ) + -- + function AI_A2G_DISPATCHER:SetSquadronSeadPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability ) + + self:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, "SEAD" ) + + end + + + --- Set the squadron Patrol parameters for CAS tasks. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that a Patrol is a group, so can consist out of 1 to 4 airplanes. The default is 1 Patrol group. + -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new Patrol will be spawned. The default is 180 seconds. + -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new Patrol will be spawned. The default is 600 seconds. + -- @param #number Probability Is not in use, you can skip this parameter. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronCasPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- A2GDispatcher:SetSquadronCasPatrolInterval( "Mineralnye", 2, 30, 60, 1 ) + -- + function AI_A2G_DISPATCHER:SetSquadronCasPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability ) + + self:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, "CAS" ) + + end + + + --- Set the squadron Patrol parameters for BAI tasks. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that a Patrol is a group, so can consist out of 1 to 4 airplanes. The default is 1 Patrol group. + -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new Patrol will be spawned. The default is 180 seconds. + -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new Patrol will be spawned. The default is 600 seconds. + -- @param #number Probability Is not in use, you can skip this parameter. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronBaiPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- A2GDispatcher:SetSquadronBaiPatrolInterval( "Mineralnye", 2, 30, 60, 1 ) + -- + function AI_A2G_DISPATCHER:SetSquadronBaiPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability ) + + self:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, "BAI" ) + + end + + + --- + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:GetPatrolDelay( SquadronName ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + self.DefenderSquadrons[SquadronName].Patrol = self.DefenderSquadrons[SquadronName].Patrol or {} + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + local Patrol = self.DefenderSquadrons[SquadronName].Patrol + if Patrol then + return math.random( Patrol.LowInterval, Patrol.HighInterval ) + else + error( "This squadron does not exist:" .. SquadronName ) + end + end + + --- + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #table DefenderSquadron + function AI_A2G_DISPATCHER:CanPatrol( SquadronName, DefenseTaskType ) + self:F({SquadronName = SquadronName}) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + if DefenderSquadron.Captured == false then -- We can only spawn new Patrol if the base has not been captured. + + if ( not DefenderSquadron.ResourceCount ) or ( DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0 ) then -- And, if there are sufficient resources. + + local Patrol = DefenderSquadron[DefenseTaskType] + if Patrol and Patrol.Patrol == true then + local PatrolCount = self:CountPatrolAirborne( SquadronName, DefenseTaskType ) + self:F( { PatrolCount = PatrolCount, PatrolLimit = Patrol.PatrolLimit, PatrolProbability = Patrol.Probability } ) + if PatrolCount < Patrol.PatrolLimit then + local Probability = math.random() + if Probability <= Patrol.Probability then + return DefenderSquadron, Patrol + end + end + else + self:F( "No patrol for " .. SquadronName ) + end + end + end + return nil + end + + + --- + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #table DefenderSquadron + function AI_A2G_DISPATCHER:CanDefend( SquadronName, DefenseTaskType ) + self:F({SquadronName = SquadronName, DefenseTaskType}) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + if DefenderSquadron.Captured == false then -- We can only spawn new defense if the home airbase has not been captured. + + if ( not DefenderSquadron.ResourceCount ) or ( DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0 ) then -- And, if there are sufficient resources. + if DefenderSquadron[DefenseTaskType] and ( DefenderSquadron[DefenseTaskType].Defend == true ) then + return DefenderSquadron, DefenderSquadron[DefenseTaskType] + end + end + end + return nil + end + + --- Set the squadron engage limit for a specific task type. + -- Mission designers should not use this method, instead use the below methods. This method is used by the below methods. + -- + -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadEngageLimit} for SEAD tasks. + -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadEngageLimit} for CAS tasks. + -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadEngageLimit} for BAI tasks. + -- + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageLimit The maximum amount of groups to engage with the enemy for this squadron. + -- @param #string DefenseTaskType Should contain "SEAD", "CAS" or "BAI". + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronEngageLimit( "Mineralnye", 2, "SEAD" ) -- Engage maximum 2 groups with the enemy for SEAD defense. + -- + function AI_A2G_DISPATCHER:SetSquadronEngageLimit( SquadronName, EngageLimit, DefenseTaskType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + local Defense = DefenderSquadron[DefenseTaskType] + if Defense then + Defense.EngageLimit = EngageLimit or 1 + else + error( "This squadron does not exist:" .. SquadronName ) + end + + end + + + + + --- + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the SEAD task can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the SEAD task can be executed. + -- @param DCS#Altitude EngageFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the engagement. + -- @param DCS#Altitude EngageCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the engagement. + -- @usage + -- + -- -- SEAD Squadron execution. + -- A2GDispatcher:SetSquadronSead( "Mozdok", 900, 1200 ) + -- A2GDispatcher:SetSquadronSead( "Novo", 900, 2100 ) + -- A2GDispatcher:SetSquadronSead( "Maykop", 900, 1200 ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronSead( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.SEAD = DefenderSquadron.SEAD or {} + + local Sead = DefenderSquadron.SEAD + Sead.Name = SquadronName + Sead.EngageMinSpeed = EngageMinSpeed + Sead.EngageMaxSpeed = EngageMaxSpeed + Sead.EngageFloorAltitude = EngageFloorAltitude + Sead.EngageCeilingAltitude = EngageCeilingAltitude + Sead.Defend = true + + self:F( { Sead = Sead } ) + end + + --- Set the squadron SEAD engage limit. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageLimit The maximum amount of groups to engage with the enemy for this squadron. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronSeadEngageLimit( "Mineralnye", 2 ) -- Engage maximum 2 groups with the enemy for SEAD defense. + -- + function AI_A2G_DISPATCHER:SetSquadronSeadEngageLimit( SquadronName, EngageLimit ) + + self:SetSquadronEngageLimit( SquadronName, EngageLimit, "SEAD" ) + + end + + + + + --- Set a Sead patrol for a Squadron. + -- The Sead patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. + -- @param #number FloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed. + -- @param #number CeilingAltitude (optional, default = 1500m ) The maximum altitude at which the cap can be executed. + -- @param #number PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed at which the cap can be executed. + -- @param #number PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the cap can be executed. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the engage can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the engage can be executed. + -- @param #number AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Sead Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronSeadPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- + function AI_A2G_DISPATCHER:SetSquadronSeadPatrol( SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.SEAD = DefenderSquadron.SEAD or {} + + local SeadPatrol = DefenderSquadron.SEAD + SeadPatrol.Name = SquadronName + SeadPatrol.Zone = Zone + SeadPatrol.PatrolFloorAltitude = FloorAltitude + SeadPatrol.PatrolCeilingAltitude = CeilingAltitude + SeadPatrol.EngageFloorAltitude = FloorAltitude + SeadPatrol.EngageCeilingAltitude = CeilingAltitude + SeadPatrol.PatrolMinSpeed = PatrolMinSpeed + SeadPatrol.PatrolMaxSpeed = PatrolMaxSpeed + SeadPatrol.EngageMinSpeed = EngageMinSpeed + SeadPatrol.EngageMaxSpeed = EngageMaxSpeed + SeadPatrol.AltType = AltType + SeadPatrol.Patrol = true + + self:SetSquadronPatrolInterval( SquadronName, self.DefenderDefault.PatrolLimit, self.DefenderDefault.PatrolMinSeconds, self.DefenderDefault.PatrolMaxSeconds, 1, "SEAD" ) + + self:F( { Sead = SeadPatrol } ) + end + + + --- + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the CAS task can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the CAS task can be executed. + -- @param DCS#Altitude EngageFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the engagement. + -- @param DCS#Altitude EngageCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the engagement. + -- @usage + -- + -- -- CAS Squadron execution. + -- A2GDispatcher:SetSquadronCas( "Mozdok", 900, 1200 ) + -- A2GDispatcher:SetSquadronCas( "Novo", 900, 2100 ) + -- A2GDispatcher:SetSquadronCas( "Maykop", 900, 1200 ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronCas( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.CAS = DefenderSquadron.CAS or {} + + local Cas = DefenderSquadron.CAS + Cas.Name = SquadronName + Cas.EngageMinSpeed = EngageMinSpeed + Cas.EngageMaxSpeed = EngageMaxSpeed + Cas.EngageFloorAltitude = EngageFloorAltitude + Cas.EngageCeilingAltitude = EngageCeilingAltitude + Cas.Defend = true + + self:F( { Cas = Cas } ) + end + + + --- Set the squadron CAS engage limit. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageLimit The maximum amount of groups to engage with the enemy for this squadron. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronCasEngageLimit( "Mineralnye", 2 ) -- Engage maximum 2 groups with the enemy for CAS defense. + -- + function AI_A2G_DISPATCHER:SetSquadronCasEngageLimit( SquadronName, EngageLimit ) + + self:SetSquadronEngageLimit( SquadronName, EngageLimit, "CAS" ) + + end + + + + + --- Set a Cas patrol for a Squadron. + -- The Cas patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. + -- @param #number FloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed. + -- @param #number CeilingAltitude (optional, default = 1500m ) The maximum altitude at which the cap can be executed. + -- @param #number PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed at which the cap can be executed. + -- @param #number PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the cap can be executed. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the engage can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the engage can be executed. + -- @param #number AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Cas Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronCasPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- + function AI_A2G_DISPATCHER:SetSquadronCasPatrol( SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.CAS = DefenderSquadron.CAS or {} + + local CasPatrol = DefenderSquadron.CAS + CasPatrol.Name = SquadronName + CasPatrol.Zone = Zone + CasPatrol.PatrolFloorAltitude = FloorAltitude + CasPatrol.PatrolCeilingAltitude = CeilingAltitude + CasPatrol.EngageFloorAltitude = FloorAltitude + CasPatrol.EngageCeilingAltitude = CeilingAltitude + CasPatrol.PatrolMinSpeed = PatrolMinSpeed + CasPatrol.PatrolMaxSpeed = PatrolMaxSpeed + CasPatrol.EngageMinSpeed = EngageMinSpeed + CasPatrol.EngageMaxSpeed = EngageMaxSpeed + CasPatrol.AltType = AltType + CasPatrol.Patrol = true + + self:SetSquadronPatrolInterval( SquadronName, self.DefenderDefault.PatrolLimit, self.DefenderDefault.PatrolMinSeconds, self.DefenderDefault.PatrolMaxSeconds, 1, "CAS" ) + + self:F( { Cas = CasPatrol } ) + end + + + --- + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the BAI task can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the BAI task can be executed. + -- @param DCS#Altitude EngageFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the engagement. + -- @param DCS#Altitude EngageCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the engagement. + -- @usage + -- + -- -- BAI Squadron execution. + -- A2GDispatcher:SetSquadronBai( "Mozdok", 900, 1200 ) + -- A2GDispatcher:SetSquadronBai( "Novo", 900, 2100 ) + -- A2GDispatcher:SetSquadronBai( "Maykop", 900, 1200 ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronBai( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.BAI = DefenderSquadron.BAI or {} + + local Bai = DefenderSquadron.BAI + Bai.Name = SquadronName + Bai.EngageMinSpeed = EngageMinSpeed + Bai.EngageMaxSpeed = EngageMaxSpeed + Bai.EngageFloorAltitude = EngageFloorAltitude + Bai.EngageCeilingAltitude = EngageCeilingAltitude + Bai.Defend = true + + self:F( { Bai = Bai } ) + end + + + --- Set the squadron BAI engage limit. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageLimit The maximum amount of groups to engage with the enemy for this squadron. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronBaiEngageLimit( "Mineralnye", 2 ) -- Engage maximum 2 groups with the enemy for BAI defense. + -- + function AI_A2G_DISPATCHER:SetSquadronBaiEngageLimit( SquadronName, EngageLimit ) + + self:SetSquadronEngageLimit( SquadronName, EngageLimit, "BAI" ) + + end + + + --- Set a Bai patrol for a Squadron. + -- The Bai patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. + -- @param #number FloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed. + -- @param #number CeilingAltitude (optional, default = 1500m ) The maximum altitude at which the cap can be executed. + -- @param #number PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed at which the cap can be executed. + -- @param #number PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the cap can be executed. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the engage can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the engage can be executed. + -- @param #number AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Bai Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronBaiPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- + function AI_A2G_DISPATCHER:SetSquadronBaiPatrol( SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.BAI = DefenderSquadron.BAI or {} + + local BaiPatrol = DefenderSquadron.BAI + BaiPatrol.Name = SquadronName + BaiPatrol.Zone = Zone + BaiPatrol.PatrolFloorAltitude = FloorAltitude + BaiPatrol.PatrolCeilingAltitude = CeilingAltitude + BaiPatrol.EngageFloorAltitude = FloorAltitude + BaiPatrol.EngageCeilingAltitude = CeilingAltitude + BaiPatrol.PatrolMinSpeed = PatrolMinSpeed + BaiPatrol.PatrolMaxSpeed = PatrolMaxSpeed + BaiPatrol.EngageMinSpeed = EngageMinSpeed + BaiPatrol.EngageMaxSpeed = EngageMaxSpeed + BaiPatrol.AltType = AltType + BaiPatrol.Patrol = true + + self:SetSquadronPatrolInterval( SquadronName, self.DefenderDefault.PatrolLimit, self.DefenderDefault.PatrolMinSeconds, self.DefenderDefault.PatrolMaxSeconds, 1, "BAI" ) + + self:F( { Bai = BaiPatrol } ) + end + + + --- Defines the default amount of extra planes that will take-off as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #number Overhead The %-tage of Units that dispatching command will allocate to intercept in surplus of detected amount of units. + -- The default overhead is 1, so equal balance. The @{#AI_A2G_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength, + -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2G missiles payload, may still be less effective than a F-15C with short missiles... + -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes. + -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values: + -- + -- * Higher than 1, will increase the defense unit amounts. + -- * Lower than 1, will decrease the defense unit amounts. + -- + -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group + -- multiplied by the Overhead and rounded up to the smallest integer. + -- + -- The Overhead value set for a Squadron, can be programmatically adjusted (by using this SetOverhead method), to adjust the defense overhead during mission execution. + -- + -- See example below. + -- + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- An overhead of 1,5 with 1 planes detected, will allocate 2 planes ( 1 * 1,5 ) = 1,5 => rounded up gives 2. + -- -- An overhead of 1,5 with 2 planes detected, will allocate 3 planes ( 2 * 1,5 ) = 3 => rounded up gives 3. + -- -- An overhead of 1,5 with 3 planes detected, will allocate 5 planes ( 3 * 1,5 ) = 4,5 => rounded up gives 5 planes. + -- -- An overhead of 1,5 with 4 planes detected, will allocate 6 planes ( 4 * 1,5 ) = 6 => rounded up gives 6 planes. + -- + -- A2GDispatcher:SetDefaultOverhead( 1.5 ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetDefaultOverhead( Overhead ) + + self.DefenderDefault.Overhead = Overhead + + return self + end + + + --- Defines the amount of extra planes that will take-off as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number Overhead The %-tage of Units that dispatching command will allocate to intercept in surplus of detected amount of units. + -- The default overhead is 1, so equal balance. The @{#AI_A2G_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength, + -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2G missiles payload, may still be less effective than a F-15C with short missiles... + -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes. + -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values: + -- + -- * Higher than 1, will increase the defense unit amounts. + -- * Lower than 1, will decrease the defense unit amounts. + -- + -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group + -- multiplied by the Overhead and rounded up to the smallest integer. + -- + -- The Overhead value set for a Squadron, can be programmatically adjusted (by using this SetOverhead method), to adjust the defense overhead during mission execution. + -- + -- See example below. + -- + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- An overhead of 1,5 with 1 planes detected, will allocate 2 planes ( 1 * 1,5 ) = 1,5 => rounded up gives 2. + -- -- An overhead of 1,5 with 2 planes detected, will allocate 3 planes ( 2 * 1,5 ) = 3 => rounded up gives 3. + -- -- An overhead of 1,5 with 3 planes detected, will allocate 5 planes ( 3 * 1,5 ) = 4,5 => rounded up gives 5 planes. + -- -- An overhead of 1,5 with 4 planes detected, will allocate 6 planes ( 4 * 1,5 ) = 6 => rounded up gives 6 planes. + -- + -- A2GDispatcher:SetSquadronOverhead( "SquadronName", 1.5 ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronOverhead( SquadronName, Overhead ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.Overhead = Overhead + + return self + end + + + --- Gets the overhead of planes as part of the defense system, in comparison with the attackers. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @return #number The %-tage of Units that dispatching command will allocate to intercept in surplus of detected amount of units. + -- The default overhead is 1, so equal balance. The @{#AI_A2G_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength, + -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2G missiles payload, may still be less effective than a F-15C with short missiles... + -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes. + -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values: + -- + -- * Higher than 1, will increase the defense unit amounts. + -- * Lower than 1, will decrease the defense unit amounts. + -- + -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group + -- multiplied by the Overhead and rounded up to the smallest integer. + -- + -- The Overhead value set for a Squadron, can be programmatically adjusted (by using this SetOverhead method), to adjust the defense overhead during mission execution. + -- + -- See example below. + -- + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- An overhead of 1,5 with 1 planes detected, will allocate 2 planes ( 1 * 1,5 ) = 1,5 => rounded up gives 2. + -- -- An overhead of 1,5 with 2 planes detected, will allocate 3 planes ( 2 * 1,5 ) = 3 => rounded up gives 3. + -- -- An overhead of 1,5 with 3 planes detected, will allocate 5 planes ( 3 * 1,5 ) = 4,5 => rounded up gives 5 planes. + -- -- An overhead of 1,5 with 4 planes detected, will allocate 6 planes ( 4 * 1,5 ) = 6 => rounded up gives 6 planes. + -- + -- local SquadronOverhead = A2GDispatcher:GetSquadronOverhead( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:GetSquadronOverhead( SquadronName ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + return DefenderSquadron.Overhead or self.DefenderDefault.Overhead + end + + + --- Sets the default grouping of new airplanes spawned. + -- Grouping will trigger how new airplanes will be grouped if more than one airplane is spawned for defense. + -- @param #AI_A2G_DISPATCHER self + -- @param #number Grouping The level of grouping that will be applied of the Patrol or GCI defenders. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Set a grouping by default per 2 airplanes. + -- A2GDispatcher:SetDefaultGrouping( 2 ) + -- + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetDefaultGrouping( Grouping ) + + self.DefenderDefault.Grouping = Grouping + + return self + end + + + --- Sets the grouping of new airplanes spawned. + -- Grouping will trigger how new airplanes will be grouped if more than one airplane is spawned for defense. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number Grouping The level of grouping that will be applied of the Patrol or GCI defenders. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Set a grouping per 2 airplanes. + -- A2GDispatcher:SetSquadronGrouping( "SquadronName", 2 ) + -- + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronGrouping( SquadronName, Grouping ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.Grouping = Grouping + + return self + end + + + --- Defines the default method at which new flights will spawn and take-off as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off in the air. + -- A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Air ) + -- + -- -- Let new flights by default take-off from the runway. + -- A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Runway ) + -- + -- -- Let new flights by default take-off from the airbase hot. + -- A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Hot ) + -- + -- -- Let new flights by default take-off from the airbase cold. + -- A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Cold ) + -- + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetDefaultTakeoff( Takeoff ) + + self.DefenderDefault.Takeoff = Takeoff + + return self + end + + --- Defines the method at which new flights will spawn and take-off as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off in the air. + -- A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Air ) + -- + -- -- Let new flights take-off from the runway. + -- A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Runway ) + -- + -- -- Let new flights take-off from the airbase hot. + -- A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Hot ) + -- + -- -- Let new flights take-off from the airbase cold. + -- A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Cold ) + -- + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetSquadronTakeoff( SquadronName, Takeoff ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.Takeoff = Takeoff + + return self + end + + + --- Gets the default method at which new flights will spawn and take-off as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @return #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off in the air. + -- local TakeoffMethod = A2GDispatcher:GetDefaultTakeoff() + -- if TakeOffMethod == , AI_A2G_Dispatcher.Takeoff.InAir then + -- ... + -- end + -- + function AI_A2G_DISPATCHER:GetDefaultTakeoff( ) + + return self.DefenderDefault.Takeoff + end + + --- Gets the method at which new flights will spawn and take-off as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @return #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off in the air. + -- local TakeoffMethod = A2GDispatcher:GetSquadronTakeoff( "SquadronName" ) + -- if TakeOffMethod == , AI_A2G_Dispatcher.Takeoff.InAir then + -- ... + -- end + -- + function AI_A2G_DISPATCHER:GetSquadronTakeoff( SquadronName ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + return DefenderSquadron.Takeoff or self.DefenderDefault.Takeoff + end + + + --- Sets flights to default take-off in the air, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off in the air. + -- A2GDispatcher:SetDefaultTakeoffInAir() + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetDefaultTakeoffInAir() + + self:SetDefaultTakeoff( AI_A2G_DISPATCHER.Takeoff.Air ) + + return self + end + + + --- Sets flights to take-off in the air, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number TakeoffAltitude (optional) The altitude in meters above the ground. If not given, the default takeoff altitude will be used. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off in the air. + -- A2GDispatcher:SetSquadronTakeoffInAir( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetSquadronTakeoffInAir( SquadronName, TakeoffAltitude ) + + self:SetSquadronTakeoff( SquadronName, AI_A2G_DISPATCHER.Takeoff.Air ) + + if TakeoffAltitude then + self:SetSquadronTakeoffInAirAltitude( SquadronName, TakeoffAltitude ) + end + + return self + end + + + --- Sets flights by default to take-off from the runway, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off from the runway. + -- A2GDispatcher:SetDefaultTakeoffFromRunway() + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetDefaultTakeoffFromRunway() + + self:SetDefaultTakeoff( AI_A2G_DISPATCHER.Takeoff.Runway ) + + return self + end + + + --- Sets flights to take-off from the runway, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off from the runway. + -- A2GDispatcher:SetSquadronTakeoffFromRunway( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetSquadronTakeoffFromRunway( SquadronName ) + + self:SetSquadronTakeoff( SquadronName, AI_A2G_DISPATCHER.Takeoff.Runway ) + + return self + end + + + --- Sets flights by default to take-off from the airbase at a hot location, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off at a hot parking spot. + -- A2GDispatcher:SetDefaultTakeoffFromParkingHot() + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetDefaultTakeoffFromParkingHot() + + self:SetDefaultTakeoff( AI_A2G_DISPATCHER.Takeoff.Hot ) + + return self + end + + --- Sets flights to take-off from the airbase at a hot location, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off in the air. + -- A2GDispatcher:SetSquadronTakeoffFromParkingHot( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetSquadronTakeoffFromParkingHot( SquadronName ) + + self:SetSquadronTakeoff( SquadronName, AI_A2G_DISPATCHER.Takeoff.Hot ) + + return self + end + + + --- Sets flights to by default take-off from the airbase at a cold location, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off from a cold parking spot. + -- A2GDispatcher:SetDefaultTakeoffFromParkingCold() + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetDefaultTakeoffFromParkingCold() + + self:SetDefaultTakeoff( AI_A2G_DISPATCHER.Takeoff.Cold ) + + return self + end + + + --- Sets flights to take-off from the airbase at a cold location, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off from a cold parking spot. + -- A2GDispatcher:SetSquadronTakeoffFromParkingCold( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetSquadronTakeoffFromParkingCold( SquadronName ) + + self:SetSquadronTakeoff( SquadronName, AI_A2G_DISPATCHER.Takeoff.Cold ) + + return self + end + + + --- Defines the default altitude where airplanes will spawn in the air and take-off as part of the defense system, when the take-off in the air method has been selected. + -- @param #AI_A2G_DISPATCHER self + -- @param #number TakeoffAltitude The altitude in meters above the ground. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Set the default takeoff altitude when taking off in the air. + -- A2GDispatcher:SetDefaultTakeoffInAirAltitude( 2000 ) -- This makes planes start at 2000 meters above the ground. + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetDefaultTakeoffInAirAltitude( TakeoffAltitude ) + + self.DefenderDefault.TakeoffAltitude = TakeoffAltitude + + return self + end + + --- Defines the default altitude where airplanes will spawn in the air and take-off as part of the defense system, when the take-off in the air method has been selected. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number TakeoffAltitude The altitude in meters above the ground. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Set the default takeoff altitude when taking off in the air. + -- A2GDispatcher:SetSquadronTakeoffInAirAltitude( "SquadronName", 2000 ) -- This makes planes start at 2000 meters above the ground. + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetSquadronTakeoffInAirAltitude( SquadronName, TakeoffAltitude ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.TakeoffAltitude = TakeoffAltitude + + return self + end + + + --- Defines the default method at which flights will land and despawn as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default despawn near the airbase when returning. + -- A2GDispatcher:SetDefaultLanding( AI_A2G_Dispatcher.Landing.NearAirbase ) + -- + -- -- Let new flights by default despawn after landing land at the runway. + -- A2GDispatcher:SetDefaultLanding( AI_A2G_Dispatcher.Landing.AtRunway ) + -- + -- -- Let new flights by default despawn after landing and parking, and after engine shutdown. + -- A2GDispatcher:SetDefaultLanding( AI_A2G_Dispatcher.Landing.AtEngineShutdown ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetDefaultLanding( Landing ) + + self.DefenderDefault.Landing = Landing + + return self + end + + + --- Defines the method at which flights will land and despawn as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights despawn near the airbase when returning. + -- A2GDispatcher:SetSquadronLanding( "SquadronName", AI_A2G_Dispatcher.Landing.NearAirbase ) + -- + -- -- Let new flights despawn after landing land at the runway. + -- A2GDispatcher:SetSquadronLanding( "SquadronName", AI_A2G_Dispatcher.Landing.AtRunway ) + -- + -- -- Let new flights despawn after landing and parking, and after engine shutdown. + -- A2GDispatcher:SetSquadronLanding( "SquadronName", AI_A2G_Dispatcher.Landing.AtEngineShutdown ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronLanding( SquadronName, Landing ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.Landing = Landing + + return self + end + + + --- Gets the default method at which flights will land and despawn as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @return #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default despawn near the airbase when returning. + -- local LandingMethod = A2GDispatcher:GetDefaultLanding( AI_A2G_Dispatcher.Landing.NearAirbase ) + -- if LandingMethod == AI_A2G_Dispatcher.Landing.NearAirbase then + -- ... + -- end + -- + function AI_A2G_DISPATCHER:GetDefaultLanding() + + return self.DefenderDefault.Landing + end + + + --- Gets the method at which flights will land and despawn as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @return #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights despawn near the airbase when returning. + -- local LandingMethod = A2GDispatcher:GetSquadronLanding( "SquadronName", AI_A2G_Dispatcher.Landing.NearAirbase ) + -- if LandingMethod == AI_A2G_Dispatcher.Landing.NearAirbase then + -- ... + -- end + -- + function AI_A2G_DISPATCHER:GetSquadronLanding( SquadronName ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + return DefenderSquadron.Landing or self.DefenderDefault.Landing + end + + + --- Sets flights by default to land and despawn near the airbase in the air, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let flights by default to land near the airbase and despawn. + -- A2GDispatcher:SetDefaultLandingNearAirbase() + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetDefaultLandingNearAirbase() + + self:SetDefaultLanding( AI_A2G_DISPATCHER.Landing.NearAirbase ) + + return self + end + + + --- Sets flights to land and despawn near the airbase in the air, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let flights to land near the airbase and despawn. + -- A2GDispatcher:SetSquadronLandingNearAirbase( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronLandingNearAirbase( SquadronName ) + + self:SetSquadronLanding( SquadronName, AI_A2G_DISPATCHER.Landing.NearAirbase ) + + return self + end + + + --- Sets flights by default to land and despawn at the runway, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let flights by default land at the runway and despawn. + -- A2GDispatcher:SetDefaultLandingAtRunway() + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetDefaultLandingAtRunway() + + self:SetDefaultLanding( AI_A2G_DISPATCHER.Landing.AtRunway ) + + return self + end + + + --- Sets flights to land and despawn at the runway, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let flights land at the runway and despawn. + -- A2GDispatcher:SetSquadronLandingAtRunway( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronLandingAtRunway( SquadronName ) + + self:SetSquadronLanding( SquadronName, AI_A2G_DISPATCHER.Landing.AtRunway ) + + return self + end + + + --- Sets flights by default to land and despawn at engine shutdown, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let flights by default land and despawn at engine shutdown. + -- A2GDispatcher:SetDefaultLandingAtEngineShutdown() + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetDefaultLandingAtEngineShutdown() + + self:SetDefaultLanding( AI_A2G_DISPATCHER.Landing.AtEngineShutdown ) + + return self + end + + + --- Sets flights to land and despawn at engine shutdown, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let flights land and despawn at engine shutdown. + -- A2GDispatcher:SetSquadronLandingAtEngineShutdown( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronLandingAtEngineShutdown( SquadronName ) + + self:SetSquadronLanding( SquadronName, AI_A2G_DISPATCHER.Landing.AtEngineShutdown ) + + return self + end + + --- Set the default fuel treshold when defenders will RTB or Refuel in the air. + -- The fuel treshold is by default set to 15%, which means that an airplane will stay in the air until 15% of its fuel has been consumed. + -- @param #AI_A2G_DISPATCHER self + -- @param #number FuelThreshold A decimal number between 0 and 1, that expresses the %-tage of the treshold of fuel remaining in the tank when the plane will go RTB or Refuel. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default fuel treshold. + -- A2GDispatcher:SetDefaultFuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. + -- + function AI_A2G_DISPATCHER:SetDefaultFuelThreshold( FuelThreshold ) + + self.DefenderDefault.FuelThreshold = FuelThreshold + + return self + end + + + --- Set the fuel treshold for the squadron when defenders will RTB or Refuel in the air. + -- The fuel treshold is by default set to 15%, which means that an airplane will stay in the air until 15% of its fuel has been consumed. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number FuelThreshold A decimal number between 0 and 1, that expresses the %-tage of the treshold of fuel remaining in the tank when the plane will go RTB or Refuel. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default fuel treshold. + -- A2GDispatcher:SetSquadronRefuelThreshold( "SquadronName", 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. + -- + function AI_A2G_DISPATCHER:SetSquadronFuelThreshold( SquadronName, FuelThreshold ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.FuelThreshold = FuelThreshold + + return self + end + + --- Set the default tanker where defenders will Refuel in the air. + -- @param #AI_A2G_DISPATCHER self + -- @param #string TankerName A string defining the group name of the Tanker as defined within the Mission Editor. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default fuel treshold. + -- A2GDispatcher:SetDefaultFuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. + -- + -- -- Now Setup the default tanker. + -- A2GDispatcher:SetDefaultTanker( "Tanker" ) -- The group name of the tanker is "Tanker" in the Mission Editor. + function AI_A2G_DISPATCHER:SetDefaultTanker( TankerName ) + + self.DefenderDefault.TankerName = TankerName + + return self + end + + + --- Set the squadron tanker where defenders will Refuel in the air. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #string TankerName A string defining the group name of the Tanker as defined within the Mission Editor. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the squadron fuel treshold. + -- A2GDispatcher:SetSquadronRefuelThreshold( "SquadronName", 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. + -- + -- -- Now Setup the squadron tanker. + -- A2GDispatcher:SetSquadronTanker( "SquadronName", "Tanker" ) -- The group name of the tanker is "Tanker" in the Mission Editor. + function AI_A2G_DISPATCHER:SetSquadronTanker( SquadronName, TankerName ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.TankerName = TankerName + + return self + end + + + + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:AddDefenderToSquadron( Squadron, Defender, Size ) + self.Defenders = self.Defenders or {} + local DefenderName = Defender:GetName() + self.Defenders[ DefenderName ] = Squadron + if Squadron.ResourceCount then + Squadron.ResourceCount = Squadron.ResourceCount - Size + end + self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } ) + end + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:RemoveDefenderFromSquadron( Squadron, Defender ) + self.Defenders = self.Defenders or {} + local DefenderName = Defender:GetName() + if Squadron.ResourceCount then + Squadron.ResourceCount = Squadron.ResourceCount + Defender:GetSize() + end + self.Defenders[ DefenderName ] = nil + self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } ) + end + + function AI_A2G_DISPATCHER:GetSquadronFromDefender( Defender ) + self.Defenders = self.Defenders or {} + local DefenderName = Defender:GetName() + self:F( { DefenderName = DefenderName } ) + return self.Defenders[ DefenderName ] + end + + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:CountPatrolAirborne( SquadronName, DefenseTaskType ) + + local PatrolCount = 0 + + local DefenderSquadron = self.DefenderSquadrons[SquadronName] + if DefenderSquadron then + for AIGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do + if DefenderTask.SquadronName == SquadronName then + if DefenderTask.Type == DefenseTaskType then + if AIGroup:IsAlive() then + -- Check if the Patrol is patrolling or engaging. If not, this is not a valid Patrol, even if it is alive! + -- The Patrol could be damaged, lost control, or out of fuel! + if DefenderTask.Fsm:Is( "Patrolling" ) or DefenderTask.Fsm:Is( "Engaging" ) or DefenderTask.Fsm:Is( "Refuelling" ) + or DefenderTask.Fsm:Is( "Started" ) then + PatrolCount = PatrolCount + 1 + end + end + end + end + end + end + + return PatrolCount + end + + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:CountDefendersEngaged( AttackerDetection ) + + -- First, count the active AIGroups Units, targetting the DetectedSet + local DefendersEngaged = 0 + local DefendersTotal = 0 + + local AttackerSet = AttackerDetection.Set + local AttackerCount = AttackerSet:Count() + local DefendersMissing = AttackerCount + --DetectedSet:Flush() + + local DefenderTasks = self:GetDefenderTasks() + for DefenderGroup, DefenderTask in pairs( DefenderTasks ) do + local Defender = DefenderGroup -- Wrapper.Group#GROUP + local DefenderTaskTarget = DefenderTask.Target + local DefenderSquadronName = DefenderTask.SquadronName + local DefenderSize = DefenderTask.Size + + -- Count the total of defenders on the battlefield. + --local DefenderSize = Defender:GetInitialSize() + if DefenderTask.Target then + --if DefenderTask.Fsm:Is( "Engaging" ) then + self:F( "Defender Group Name: " .. Defender:GetName() .. ", Size: " .. DefenderSize ) + DefendersTotal = DefendersTotal + DefenderSize + if DefenderTaskTarget and DefenderTaskTarget.Index == AttackerDetection.Index then + + local SquadronOverhead = self:GetSquadronOverhead( DefenderSquadronName ) + self:F( { SquadronOverhead = SquadronOverhead } ) + if DefenderSize then + DefendersEngaged = DefendersEngaged + DefenderSize + DefendersMissing = DefendersMissing - DefenderSize / SquadronOverhead + self:F( "Defender Group Name: " .. Defender:GetName() .. ", Size: " .. DefenderSize ) + else + DefendersEngaged = 0 + end + end + --end + end + + + end + + for QueueID, QueueItem in pairs( self.DefenseQueue ) do + local QueueItem = QueueItem -- #AI_A2G_DISPATCHER.DefenseQueueItem + if QueueItem.AttackerDetection and QueueItem.AttackerDetection.ItemID == AttackerDetection.ItemID then + DefendersMissing = DefendersMissing - QueueItem.DefendersNeeded / QueueItem.DefenderSquadron.Overhead + --DefendersEngaged = DefendersEngaged + QueueItem.DefenderGrouping + end + self:F( { QueueItemName = QueueItem.Defense, QueueItem_ItemID = QueueItem.AttackerDetection.ItemID, DetectedItem = AttackerDetection.ItemID, DefendersMissing = DefendersMissing } ) + end + + self:F( { DefenderCount = DefendersEngaged } ) + + return DefendersTotal, DefendersEngaged, DefendersMissing + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:CountDefenders( AttackerDetection, DefenderCount, DefenderTaskType ) + + local Friendlies = nil + + local AttackerSet = AttackerDetection.Set + local AttackerCount = AttackerSet:Count() + + local DefenderFriendlies = self:GetDefenderFriendliesNearBy( AttackerDetection ) + + for FriendlyDistance, DefenderFriendlyUnit in UTILS.spairs( DefenderFriendlies or {} ) do + -- We only allow to engage targets as long as the units on both sides are balanced. + if AttackerCount > DefenderCount then + local FriendlyGroup = DefenderFriendlyUnit:GetGroup() -- Wrapper.Group#GROUP + if FriendlyGroup and FriendlyGroup:IsAlive() then + -- Ok, so we have a friendly near the potential target. + -- Now we need to check if the AIGroup has a Task. + local DefenderTask = self:GetDefenderTask( FriendlyGroup ) + if DefenderTask then + -- The Task should be of the same type. + if DefenderTaskType == DefenderTask.Type then + -- If there is no target, then add the AIGroup to the ResultAIGroups for Engagement to the AttackerSet + if DefenderTask.Target == nil then + if DefenderTask.Fsm:Is( "Returning" ) + or DefenderTask.Fsm:Is( "Patrolling" ) then + Friendlies = Friendlies or {} + Friendlies[FriendlyGroup] = FriendlyGroup + DefenderCount = DefenderCount + FriendlyGroup:GetSize() + self:F( { Friendly = FriendlyGroup:GetName(), FriendlyDistance = FriendlyDistance } ) + end + end + end + end + end + else + break + end + end + + return Friendlies + end + + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ResourceActivate( DefenderSquadron, DefendersNeeded ) + + local SquadronName = DefenderSquadron.Name + DefendersNeeded = DefendersNeeded or 4 + local DefenderGrouping = DefenderSquadron.Grouping or self.DefenderDefault.Grouping + DefenderGrouping = ( DefenderGrouping < DefendersNeeded ) and DefenderGrouping or DefendersNeeded + + if self:IsSquadronVisible( SquadronName ) then + + -- Here we Patrol the new planes. + -- The Resources table is filled in advance. + local TemplateID = math.random( 1, #DefenderSquadron.Spawn ) -- Choose the template. + + -- We determine the grouping based on the parameters set. + self:F( { DefenderGrouping = DefenderGrouping } ) + + -- New we will form the group to spawn in. + -- We search for the first free resource matching the template. + local DefenderUnitIndex = 1 + local DefenderPatrolTemplate = nil + local DefenderName = nil + for GroupName, DefenderGroup in pairs( DefenderSquadron.Resources[TemplateID] or {} ) do + self:F( { GroupName = GroupName } ) + local DefenderTemplate = _DATABASE:GetGroupTemplate( GroupName ) + if DefenderUnitIndex == 1 then + DefenderPatrolTemplate = UTILS.DeepCopy( DefenderTemplate ) + self.DefenderPatrolIndex = self.DefenderPatrolIndex + 1 + --DefenderPatrolTemplate.name = SquadronName .. "#" .. self.DefenderPatrolIndex .. "#" .. GroupName + DefenderPatrolTemplate.name = GroupName + DefenderName = DefenderPatrolTemplate.name + else + -- Add the unit in the template to the DefenderPatrolTemplate. + local DefenderUnitTemplate = DefenderTemplate.units[1] + DefenderPatrolTemplate.units[DefenderUnitIndex] = DefenderUnitTemplate + end + DefenderPatrolTemplate.units[DefenderUnitIndex].name = string.format( DefenderPatrolTemplate.name .. '-%02d', DefenderUnitIndex ) + DefenderPatrolTemplate.units[DefenderUnitIndex].unitId = nil + DefenderUnitIndex = DefenderUnitIndex + 1 + DefenderSquadron.Resources[TemplateID][GroupName] = nil + if DefenderUnitIndex > DefenderGrouping then + break + end + + end + + if DefenderPatrolTemplate then + local TakeoffMethod = self:GetSquadronTakeoff( SquadronName ) + local SpawnGroup = GROUP:Register( DefenderName ) + DefenderPatrolTemplate.lateActivation = nil + DefenderPatrolTemplate.uncontrolled = nil + local Takeoff = self:GetSquadronTakeoff( SquadronName ) + DefenderPatrolTemplate.route.points[1].type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type + DefenderPatrolTemplate.route.points[1].action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action + local Defender = _DATABASE:Spawn( DefenderPatrolTemplate ) + self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping ) + Defender:Activate() + return Defender, DefenderGrouping + end + else + local Spawn = DefenderSquadron.Spawn[ math.random( 1, #DefenderSquadron.Spawn ) ] -- Core.Spawn#SPAWN + if DefenderGrouping then + Spawn:InitGrouping( DefenderGrouping ) + else + Spawn:InitGrouping() + end + + local TakeoffMethod = self:GetSquadronTakeoff( SquadronName ) + local Defender = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, TakeoffMethod, DefenderSquadron.TakeoffAltitude or self.DefenderDefault.TakeoffAltitude ) -- Wrapper.Group#GROUP + self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping ) + return Defender, DefenderGrouping + end + + return nil, nil + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:onafterPatrol( From, Event, To, SquadronName, DefenseTaskType ) + + local DefenderSquadron, Patrol = self:CanPatrol( SquadronName, DefenseTaskType ) + + if Patrol then + self:ResourceQueue( true, DefenderSquadron, nil, Patrol, DefenseTaskType, nil, SquadronName ) + end + + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ResourceQueue( Patrol, DefenderSquadron, DefendersNeeded, Defense, DefenseTaskType, AttackerDetection, SquadronName ) + + self:F( { DefenderSquadron, DefendersNeeded, Defense, DefenseTaskType, AttackerDetection, SquadronName } ) + + local DefenseQueueItem = {} -- #AI_A2G_DISPATCHER.DefenderQueueItem + + + DefenseQueueItem.Patrol = Patrol + DefenseQueueItem.DefenderSquadron = DefenderSquadron + DefenseQueueItem.DefendersNeeded = DefendersNeeded + DefenseQueueItem.Defense = Defense + DefenseQueueItem.DefenseTaskType = DefenseTaskType + DefenseQueueItem.AttackerDetection = AttackerDetection + DefenseQueueItem.SquadronName = SquadronName + + table.insert( self.DefenseQueue, DefenseQueueItem ) + self:F( { QueueItems = #self.DefenseQueue } ) + + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ResourceTakeoff() + + for DefenseQueueID, DefenseQueueItem in pairs( self.DefenseQueue ) do + self:F( { DefenseQueueID } ) + end + + for SquadronName, Squadron in pairs( self.DefenderSquadrons ) do + + if #self.DefenseQueue > 0 then + + self:F( { SquadronName, Squadron.Name, Squadron.TakeoffTime, Squadron.TakeoffInterval, timer.getTime() } ) + + local DefenseQueueItem = self.DefenseQueue[1] + self:F( {DefenderSquadron=DefenseQueueItem.DefenderSquadron} ) + + if DefenseQueueItem.SquadronName == SquadronName then + + if Squadron.TakeoffTime + Squadron.TakeoffInterval < timer.getTime() then + Squadron.TakeoffTime = timer.getTime() + + if DefenseQueueItem.Patrol == true then + self:ResourcePatrol( DefenseQueueItem.DefenderSquadron, DefenseQueueItem.DefendersNeeded, DefenseQueueItem.Defense, DefenseQueueItem.DefenseTaskType, DefenseQueueItem.AttackerDetection, DefenseQueueItem.SquadronName ) + else + self:ResourceEngage( DefenseQueueItem.DefenderSquadron, DefenseQueueItem.DefendersNeeded, DefenseQueueItem.Defense, DefenseQueueItem.DefenseTaskType, DefenseQueueItem.AttackerDetection, DefenseQueueItem.SquadronName ) + end + table.remove( self.DefenseQueue, 1 ) + end + end + end + + end + + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ResourcePatrol( DefenderSquadron, DefendersNeeded, Patrol, DefenseTaskType, AttackerDetection, SquadronName ) + + + self:F({DefenderSquadron=DefenderSquadron}) + self:F({DefendersNeeded=DefendersNeeded}) + self:F({Patrol=Patrol}) + self:F({DefenseTaskType=DefenseTaskType}) + self:F({AttackerDetection=AttackerDetection}) + self:F({SquadronName=SquadronName}) + + local DefenderGroup, DefenderGrouping = self:ResourceActivate( DefenderSquadron, DefendersNeeded ) + + if DefenderGroup then + + local AI_A2G_PATROL = { SEAD = AI_A2G_SEAD, BAI = AI_A2G_BAI, CAS = AI_A2G_CAS } + + local Fsm = AI_A2G_PATROL[DefenseTaskType]:New( DefenderGroup, Patrol.EngageMinSpeed, Patrol.EngageMaxSpeed, Patrol.EngageFloorAltitude, Patrol.EngageCeilingAltitude, Patrol.Zone, Patrol.PatrolFloorAltitude, Patrol.PatrolCeilingAltitude, Patrol.PatrolMinSpeed, Patrol.PatrolMaxSpeed, Patrol.AltType ) + Fsm:SetDispatcher( self ) + Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) + Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) + Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold ) + Fsm:SetDisengageRadius( self.DisengageRadius ) + Fsm:SetTanker( DefenderSquadron.TankerName or self.DefenderDefault.TankerName ) + Fsm:Start() + + self:SetDefenderTask( SquadronName, DefenderGroup, DefenseTaskType, Fsm, nil, DefenderGrouping ) + + function Fsm:onafterTakeoff( Defender, From, Event, To ) + self:F({"Defender Birth", Defender:GetName()}) + --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) + + local DefenderName = Defender:GetName() + local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + + if Squadron then + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " airborne." ) + Fsm:Patrol() -- Engage on the TargetSetUnit + end + end + + function Fsm:onafterRTB( Defender, From, Event, To ) + self:F({"Defender RTB", Defender:GetName()}) + self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) + + local DefenderName = Defender:GetName() + local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " returning." ) + + Dispatcher:ClearDefenderTaskTarget( Defender ) + end + + --- @param #AI_A2G_DISPATCHER self + function Fsm:onafterLostControl( Defender, From, Event, To ) + self:F({"Defender LostControl", Defender:GetName()}) + self:GetParent(self).onafterHome( self, Defender, From, Event, To ) + + local DefenderName = Defender:GetName() + local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " lost control." ) + if Defender:IsAboveRunway() then + Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) + Defender:Destroy() + end + end + + --- @param #AI_A2G_DISPATCHER self + function Fsm:onafterHome( Defender, From, Event, To, Action ) + self:F({"Defender Home", Defender:GetName()}) + self:GetParent(self).onafterHome( self, Defender, From, Event, To ) + + local DefenderName = Defender:GetName() + local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " landing." ) + + if Action and Action == "Destroy" then + Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) + Defender:Destroy() + end + + if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2G_DISPATCHER.Landing.NearAirbase then + Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) + Defender:Destroy() + Dispatcher:ResourcePark( Squadron, Defender ) + end + end + end + + end + + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ResourceEngage( DefenderSquadron, DefendersNeeded, Defense, DefenseTaskType, AttackerDetection, SquadronName ) + + self:F({DefenderSquadron=DefenderSquadron}) + self:F({DefendersNeeded=DefendersNeeded}) + self:F({Defense=Defense}) + self:F({DefenseTaskType=DefenseTaskType}) + self:F({AttackerDetection=AttackerDetection}) + self:F({SquadronName=SquadronName}) + + local DefenderGroup, DefenderGrouping = self:ResourceActivate( DefenderSquadron, DefendersNeeded ) + + if DefenderGroup then + + local AI_A2G = { SEAD = AI_A2G_SEAD, BAI = AI_A2G_BAI, CAS = AI_A2G_CAS } + + local Fsm = AI_A2G[DefenseTaskType]:New( DefenderGroup, Defense.EngageMinSpeed, Defense.EngageMaxSpeed, Defense.EngageFloorAltitude, Defense.EngageCeilingAltitude ) -- AI.AI_A2G_ENGAGE + Fsm:SetDispatcher( self ) + Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) + Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) + Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold ) + Fsm:SetDisengageRadius( self.DisengageRadius ) + Fsm:Start() + + self:SetDefenderTask( SquadronName, DefenderGroup, DefenseTaskType, Fsm, AttackerDetection, DefenderGrouping ) + + function Fsm:onafterTakeoff( Defender, From, Event, To ) + self:F({"Defender Birth", Defender:GetName()}) + --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) + + local DefenderName = Defender:GetName() + local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + local DefenderTarget = Dispatcher:GetDefenderTaskTarget( Defender ) + + self:F( { DefenderTarget = DefenderTarget } ) + + if DefenderTarget then + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " airborne." ) + Fsm:Engage( DefenderTarget.Set ) -- Engage on the TargetSetUnit + end + end + + function Fsm:onafterRTB( Defender, From, Event, To ) + self:F({"Defender RTB", Defender:GetName()}) + + local DefenderName = Defender:GetName() + local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " returning." ) + + self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) + + Dispatcher:ClearDefenderTaskTarget( Defender ) + end + + --- @param #AI_A2G_DISPATCHER self + function Fsm:onafterLostControl( Defender, From, Event, To ) + self:F({"Defender LostControl", Defender:GetName()}) + self:GetParent(self).onafterHome( self, Defender, From, Event, To ) + + local DefenderName = Defender:GetName() + local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " lost control." ) + + if Defender:IsAboveRunway() then + Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) + Defender:Destroy() + end + end + + --- @param #AI_A2G_DISPATCHER self + function Fsm:onafterHome( Defender, From, Event, To, Action ) + self:F({"Defender Home", Defender:GetName()}) + self:GetParent(self).onafterHome( self, Defender, From, Event, To ) + + local DefenderName = Defender:GetName() + local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " landing." ) + + if Action and Action == "Destroy" then + Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) + Defender:Destroy() + end + + if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2G_DISPATCHER.Landing.NearAirbase then + Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) + Defender:Destroy() + Dispatcher:ResourcePark( Squadron, Defender ) + end + end + end + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:onafterEngage( From, Event, To, AttackerDetection, Defenders ) + + if Defenders then + + for DefenderID, Defender in pairs( Defenders or {} ) do + + local Fsm = self:GetDefenderTaskFsm( Defender ) + Fsm:Engage( AttackerDetection.Set ) -- Engage on the TargetSetUnit + + self:SetDefenderTaskTarget( Defender, AttackerDetection ) + + end + end + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:onafterDefend( From, Event, To, AttackerDetection, DefendersTotal, DefendersEngaged, DefendersMissing, DefenderFriendlies, DefenseTaskType ) + + self:F( { From, Event, To, AttackerDetection.Index, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing, DefenderFriendlies = DefenderFriendlies } ) + + AttackerDetection.Type = DefenseTaskType -- This is set to report the task type in the status panel. + + local AttackerSet = AttackerDetection.Set + local AttackerUnit = AttackerSet:GetFirst() + + if AttackerUnit and AttackerUnit:IsAlive() then + local AttackerCount = AttackerSet:Count() + local DefenderCount = 0 + + for DefenderID, DefenderGroup in pairs( DefenderFriendlies or {} ) do + + local SquadronName = self:GetDefenderTask( DefenderGroup ).SquadronName + local SquadronOverhead = self:GetSquadronOverhead( SquadronName ) + + local Fsm = self:GetDefenderTaskFsm( DefenderGroup ) + Fsm:Engage( AttackerSet ) -- Engage on the TargetSetUnit + + self:SetDefenderTaskTarget( DefenderGroup, AttackerDetection ) + + local DefenderGroupSize = DefenderGroup:GetSize() + DefendersMissing = DefendersMissing - DefenderGroupSize / SquadronOverhead + DefendersTotal = DefendersTotal + DefenderGroupSize / SquadronOverhead + + if DefendersMissing <= 0 then + break + end + end + + self:F( { DefenderCount = DefenderCount, DefendersMissing = DefendersMissing } ) + DefenderCount = DefendersMissing + + local ClosestDistance = 0 + local ClosestDefenderSquadronName = nil + + local BreakLoop = false + + while( DefenderCount > 0 and not BreakLoop ) do + + self:F( { DefenderSquadrons = self.DefenderSquadrons } ) + + for SquadronName, DefenderSquadron in pairs( self.DefenderSquadrons or {} ) do + + if DefenderSquadron[DefenseTaskType] then + + local SpawnCoord = DefenderSquadron.Airbase:GetCoordinate() -- Core.Point#COORDINATE + local AttackerCoord = AttackerUnit:GetCoordinate() + local InterceptCoord = AttackerDetection.InterceptCoord + self:F( { InterceptCoord = InterceptCoord } ) + if InterceptCoord then + local InterceptDistance = SpawnCoord:Get2DDistance( InterceptCoord ) + local AirbaseDistance = SpawnCoord:Get2DDistance( AttackerCoord ) + self:F( { InterceptDistance = InterceptDistance, AirbaseDistance = AirbaseDistance, InterceptCoord = InterceptCoord } ) + + if ClosestDistance == 0 or InterceptDistance < ClosestDistance then + + -- Only intercept if the distance to target is smaller or equal to the GciRadius limit. + if AirbaseDistance <= self.DefenseRadius then + ClosestDistance = InterceptDistance + ClosestDefenderSquadronName = SquadronName + end + end + end + end + end + + if ClosestDefenderSquadronName then + + local DefenderSquadron, Defense = self:CanDefend( ClosestDefenderSquadronName, DefenseTaskType ) + + if Defense then + + local DefenderOverhead = DefenderSquadron.Overhead or self.DefenderDefault.Overhead + local DefenderGrouping = DefenderSquadron.Grouping or self.DefenderDefault.Grouping + local DefendersNeeded = math.ceil( DefenderCount * DefenderOverhead ) + + self:F( { Overhead = DefenderOverhead, SquadronOverhead = DefenderSquadron.Overhead , DefaultOverhead = self.DefenderDefault.Overhead } ) + self:F( { Grouping = DefenderGrouping, SquadronGrouping = DefenderSquadron.Grouping, DefaultGrouping = self.DefenderDefault.Grouping } ) + self:F( { DefendersCount = DefenderCount, DefendersNeeded = DefendersNeeded } ) + + -- Validate that the maximum limit of Defenders has been reached. + -- If yes, then cancel the engaging of more defenders. + local DefendersLimit = DefenderSquadron.EngageLimit or self.DefenderDefault.EngageLimit + if DefendersLimit then + if DefendersTotal >= DefendersLimit then + DefendersNeeded = 0 + BreakLoop = true + else + -- If the total of amount of defenders + the defenders needed, is larger than the limit of defenders, + -- then the defenders needed is the difference between defenders total - defenders limit. + if DefendersTotal + DefendersNeeded > DefendersLimit then + DefendersNeeded = DefendersLimit - DefendersTotal + end + end + end + + -- DefenderSquadron.ResourceCount can have the value nil, which expresses unlimited resources. + -- DefendersNeeded cannot exceed DefenderSquadron.ResourceCount! + if DefenderSquadron.ResourceCount and DefendersNeeded > DefenderSquadron.ResourceCount then + DefendersNeeded = DefenderSquadron.ResourceCount + BreakLoop = true + end + + while ( DefendersNeeded > 0 ) do + self:ResourceQueue( false, DefenderSquadron, DefendersNeeded, Defense, DefenseTaskType, AttackerDetection, ClosestDefenderSquadronName ) + DefendersNeeded = DefendersNeeded - DefenderGrouping + DefenderCount = DefenderCount - DefenderGrouping / DefenderOverhead + end -- while ( DefendersNeeded > 0 ) do + else + -- No more resources, try something else. + -- Subject for a later enhancement to try to depart from another squadron and disable this one. + BreakLoop = true + break + end + else + -- There isn't any closest airbase anymore, break the loop. + break + end + end -- if DefenderSquadron then + end -- if AttackerUnit + end + + + + --- Creates an SEAD task when the targets have radars. + -- @param #AI_A2G_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item. + -- @return Core.Set#SET_UNIT The set of units of the targets to be engaged. + -- @return #nil If there are no targets to be set. + function AI_A2G_DISPATCHER:Evaluate_SEAD( DetectedItem ) + self:F( { DetectedItem.ItemID } ) + + local AttackerSet = DetectedItem.Set -- Core.Set#SET_UNIT + local AttackerCount = AttackerSet:Count() + local IsSEAD = AttackerSet:HasSEAD() -- Is the AttackerSet a SEAD group? + + if ( IsSEAD > 0 ) then + + -- First, count the active defenders, engaging the DetectedItem. + local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem ) + + self:F( { AttackerCount = AttackerCount, DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) + + local DefenderGroups = self:CountDefenders( DetectedItem, DefendersEngaged, "SEAD" ) + + + + if DetectedItem.IsDetected == true then + + return DefendersTotal, DefendersEngaged, DefendersMissing, DefenderGroups + end + end + + return nil, nil, nil + end + + + --- Creates an CAS task. + -- @param #AI_A2G_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item. + -- @return Core.Set#SET_UNIT The set of units of the targets to be engaged. + -- @return #nil If there are no targets to be set. + function AI_A2G_DISPATCHER:Evaluate_CAS( DetectedItem ) + self:F( { DetectedItem.ItemID } ) + + local AttackerSet = DetectedItem.Set -- Core.Set#SET_UNIT + local AttackerCount = AttackerSet:Count() + local AttackerRadarCount = AttackerSet:HasSEAD() + local IsFriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) + local IsCas = ( AttackerRadarCount == 0 ) and ( IsFriendliesNearBy == true ) -- Is the AttackerSet a CAS group? + + if IsCas == true then + + -- First, count the active defenders, engaging the DetectedItem. + local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem ) + + self:F( { AttackerCount = AttackerCount, DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) + + local DefenderGroups = self:CountDefenders( DetectedItem, DefendersEngaged, "CAS" ) + + if DetectedItem.IsDetected == true then + + return DefendersTotal, DefendersEngaged, DefendersMissing, DefenderGroups + end + end + + return nil, nil, nil + end + + + --- Evaluates an BAI task. + -- @param #AI_A2G_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item. + -- @return Core.Set#SET_UNIT The set of units of the targets to be engaged. + -- @return #nil If there are no targets to be set. + function AI_A2G_DISPATCHER:Evaluate_BAI( DetectedItem ) + self:F( { DetectedItem.ItemID } ) + + local AttackerSet = DetectedItem.Set -- Core.Set#SET_UNIT + local AttackerCount = AttackerSet:Count() + local AttackerRadarCount = AttackerSet:HasSEAD() + local IsFriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) + local IsBai = ( AttackerRadarCount == 0 ) and ( IsFriendliesNearBy == false ) -- Is the AttackerSet a BAI group? + + if IsBai == true then + + -- First, count the active defenders, engaging the DetectedItem. + local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem ) + + self:F( { AttackerCount = AttackerCount, DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) + + local DefenderGroups = self:CountDefenders( DetectedItem, DefendersEngaged, "BAI" ) + + if DetectedItem.IsDetected == true then + + return DefendersTotal, DefendersEngaged, DefendersMissing, DefenderGroups + end + end + + return nil, nil, nil + end + + + --- Assigns A2G AI Tasks in relation to the detected items. + -- @param #AI_A2G_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Functional.Detection#DETECTION_BASE} derived object. + -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop. + function AI_A2G_DISPATCHER:ProcessDetected( Detection ) + + local AreaMsg = {} + local TaskMsg = {} + local ChangeMsg = {} + + local TaskReport = REPORT:New() + + + for DefenderGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do + local DefenderGroup = DefenderGroup -- Wrapper.Group#GROUP + local DefenderTaskFsm = self:GetDefenderTaskFsm( DefenderGroup ) + --if DefenderTaskFsm:Is( "LostControl" ) then + -- self:ClearDefenderTask( DefenderGroup ) + --end + if not DefenderGroup:IsAlive() then + self:F( { Defender = DefenderGroup:GetName(), DefenderState = DefenderTaskFsm:GetState() } ) + if not DefenderTaskFsm:Is( "Started" ) then + self:ClearDefenderTask( DefenderGroup ) + end + else + if DefenderTask.Target then + local AttackerItem = Detection:GetDetectedItemByIndex( DefenderTask.Target.Index ) + if not AttackerItem then + self:F( { "Removing obsolete Target:", DefenderTask.Target.Index } ) + self:ClearDefenderTaskTarget( DefenderGroup ) + else + if DefenderTask.Target.Set then + local TargetCount = DefenderTask.Target.Set:Count() + if TargetCount == 0 then + self:F( { "All Targets destroyed in Target, removing:", DefenderTask.Target.Index } ) + self:ClearDefenderTask( DefenderGroup ) + end + end + end + end + end + end + + local Report = REPORT:New( "\nTactical Overview" ) + + local DefenderGroupCount = 0 + local Delay = 0 -- We need to implement a delay for each action because the spawning on airbases get confused if done too quick. + + local DefendersTotal = 0 + + -- Now that all obsolete tasks are removed, loop through the detected targets. + for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do + + local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem + local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT + local DetectedCount = DetectedSet:Count() + local DetectedZone = DetectedItem.Zone + + self:F( { "Target ID", DetectedItem.ItemID } ) + DetectedSet:Flush( self ) + + local DetectedID = DetectedItem.ID + local DetectionIndex = DetectedItem.Index + local DetectedItemChanged = DetectedItem.Changed + + local AttackerCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem ) + + -- Calculate if for this DetectedItem if a defense needs to be initiated. + -- This calculation is based on the distance between the defense point and the attackers, and the defensiveness parameter. + -- The attackers closest to the defense coordinates will be handled first, or course! + + local DefenseCoordinate = nil + + for DefenseCoordinateName, EvaluateCoordinate in pairs( self.DefenseCoordinates ) do + + local EvaluateDistance = AttackerCoordinate:Get2DDistance( EvaluateCoordinate ) + + if EvaluateDistance <= self.DefenseRadius then + + local DistanceProbability = ( self.DefenseRadius / EvaluateDistance * self.DefenseReactivity ) + local DefenseProbability = math.random() + + self:F( { DistanceProbability = DistanceProbability, DefenseProbability = DefenseProbability } ) + + if DefenseProbability <= DistanceProbability / ( 300 / 30 ) then + DefenseCoordinate = EvaluateCoordinate + break + end + end + end + + if DefenseCoordinate then + do + local DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies = self:Evaluate_SEAD( DetectedItem ) -- Returns a SET_UNIT with the SEAD targets to be engaged... + if DefendersMissing and DefendersMissing > 0 then + self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) + self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "SEAD", DefenseCoordinate ) + Delay = Delay + 1 + end + end + + do + local DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies = self:Evaluate_CAS( DetectedItem ) -- Returns a SET_UNIT with the CAS targets to be engaged... + if DefendersMissing and DefendersMissing > 0 then + self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) + self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "CAS", DefenseCoordinate ) + Delay = Delay + 1 + end + end + + do + local DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies = self:Evaluate_BAI( DetectedItem ) -- Returns a SET_UNIT with the CAS targets to be engaged... + if DefendersMissing and DefendersMissing > 0 then + self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) + self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "BAI", DefenseCoordinate ) + Delay = Delay + 1 + end + end + end + +-- do +-- local DefendersMissing, Friendlies = self:Evaluate_CAS( DetectedItem ) +-- if DefendersMissing and DefendersMissing > 0 then +-- self:F( { DefendersMissing = DefendersMissing } ) +-- self:CAS( DetectedItem, DefendersMissing, Friendlies ) +-- end +-- end + + if self.TacticalDisplay then + -- Show tactical situation + Report:Add( string.format( " - %s ( %s ): ( #%d - %4s ) %s" , DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Type or " --- ", DetectedItem.Set:GetObjectNames() ) ) + for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do + local Defender = Defender -- Wrapper.Group#GROUP + if DefenderTask.Target and DefenderTask.Target.Index == DetectedItem.Index then + if Defender:IsAlive() then + DefenderGroupCount = DefenderGroupCount + 1 + local Fuel = Defender:GetFuelMin() * 100 + local Damage = Defender:GetLife() / Defender:GetLife0() * 100 + Report:Add( string.format( " - %s ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", + Defender:GetName(), + DefenderTask.Type, + DefenderTask.Fsm:GetState(), + Defender:GetSize(), + Fuel, + Damage, + Defender:HasTask() == true and "Executing" or "Idle" ) ) + end + end + end + end + end + + if self.TacticalDisplay then + Report:Add( "\n - No Targets:") + local TaskCount = 0 + for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do + TaskCount = TaskCount + 1 + local Defender = Defender -- Wrapper.Group#GROUP + if not DefenderTask.Target then + if Defender:IsAlive() then + local DefenderHasTask = Defender:HasTask() + local Fuel = Defender:GetFuelMin() * 100 + local Damage = Defender:GetLife() / Defender:GetLife0() * 100 + DefenderGroupCount = DefenderGroupCount + 1 + Report:Add( string.format( " - %s ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", + Defender:GetName(), + DefenderTask.Type, + DefenderTask.Fsm:GetState(), + Defender:GetSize(), + Fuel, + Damage, + Defender:HasTask() == true and "Executing" or "Idle" ) ) + end + end + end + Report:Add( string.format( "\n - %d Tasks - %d Defender Groups", TaskCount, DefenderGroupCount ) ) + + Report:Add( string.format( "\n - %d Queued Aircraft Launches", #self.DefenseQueue ) ) + for DefenseQueueID, DefenseQueueItem in pairs( self.DefenseQueue ) do + local DefenseQueueItem = DefenseQueueItem -- #AI_A2G_DISPATCHER.DefenseQueueItem + Report:Add( string.format( " - %s - %s", DefenseQueueItem.SquadronName, DefenseQueueItem.DefenderSquadron.TakeoffTime, DefenseQueueItem.DefenderSquadron.TakeoffInterval) ) + + end + + self:F( Report:Text( "\n" ) ) + trigger.action.outText( Report:Text( "\n" ), 25 ) + end + + return true + end + +end + +do + + --- Calculates which HUMAN friendlies are nearby the area. + -- @param #AI_A2G_DISPATCHER self + -- @param DetectedItem The detected item. + -- @return #number, Core.Report#REPORT The amount of friendlies and a text string explaining which friendlies of which type. + function AI_A2G_DISPATCHER:GetPlayerFriendliesNearBy( DetectedItem ) + + local DetectedSet = DetectedItem.Set + local PlayersNearBy = self.Detection:GetPlayersNearBy( DetectedItem ) + + local PlayerTypes = {} + local PlayersCount = 0 + + if PlayersNearBy then + local DetectedTreatLevel = DetectedSet:CalculateThreatLevelA2G() + for PlayerUnitName, PlayerUnitData in pairs( PlayersNearBy ) do + local PlayerUnit = PlayerUnitData -- Wrapper.Unit#UNIT + local PlayerName = PlayerUnit:GetPlayerName() + --self:F( { PlayerName = PlayerName, PlayerUnit = PlayerUnit } ) + if PlayerUnit:IsAirPlane() and PlayerName ~= nil then + local FriendlyUnitThreatLevel = PlayerUnit:GetThreatLevel() + PlayersCount = PlayersCount + 1 + local PlayerType = PlayerUnit:GetTypeName() + PlayerTypes[PlayerName] = PlayerType + if DetectedTreatLevel < FriendlyUnitThreatLevel + 2 then + end + end + end + + end + + --self:F( { PlayersCount = PlayersCount } ) + + local PlayerTypesReport = REPORT:New() + + if PlayersCount > 0 then + for PlayerName, PlayerType in pairs( PlayerTypes ) do + PlayerTypesReport:Add( string.format('"%s" in %s', PlayerName, PlayerType ) ) + end + else + PlayerTypesReport:Add( "-" ) + end + + + return PlayersCount, PlayerTypesReport + end + + --- Calculates which friendlies are nearby the area. + -- @param #AI_A2G_DISPATCHER self + -- @param DetectedItem The detected item. + -- @return #number, Core.Report#REPORT The amount of friendlies and a text string explaining which friendlies of which type. + function AI_A2G_DISPATCHER:GetFriendliesNearBy( DetectedItem ) + + local DetectedSet = DetectedItem.Set + local FriendlyUnitsNearBy = self.Detection:GetFriendliesNearBy( DetectedItem ) + + local FriendlyTypes = {} + local FriendliesCount = 0 + + if FriendlyUnitsNearBy then + local DetectedTreatLevel = DetectedSet:CalculateThreatLevelA2G() + for FriendlyUnitName, FriendlyUnitData in pairs( FriendlyUnitsNearBy ) do + local FriendlyUnit = FriendlyUnitData -- Wrapper.Unit#UNIT + if FriendlyUnit:IsAirPlane() then + local FriendlyUnitThreatLevel = FriendlyUnit:GetThreatLevel() + FriendliesCount = FriendliesCount + 1 + local FriendlyType = FriendlyUnit:GetTypeName() + FriendlyTypes[FriendlyType] = FriendlyTypes[FriendlyType] and ( FriendlyTypes[FriendlyType] + 1 ) or 1 + if DetectedTreatLevel < FriendlyUnitThreatLevel + 2 then + end + end + end + + end + + --self:F( { FriendliesCount = FriendliesCount } ) + + local FriendlyTypesReport = REPORT:New() + + if FriendliesCount > 0 then + for FriendlyType, FriendlyTypeCount in pairs( FriendlyTypes ) do + FriendlyTypesReport:Add( string.format("%d of %s", FriendlyTypeCount, FriendlyType ) ) + end + else + FriendlyTypesReport:Add( "-" ) + end + + + return FriendliesCount, FriendlyTypesReport + end + + --- Schedules a new Patrol for the given SquadronName. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + function AI_A2G_DISPATCHER:SchedulerPatrol( SquadronName ) + local PatrolTaskTypes = { "SEAD", "CAS", "BAI" } + local PatrolTaskType = PatrolTaskTypes[math.random(1,3)] + self:Patrol( SquadronName, PatrolTaskType ) + end + +end + +do + + --- @type AI_A2G_GCICAP + -- @extends #AI_A2G_DISPATCHER + + --- Create an automatic air defence system for a coalition setting up GCI and CAP air defenses. + -- The class derives from @{#AI_A2G_DISPATCHER} and thus, all the methods that are defined in the @{#AI_A2G_DISPATCHER} class, can be used also in AI\_A2G\_GCICAP. + -- + -- === + -- + -- # Demo Missions + -- + -- ### [AI\_A2G\_GCICAP for Caucasus](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-200%20-%20AI_A2G%20-%20GCICAP%20Demonstration) + -- ### [AI\_A2G\_GCICAP for NTTR](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-210%20-%20NTTR%20AI_A2G_GCICAP%20Demonstration) + -- ### [AI\_A2G\_GCICAP for Normandy](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-220%20-%20NORMANDY%20AI_A2G_GCICAP%20Demonstration) + -- + -- ### [AI\_A2G\_GCICAP for beta testers](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching) + -- + -- === + -- + -- # YouTube Channel + -- + -- ### [DCS WORLD - MOOSE - A2G GCICAP - Build an automatic A2G Defense System](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0S4KMNUUJpaUs6zZHjLKNx) + -- + -- === + -- + -- ![Banner Image](..\Presentations\AI_A2G_DISPATCHER\Dia3.JPG) + -- + -- AI\_A2G\_GCICAP includes automatic spawning of Combat Air Patrol aircraft (CAP) and Ground Controlled Intercept aircraft (GCI) in response to enemy + -- air movements that are detected by an airborne or ground based radar network. + -- + -- With a little time and with a little work it provides the mission designer with a convincing and completely automatic air defence system. + -- + -- The AI_A2G_GCICAP provides a lightweight configuration method using the mission editor. Within a very short time, and with very little coding, + -- the mission designer is able to configure a complete A2G defense system for a coalition using the DCS Mission Editor available functions. + -- Using the DCS Mission Editor, you define borders of the coalition which are guarded by GCICAP, + -- configure airbases to belong to the coalition, define squadrons flying certain types of planes or payloads per airbase, and define CAP zones. + -- **Very little lua needs to be applied, a one liner**, which is fully explained below, which can be embedded + -- right in a DO SCRIPT trigger action or in a larger DO SCRIPT FILE trigger action. + -- + -- CAP flights will take off and proceed to designated CAP zones where they will remain on station until the ground radars direct them to intercept + -- detected enemy aircraft or they run short of fuel and must return to base (RTB). + -- + -- When a CAP flight leaves their zone to perform a GCI or return to base a new CAP flight will spawn to take its place. + -- If all CAP flights are engaged or RTB then additional GCI interceptors will scramble to intercept unengaged enemy aircraft under ground radar control. + -- + -- In short it is a plug in very flexible and configurable air defence module for DCS World. + -- + -- === + -- + -- # The following actions need to be followed when using AI\_A2G\_GCICAP in your mission: + -- + -- ## 1) Configure a working AI\_A2G\_GCICAP defense system for ONE coalition. + -- + -- ### 1.1) Define which airbases are for which coalition. + -- + -- ![Mission Editor Action](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_GCICAP-ME_1.JPG) + -- + -- Color the airbases red or blue. You can do this by selecting the airbase on the map, and select the coalition blue or red. + -- + -- ### 1.2) Place groups of units given a name starting with a **EWR prefix** of your choice to build your EWR network. + -- + -- ![Mission Editor Action](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_GCICAP-ME_2.JPG) + -- + -- **All EWR groups starting with the EWR prefix (text) will be included in the detection system.** + -- + -- An EWR network, or, Early Warning Radar network, is used to early detect potential airborne targets and to understand the position of patrolling targets of the enemy. + -- Typically EWR networks are setup using 55G6 EWR, 1L13 EWR, Hawk sr and Patriot str ground based radar units. + -- These radars have different ranges and 55G6 EWR and 1L13 EWR radars are Eastern Bloc units (eg Russia, Ukraine, Georgia) while the Hawk and Patriot radars are Western (eg US). + -- Additionally, ANY other radar capable unit can be part of the EWR network! + -- Also AWACS airborne units, planes, helicopters can help to detect targets, as long as they have radar. + -- The position of these units is very important as they need to provide enough coverage + -- to pick up enemy aircraft as they approach so that CAP and GCI flights can be tasked to intercept them. + -- + -- Additionally in a hot war situation where the border is no longer respected the placement of radars has a big effect on how fast the war escalates. + -- For example if they are a long way forward and can detect enemy planes on the ground and taking off + -- they will start to vector CAP and GCI flights to attack them straight away which will immediately draw a response from the other coalition. + -- Having the radars further back will mean a slower escalation because fewer targets will be detected and + -- therefore less CAP and GCI flights will spawn and this will tend to make just the border area active rather than a melee over the whole map. + -- It all depends on what the desired effect is. + -- + -- EWR networks are **dynamically maintained**. By defining in a **smart way the names or name prefixes of the groups** with EWR capable units, these groups will be **automatically added or deleted** from the EWR network, + -- increasing or decreasing the radar coverage of the Early Warning System. + -- + -- ### 1.3) Place Airplane or Helicopter Groups with late activation switched on above the airbases to define Squadrons. + -- + -- ![Mission Editor Action](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_GCICAP-ME_3.JPG) + -- + -- These are **templates**, with a given name starting with a **Template prefix** above each airbase that you wanna have a squadron. + -- These **templates** need to be within 1.5km from the airbase center. They don't need to have a slot at the airplane, they can just be positioned above the airbase, + -- without a route, and should only have ONE unit. + -- + -- ![Mission Editor Action](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_GCICAP-ME_4.JPG) + -- + -- **All airplane or helicopter groups that are starting with any of the choosen Template Prefixes will result in a squadron created at the airbase.** + -- + -- ### 1.4) Place floating helicopters to create the CAP zones defined by its route points. + -- + -- ![Mission Editor Action](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_GCICAP-ME_5.JPG) + -- + -- **All airplane or helicopter groups that are starting with any of the choosen Template Prefixes will result in a squadron created at the airbase.** + -- + -- The helicopter indicates the start of the CAP zone. + -- The route points define the form of the CAP zone polygon. + -- + -- ![Mission Editor Action](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_GCICAP-ME_6.JPG) + -- + -- **The place of the helicopter is important, as the airbase closest to the helicopter will be the airbase from where the CAP planes will take off for CAP.** + -- + -- ## 2) There are a lot of defaults set, which can be further modified using the methods in @{#AI_A2G_DISPATCHER}: + -- + -- ### 2.1) Planes are taking off in the air from the airbases. + -- + -- This prevents airbases to get cluttered with airplanes taking off, it also reduces the risk of human players colliding with taxiiing airplanes, + -- resulting in the airbase to halt operations. + -- + -- You can change the way how planes take off by using the inherited methods from AI\_A2G\_DISPATCHER: + -- + -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoff}() is the generic configuration method to control takeoff from the air, hot, cold or from the runway. See the method for further details. + -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffInAir}() will spawn new aircraft from the squadron directly in the air. + -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffFromParkingCold}() will spawn new aircraft in without running engines at a parking spot at the airfield. + -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffFromParkingHot}() will spawn new aircraft in with running engines at a parking spot at the airfield. + -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffFromRunway}() will spawn new aircraft at the runway at the airfield. + -- + -- Use these methods to fine-tune for specific airfields that are known to create bottlenecks, or have reduced airbase efficiency. + -- The more and the longer aircraft need to taxi at an airfield, the more risk there is that: + -- + -- * aircraft will stop waiting for each other or for a landing aircraft before takeoff. + -- * aircraft may get into a "dead-lock" situation, where two aircraft are blocking each other. + -- * aircraft may collide at the airbase. + -- * aircraft may be awaiting the landing of a plane currently in the air, but never lands ... + -- + -- Currently within the DCS engine, the airfield traffic coordination is erroneous and contains a lot of bugs. + -- If you experience while testing problems with aircraft take-off or landing, please use one of the above methods as a solution to workaround these issues! + -- + -- ### 2.2) Planes return near the airbase or will land if damaged. + -- + -- When damaged airplanes return to the airbase, they will be routed and will dissapear in the air when they are near the airbase. + -- There are exceptions to this rule, airplanes that aren't "listening" anymore due to damage or out of fuel, will return to the airbase and land. + -- + -- You can change the way how planes land by using the inherited methods from AI\_A2G\_DISPATCHER: + -- + -- * @{#AI_A2G_DISPATCHER.SetSquadronLanding}() is the generic configuration method to control landing, namely despawn the aircraft near the airfield in the air, right after landing, or at engine shutdown. + -- * @{#AI_A2G_DISPATCHER.SetSquadronLandingNearAirbase}() will despawn the returning aircraft in the air when near the airfield. + -- * @{#AI_A2G_DISPATCHER.SetSquadronLandingAtRunway}() will despawn the returning aircraft directly after landing at the runway. + -- * @{#AI_A2G_DISPATCHER.SetSquadronLandingAtEngineShutdown}() will despawn the returning aircraft when the aircraft has returned to its parking spot and has turned off its engines. + -- + -- You can use these methods to minimize the airbase coodination overhead and to increase the airbase efficiency. + -- When there are lots of aircraft returning for landing, at the same airbase, the takeoff process will be halted, which can cause a complete failure of the + -- A2G defense system, as no new CAP or GCI planes can takeoff. + -- Note that the method @{#AI_A2G_DISPATCHER.SetSquadronLandingNearAirbase}() will only work for returning aircraft, not for damaged or out of fuel aircraft. + -- Damaged or out-of-fuel aircraft are returning to the nearest friendly airbase and will land, and are out of control from ground control. + -- + -- ### 2.3) CAP operations setup for specific airbases, will be executed with the following parameters: + -- + -- * The altitude will range between 6000 and 10000 meters. + -- * The CAP speed will vary between 500 and 800 km/h. + -- * The engage speed between 800 and 1200 km/h. + -- + -- You can change or add a CAP zone by using the inherited methods from AI\_A2G\_DISPATCHER: + -- + -- The method @{#AI_A2G_DISPATCHER.SetSquadronPatrol}() defines a CAP execution for a squadron. + -- + -- Setting-up a CAP zone also requires specific parameters: + -- + -- * The minimum and maximum altitude + -- * The minimum speed and maximum patrol speed + -- * The minimum and maximum engage speed + -- * The type of altitude measurement + -- + -- These define how the squadron will perform the CAP while partrolling. Different terrain types requires different types of CAP. + -- + -- The @{#AI_A2G_DISPATCHER.SetSquadronPatrolInterval}() method specifies **how much** and **when** CAP flights will takeoff. + -- + -- It is recommended not to overload the air defense with CAP flights, as these will decrease the performance of the overall system. + -- + -- For example, the following setup will create a CAP for squadron "Sochi": + -- + -- A2GDispatcher:SetSquadronPatrol( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) + -- A2GDispatcher:SetSquadronPatrolInterval( "Sochi", 2, 30, 120, 1 ) + -- + -- ### 2.4) Each airbase will perform GCI when required, with the following parameters: + -- + -- * The engage speed is between 800 and 1200 km/h. + -- + -- You can change or add a GCI parameters by using the inherited methods from AI\_A2G\_DISPATCHER: + -- + -- The method @{#AI_A2G_DISPATCHER.SetSquadronGci}() defines a GCI execution for a squadron. + -- + -- Setting-up a GCI readiness also requires specific parameters: + -- + -- * The minimum speed and maximum patrol speed + -- + -- Essentially this controls how many flights of GCI aircraft can be active at any time. + -- Note allowing large numbers of active GCI flights can adversely impact mission performance on low or medium specification hosts/servers. + -- GCI needs to be setup at strategic airbases. Too far will mean that the aircraft need to fly a long way to reach the intruders, + -- too short will mean that the intruders may have alraedy passed the ideal interception point! + -- + -- For example, the following setup will create a GCI for squadron "Sochi": + -- + -- A2GDispatcher:SetSquadronGci( "Mozdok", 900, 1200 ) + -- + -- ### 2.5) Grouping or detected targets. + -- + -- Detected targets are constantly re-grouped, that is, when certain detected aircraft are moving further than the group radius, then these aircraft will become a separate + -- group being detected. + -- + -- Targets will be grouped within a radius of 30km by default. + -- + -- The radius indicates that detected targets need to be grouped within a radius of 30km. + -- The grouping radius should not be too small, but also depends on the types of planes and the era of the simulation. + -- Fast planes like in the 80s, need a larger radius than WWII planes. + -- Typically I suggest to use 30000 for new generation planes and 10000 for older era aircraft. + -- + -- ## 3) Additional notes: + -- + -- In order to create a two way A2G defense system, **two AI\_A2G\_GCICAP defense systems must need to be created**, for each coalition one. + -- Each defense system needs its own EWR network setup, airplane templates and CAP configurations. + -- + -- This is a good implementation, because maybe in the future, more coalitions may become available in DCS world. + -- + -- ## 4) Coding examples how to use the AI\_A2G\_GCICAP class: + -- + -- ### 4.1) An easy setup: + -- + -- -- Setup the AI_A2G_GCICAP dispatcher for one coalition, and initialize it. + -- GCI_Red = AI_A2G_GCICAP:New( "EWR CCCP", "SQUADRON CCCP", "CAP CCCP", 2 ) + -- -- + -- The following parameters were given to the :New method of AI_A2G_GCICAP, and mean the following: + -- + -- * `"EWR CCCP"`: Groups of the blue coalition are placed that define the EWR network. These groups start with the name `EWR CCCP`. + -- * `"SQUADRON CCCP"`: Late activated Groups objects of the red coalition are placed above the relevant airbases that will contain these templates in the squadron. + -- These late activated Groups start with the name `SQUADRON CCCP`. Each Group object contains only one Unit, and defines the weapon payload, skin and skill level. + -- * `"CAP CCCP"`: CAP Zones are defined using floating, late activated Helicopter Group objects, where the route points define the route of the polygon of the CAP Zone. + -- These Helicopter Group objects start with the name `CAP CCCP`, and will be the locations wherein CAP will be performed. + -- * `2` Defines how many CAP airplanes are patrolling in each CAP zone defined simulateneously. + -- + -- + -- ### 4.2) A more advanced setup: + -- + -- -- Setup the AI_A2G_GCICAP dispatcher for the blue coalition. + -- + -- A2G_GCICAP_Blue = AI_A2G_GCICAP:New( { "BLUE EWR" }, { "104th", "105th", "106th" }, { "104th CAP" }, 4 ) + -- + -- The following parameters for the :New method have the following meaning: + -- + -- * `{ "BLUE EWR" }`: An array of the group name prefixes of the groups of the blue coalition are placed that define the EWR network. These groups start with the name `BLUE EWR`. + -- * `{ "104th", "105th", "106th" } `: An array of the group name prefixes of the Late activated Groups objects of the blue coalition are + -- placed above the relevant airbases that will contain these templates in the squadron. + -- These late activated Groups start with the name `104th` or `105th` or `106th`. + -- * `{ "104th CAP" }`: An array of the names of the CAP zones are defined using floating, late activated helicopter group objects, + -- where the route points define the route of the polygon of the CAP Zone. + -- These Helicopter Group objects start with the name `104th CAP`, and will be the locations wherein CAP will be performed. + -- * `4` Defines how many CAP airplanes are patrolling in each CAP zone defined simulateneously. + -- + -- @field #AI_A2G_GCICAP + AI_A2G_GCICAP = { + ClassName = "AI_A2G_GCICAP", + Detection = nil, + } + + + --- AI_A2G_GCICAP constructor. + -- @param #AI_A2G_GCICAP self + -- @param #string EWRPrefixes A list of prefixes that of groups that setup the Early Warning Radar network. + -- @param #string TemplatePrefixes A list of template prefixes. + -- @param #string PatrolPrefixes A list of CAP zone prefixes (polygon zones). + -- @param #number PatrolLimit A number of how many CAP maximum will be spawned. + -- @param #number GroupingRadius The radius in meters wherein detected planes are being grouped as one target area. + -- For airplanes, 6000 (6km) is recommended, and is also the default value of this parameter. + -- @param #number EngageRadius The radius in meters wherein detected airplanes will be engaged by airborne defenders without a task. + -- @param #number GciRadius The radius in meters wherein detected airplanes will GCI. + -- @param #number ResourceCount The amount of resources that will be allocated to each squadron. + -- @return #AI_A2G_GCICAP + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object. Each squadron has unlimited resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- A2GDispatcher = AI_A2G_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object. Each squadron has unlimited resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. + -- A2GDispatcher = AI_A2G_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object. Each squadron has unlimited resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. + -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, + -- -- will be considered a defense task if the target is within 60km from the defender. + -- A2GDispatcher = AI_A2G_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000, 60000 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object. Each squadron has unlimited resources. + -- -- The EWR network group prefix is DF CCCP. All groups starting with DF CCCP will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. + -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, + -- -- will be considered a defense task if the target is within 60km from the defender. + -- -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement. + -- A2GDispatcher = AI_A2G_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000, 60000, 150000 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object. Each squadron has 30 resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. + -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, + -- -- will be considered a defense task if the target is within 60km from the defender. + -- -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement. + -- -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created. + -- + -- A2GDispatcher = AI_A2G_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000, 60000, 150000, 30 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object. Each squadron has 30 resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The CAP Zone prefix is nil. No CAP is created. + -- -- The CAP Limit is nil. + -- -- The Grouping Radius is nil. The default range of 6km radius will be grouped as a group of targets. + -- -- The Engage Radius is set nil. The default Engage Radius will be used to consider a defenser being assigned to a task. + -- -- The GCI Radius is nil. Any target detected within the default GCI Radius will be considered for GCI engagement. + -- -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created. + -- + -- A2GDispatcher = AI_A2G_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, nil, nil, nil, nil, nil, 30 ) + -- + function AI_A2G_GCICAP:New( EWRPrefixes, TemplatePrefixes, PatrolPrefixes, PatrolLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) + + local EWRSetGroup = SET_GROUP:New() + EWRSetGroup:FilterPrefixes( EWRPrefixes ) + EWRSetGroup:FilterStart() + + local Detection = DETECTION_AREAS:New( EWRSetGroup, GroupingRadius or 30000 ) + + local self = BASE:Inherit( self, AI_A2G_DISPATCHER:New( Detection ) ) -- #AI_A2G_GCICAP + + self:SetGciRadius( GciRadius ) + + -- Determine the coalition of the EWRNetwork, this will be the coalition of the GCICAP. + local EWRFirst = EWRSetGroup:GetFirst() -- Wrapper.Group#GROUP + local EWRCoalition = EWRFirst:GetCoalition() + + -- Determine the airbases belonging to the coalition. + local AirbaseNames = {} -- #list<#string> + for AirbaseID, AirbaseData in pairs( _DATABASE.AIRBASES ) do + local Airbase = AirbaseData -- Wrapper.Airbase#AIRBASE + local AirbaseName = Airbase:GetName() + if Airbase:GetCoalition() == EWRCoalition then + table.insert( AirbaseNames, AirbaseName ) + end + end + + self.Templates = SET_GROUP + :New() + :FilterPrefixes( TemplatePrefixes ) + :FilterOnce() + + -- Setup squadrons + + self:I( { Airbases = AirbaseNames } ) + + self:I( "Defining Templates for Airbases ..." ) + for AirbaseID, AirbaseName in pairs( AirbaseNames ) do + local Airbase = _DATABASE:FindAirbase( AirbaseName ) -- Wrapper.Airbase#AIRBASE + local AirbaseName = Airbase:GetName() + local AirbaseCoord = Airbase:GetCoordinate() + local AirbaseZone = ZONE_RADIUS:New( "Airbase", AirbaseCoord:GetVec2(), 3000 ) + local Templates = nil + self:I( { Airbase = AirbaseName } ) + for TemplateID, Template in pairs( self.Templates:GetSet() ) do + local Template = Template -- Wrapper.Group#GROUP + local TemplateCoord = Template:GetCoordinate() + if AirbaseZone:IsVec2InZone( TemplateCoord:GetVec2() ) then + Templates = Templates or {} + table.insert( Templates, Template:GetName() ) + self:I( { Template = Template:GetName() } ) + end + end + if Templates then + self:SetSquadron( AirbaseName, AirbaseName, Templates, ResourceCount ) + end + end + + -- Setup CAP. + -- Find for each CAP the nearest airbase to the (start or center) of the zone. + -- CAP will be launched from there. + + self.CAPTemplates = SET_GROUP:New() + self.CAPTemplates:FilterPrefixes( PatrolPrefixes ) + self.CAPTemplates:FilterOnce() + + self:I( "Setting up CAP ..." ) + for CAPID, CAPTemplate in pairs( self.CAPTemplates:GetSet() ) do + local CAPZone = ZONE_POLYGON:New( CAPTemplate:GetName(), CAPTemplate ) + -- Now find the closest airbase from the ZONE (start or center) + local AirbaseDistance = 99999999 + local AirbaseClosest = nil -- Wrapper.Airbase#AIRBASE + self:I( { CAPZoneGroup = CAPID } ) + for AirbaseID, AirbaseName in pairs( AirbaseNames ) do + local Airbase = _DATABASE:FindAirbase( AirbaseName ) -- Wrapper.Airbase#AIRBASE + local AirbaseName = Airbase:GetName() + local AirbaseCoord = Airbase:GetCoordinate() + local Squadron = self.DefenderSquadrons[AirbaseName] + if Squadron then + local Distance = AirbaseCoord:Get2DDistance( CAPZone:GetCoordinate() ) + self:I( { AirbaseDistance = Distance } ) + if Distance < AirbaseDistance then + AirbaseDistance = Distance + AirbaseClosest = Airbase + end + end + end + if AirbaseClosest then + self:I( { CAPAirbase = AirbaseClosest:GetName() } ) + self:SetSquadronPatrol( AirbaseClosest:GetName(), CAPZone, 6000, 10000, 500, 800, 800, 1200, "RADIO" ) + self:SetSquadronPatrolInterval( AirbaseClosest:GetName(), PatrolLimit, 300, 600, 1 ) + end + end + + -- Setup GCI. + -- GCI is setup for all Squadrons. + self:I( "Setting up GCI ..." ) + for AirbaseID, AirbaseName in pairs( AirbaseNames ) do + local Airbase = _DATABASE:FindAirbase( AirbaseName ) -- Wrapper.Airbase#AIRBASE + local AirbaseName = Airbase:GetName() + local Squadron = self.DefenderSquadrons[AirbaseName] + self:F( { Airbase = AirbaseName } ) + if Squadron then + self:I( { GCIAirbase = AirbaseName } ) + self:SetSquadronGci( AirbaseName, 800, 1200 ) + end + end + + self:__Start( 5 ) + + self:HandleEvent( EVENTS.Crash, self.OnEventCrashOrDead ) + self:HandleEvent( EVENTS.Dead, self.OnEventCrashOrDead ) + --self:HandleEvent( EVENTS.RemoveUnit, self.OnEventCrashOrDead ) + + self:HandleEvent( EVENTS.Land ) + self:HandleEvent( EVENTS.EngineShutdown ) + + return self + end + + --- AI_A2G_GCICAP constructor with border. + -- @param #AI_A2G_GCICAP self + -- @param #string EWRPrefixes A list of prefixes that of groups that setup the Early Warning Radar network. + -- @param #string TemplatePrefixes A list of template prefixes. + -- @param #string BorderPrefix A Border Zone Prefix. + -- @param #string PatrolPrefixes A list of CAP zone prefixes (polygon zones). + -- @param #number PatrolLimit A number of how many CAP maximum will be spawned. + -- @param #number GroupingRadius The radius in meters wherein detected planes are being grouped as one target area. + -- For airplanes, 6000 (6km) is recommended, and is also the default value of this parameter. + -- @param #number EngageRadius The radius in meters wherein detected airplanes will be engaged by airborne defenders without a task. + -- @param #number GciRadius The radius in meters wherein detected airplanes will GCI. + -- @param #number ResourceCount The amount of resources that will be allocated to each squadron. + -- @return #AI_A2G_GCICAP + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has unlimited resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- + -- A2GDispatcher = AI_A2G_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has unlimited resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. + -- + -- A2GDispatcher = AI_A2G_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has unlimited resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. + -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, + -- -- will be considered a defense task if the target is within 60km from the defender. + -- + -- A2GDispatcher = AI_A2G_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000, 60000 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has unlimited resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. + -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, + -- -- will be considered a defense task if the target is within 60km from the defender. + -- -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement. + -- + -- A2GDispatcher = AI_A2G_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000, 60000, 150000 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has 30 resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. + -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, + -- -- will be considered a defense task if the target is within 60km from the defender. + -- -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement. + -- -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created. + -- + -- A2GDispatcher = AI_A2G_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000, 60000, 150000, 30 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has 30 resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border. + -- -- The CAP Zone prefix is nil. No CAP is created. + -- -- The CAP Limit is nil. + -- -- The Grouping Radius is nil. The default range of 6km radius will be grouped as a group of targets. + -- -- The Engage Radius is set nil. The default Engage Radius will be used to consider a defenser being assigned to a task. + -- -- The GCI Radius is nil. Any target detected within the default GCI Radius will be considered for GCI engagement. + -- -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created. + -- + -- A2GDispatcher = AI_A2G_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", nil, nil, nil, nil, nil, 30 ) + -- + function AI_A2G_GCICAP:NewWithBorder( EWRPrefixes, TemplatePrefixes, BorderPrefix, PatrolPrefixes, PatrolLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) + + local self = AI_A2G_GCICAP:New( EWRPrefixes, TemplatePrefixes, PatrolPrefixes, PatrolLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) + + if BorderPrefix then + self:SetBorderZone( ZONE_POLYGON:New( BorderPrefix, GROUP:FindByName( BorderPrefix ) ) ) + end + + return self + + end + +end + diff --git a/Moose Development/Moose/AI/AI_A2G_Engage.lua b/Moose Development/Moose/AI/AI_A2G_Engage.lua new file mode 100644 index 000000000..b71da1dce --- /dev/null +++ b/Moose Development/Moose/AI/AI_A2G_Engage.lua @@ -0,0 +1,378 @@ +--- **AI** -- Models the process of air to ground engagement for airplanes and helicopters. +-- +-- This is a class used in the @{AI_A2G_Dispatcher}. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_A2G_Engage +-- @image AI_Air_To_Ground_Engage.JPG + + + +--- @type AI_A2G_ENGAGE +-- @extends AI.AI_A2G#AI_A2G + + +--- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders. +-- +-- ![Process](..\Presentations\AI_GCI\Dia3.JPG) +-- +-- The AI_A2G_ENGAGE is assigned a @{Wrapper.Group} and this must be done before the AI_A2G_ENGAGE process can be started using the **Start** event. +-- +-- ![Process](..\Presentations\AI_GCI\Dia4.JPG) +-- +-- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. +-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. +-- +-- ![Process](..\Presentations\AI_GCI\Dia5.JPG) +-- +-- This cycle will continue. +-- +-- ![Process](..\Presentations\AI_GCI\Dia6.JPG) +-- +-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. +-- +-- ![Process](..\Presentations\AI_GCI\Dia9.JPG) +-- +-- When enemies are detected, the AI will automatically engage the enemy. +-- +-- ![Process](..\Presentations\AI_GCI\Dia10.JPG) +-- +-- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. +-- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- +-- ![Process](..\Presentations\AI_GCI\Dia13.JPG) +-- +-- ## 1. AI_A2G_ENGAGE constructor +-- +-- * @{#AI_A2G_ENGAGE.New}(): Creates a new AI_A2G_ENGAGE object. +-- +-- ## 3. Set the Range of Engagement +-- +-- ![Range](..\Presentations\AI_GCI\Dia11.JPG) +-- +-- An optional range can be set in meters, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- The range can be beyond or smaller than the range of the Patrol Zone. +-- The range is applied at the position of the AI. +-- Use the method @{AI.AI_GCI#AI_A2G_ENGAGE.SetEngageRange}() to define that range. +-- +-- ## 4. Set the Zone of Engagement +-- +-- ![Zone](..\Presentations\AI_GCI\Dia12.JPG) +-- +-- An optional @{Zone} can be set, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- Use the method @{AI.AI_Cap#AI_A2G_ENGAGE.SetEngageZone}() to define that Zone. +-- +-- === +-- +-- @field #AI_A2G_ENGAGE +AI_A2G_ENGAGE = { + ClassName = "AI_A2G_ENGAGE", +} + + + +--- Creates a new AI_A2G_ENGAGE object +-- @param #AI_A2G_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup +-- @param DCS#Speed EngageMinSpeed (optional, default = 50% of max speed) The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Altitude EngageFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the engagement. +-- @param DCS#Altitude EngageCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the engagement. +-- @return #AI_A2G_ENGAGE +function AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_A2G:New( AIGroup ) ) -- #AI_A2G_ENGAGE + + self.Accomplished = false + self.Engaging = false + + local SpeedMax = AIGroup:GetSpeedMax() + + self.EngageMinSpeed = EngageMinSpeed or SpeedMax * 0.5 + self.EngageMaxSpeed = EngageMaxSpeed or SpeedMax * 0.75 + self.EngageFloorAltitude = EngageFloorAltitude or 1000 + self.EngageCeilingAltitude = EngageCeilingAltitude or 1500 + + self:AddTransition( { "Started", "Engaging", "Returning", "Airborne", "Patrolling" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. + + --- OnBefore Transition Handler for Event Engage. + -- @function [parent=#AI_A2G_ENGAGE] OnBeforeEngage + -- @param #AI_A2G_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Engage. + -- @function [parent=#AI_A2G_ENGAGE] OnAfterEngage + -- @param #AI_A2G_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Engage. + -- @function [parent=#AI_A2G_ENGAGE] Engage + -- @param #AI_A2G_ENGAGE self + + --- Asynchronous Event Trigger for Event Engage. + -- @function [parent=#AI_A2G_ENGAGE] __Engage + -- @param #AI_A2G_ENGAGE self + -- @param #number Delay The delay in seconds. + +--- OnLeave Transition Handler for State Engaging. +-- @function [parent=#AI_A2G_ENGAGE] OnLeaveEngaging +-- @param #AI_A2G_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Engaging. +-- @function [parent=#AI_A2G_ENGAGE] OnEnterEngaging +-- @param #AI_A2G_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + + self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. + + --- OnBefore Transition Handler for Event Fired. + -- @function [parent=#AI_A2G_ENGAGE] OnBeforeFired + -- @param #AI_A2G_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Fired. + -- @function [parent=#AI_A2G_ENGAGE] OnAfterFired + -- @param #AI_A2G_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Fired. + -- @function [parent=#AI_A2G_ENGAGE] Fired + -- @param #AI_A2G_ENGAGE self + + --- Asynchronous Event Trigger for Event Fired. + -- @function [parent=#AI_A2G_ENGAGE] __Fired + -- @param #AI_A2G_ENGAGE self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. + + --- OnBefore Transition Handler for Event Destroy. + -- @function [parent=#AI_A2G_ENGAGE] OnBeforeDestroy + -- @param #AI_A2G_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Destroy. + -- @function [parent=#AI_A2G_ENGAGE] OnAfterDestroy + -- @param #AI_A2G_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Destroy. + -- @function [parent=#AI_A2G_ENGAGE] Destroy + -- @param #AI_A2G_ENGAGE self + + --- Asynchronous Event Trigger for Event Destroy. + -- @function [parent=#AI_A2G_ENGAGE] __Destroy + -- @param #AI_A2G_ENGAGE self + -- @param #number Delay The delay in seconds. + + + self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. + + --- OnBefore Transition Handler for Event Abort. + -- @function [parent=#AI_A2G_ENGAGE] OnBeforeAbort + -- @param #AI_A2G_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Abort. + -- @function [parent=#AI_A2G_ENGAGE] OnAfterAbort + -- @param #AI_A2G_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Abort. + -- @function [parent=#AI_A2G_ENGAGE] Abort + -- @param #AI_A2G_ENGAGE self + + --- Asynchronous Event Trigger for Event Abort. + -- @function [parent=#AI_A2G_ENGAGE] __Abort + -- @param #AI_A2G_ENGAGE self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. + + --- OnBefore Transition Handler for Event Accomplish. + -- @function [parent=#AI_A2G_ENGAGE] OnBeforeAccomplish + -- @param #AI_A2G_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Accomplish. + -- @function [parent=#AI_A2G_ENGAGE] OnAfterAccomplish + -- @param #AI_A2G_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Accomplish. + -- @function [parent=#AI_A2G_ENGAGE] Accomplish + -- @param #AI_A2G_ENGAGE self + + --- Asynchronous Event Trigger for Event Accomplish. + -- @function [parent=#AI_A2G_ENGAGE] __Accomplish + -- @param #AI_A2G_ENGAGE self + -- @param #number Delay The delay in seconds. + + return self +end + +--- onafter event handler for Start event. +-- @param #AI_A2G_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The AI group managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_ENGAGE:onafterStart( AIGroup, From, Event, To ) + + self:GetParent( self, AI_A2G_ENGAGE ).onafterStart( self, AIGroup, From, Event, To ) + + AIGroup:HandleEvent( EVENTS.Takeoff, nil, self ) + +end + + + +--- onafter event handler for Engage event. +-- @param #AI_A2G_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The AI Group managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_ENGAGE:onafterEngage( AIGroup, From, Event, To ) + + self:HandleEvent( EVENTS.Dead ) + +end + +-- todo: need to fix this global function + +--- @param Wrapper.Group#GROUP AIControllable +function AI_A2G_ENGAGE.EngageRoute( AIGroup, Fsm ) + + AIGroup:I( { "AI_A2G_ENGAGE.EngageRoute:", AIGroup:GetName() } ) + + if AIGroup:IsAlive() then + Fsm:__Engage( Fsm.TaskDelay ) + + --local Task = AIGroup:TaskOrbitCircle( 4000, 400 ) + --AIGroup:SetTask( Task ) + end +end + +--- onbefore event handler for Engage event. +-- @param #AI_A2G_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_ENGAGE:onbeforeEngage( AIGroup, From, Event, To ) + + if self.Accomplished == true then + return false + end +end + +--- onafter event handler for Abort event. +-- @param #AI_A2G_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The AI Group managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_ENGAGE:onafterAbort( AIGroup, From, Event, To ) + AIGroup:ClearTasks() + self:Return() + self:__RTB( self.TaskDelay ) +end + + +--- @param #AI_A2G_ENGAGE self +-- @param Wrapper.Group#GROUP DefenderGroup The GroupGroup managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_ENGAGE:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit ) + + self:F( { DefenderGroup, From, Event, To, AttackSetUnit} ) + +end + +--- @param #AI_A2G_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_ENGAGE:onafterAccomplish( AIGroup, From, Event, To ) + self.Accomplished = true + self:SetDetectionOff() +end + +--- @param #AI_A2G_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @param Core.Event#EVENTDATA EventData +function AI_A2G_ENGAGE:onafterDestroy( AIGroup, From, Event, To, EventData ) + + if EventData.IniUnit then + self.AttackUnits[EventData.IniUnit] = nil + end +end + +--- @param #AI_A2G_ENGAGE self +-- @param Core.Event#EVENTDATA EventData +function AI_A2G_ENGAGE:OnEventDead( EventData ) + self:F( { "EventDead", EventData } ) + + if EventData.IniDCSUnit then + if self.AttackUnits and self.AttackUnits[EventData.IniUnit] then + self:__Destroy( self.TaskDelay, EventData ) + end + end +end diff --git a/Moose Development/Moose/AI/AI_A2G_Patrol.lua b/Moose Development/Moose/AI/AI_A2G_Patrol.lua new file mode 100644 index 000000000..4e8a6e953 --- /dev/null +++ b/Moose Development/Moose/AI/AI_A2G_Patrol.lua @@ -0,0 +1,323 @@ +--- **AI** -- Models the process of A2G patrolling and engaging ground targets for airplanes and helicopters. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_A2G_Patrol +-- @image AI_Air_To_Ground_Patrol.JPG + +--- @type AI_A2G_PATROL +-- @extends AI.AI_A2G_Engage#AI_A2G_ENGAGE + + +--- The AI_A2G_PATROL class implements the core functions to patrol a @{Zone} by an AI @{Wrapper.Group} or @{Wrapper.Group} +-- and automatically engage any airborne enemies that are within a certain range or within a certain zone. +-- +-- ![Process](..\Presentations\AI_CAP\Dia3.JPG) +-- +-- The AI_A2G_PATROL is assigned a @{Wrapper.Group} and this must be done before the AI_A2G_PATROL process can be started using the **Start** event. +-- +-- ![Process](..\Presentations\AI_CAP\Dia4.JPG) +-- +-- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. +-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. +-- +-- ![Process](..\Presentations\AI_CAP\Dia5.JPG) +-- +-- This cycle will continue. +-- +-- ![Process](..\Presentations\AI_CAP\Dia6.JPG) +-- +-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. +-- +-- ![Process](..\Presentations\AI_CAP\Dia9.JPG) +-- +-- When enemies are detected, the AI will automatically engage the enemy. +-- +-- ![Process](..\Presentations\AI_CAP\Dia10.JPG) +-- +-- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. +-- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- +-- ![Process](..\Presentations\AI_CAP\Dia13.JPG) +-- +-- ## 1. AI_A2G_PATROL constructor +-- +-- * @{#AI_A2G_PATROL.New}(): Creates a new AI_A2G_PATROL object. +-- +-- ## 2. AI_A2G_PATROL is a FSM +-- +-- ![Process](..\Presentations\AI_CAP\Dia2.JPG) +-- +-- ### 2.1 AI_A2G_PATROL States +-- +-- * **None** ( Group ): The process is not started yet. +-- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. +-- * **Engaging** ( Group ): The AI is engaging the bogeys. +-- * **Returning** ( Group ): The AI is returning to Base.. +-- +-- ### 2.2 AI_A2G_PATROL Events +-- +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone. +-- * **@{#AI_A2G_PATROL.Engage}**: Let the AI engage the bogeys. +-- * **@{#AI_A2G_PATROL.Abort}**: Aborts the engagement and return patrolling in the patrol zone. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. +-- * **@{#AI_A2G_PATROL.Destroy}**: The AI has destroyed a bogey @{Wrapper.Unit}. +-- * **@{#AI_A2G_PATROL.Destroyed}**: The AI has destroyed all bogeys @{Wrapper.Unit}s assigned in the CAS task. +-- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. +-- +-- ## 3. Set the Range of Engagement +-- +-- ![Range](..\Presentations\AI_CAP\Dia11.JPG) +-- +-- An optional range can be set in meters, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- The range can be beyond or smaller than the range of the Patrol Zone. +-- The range is applied at the position of the AI. +-- Use the method @{AI.AI_CAP#AI_A2G_PATROL.SetEngageRange}() to define that range. +-- +-- ## 4. Set the Zone of Engagement +-- +-- ![Zone](..\Presentations\AI_CAP\Dia12.JPG) +-- +-- An optional @{Zone} can be set, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- Use the method @{AI.AI_Cap#AI_A2G_PATROL.SetEngageZone}() to define that Zone. +-- +-- === +-- +-- @field #AI_A2G_PATROL +AI_A2G_PATROL = { + ClassName = "AI_A2G_PATROL", +} + +--- Creates a new AI_A2G_PATROL object +-- @param #AI_A2G_PATROL self +-- @param Wrapper.Group#GROUP AIGroup +-- @param DCS#Speed EngageMinSpeed (optional, default = 50% of max speed) The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Altitude EngageFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the engagement. +-- @param DCS#Altitude EngageCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the engagement. +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param DCS#Altitude PatrolFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO. +-- @return #AI_A2G_PATROL +function AI_A2G_PATROL:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) ) -- #AI_A2G_PATROL + + local SpeedMax = AIGroup:GetSpeedMax() + + self.PatrolZone = PatrolZone + + self.PatrolFloorAltitude = PatrolFloorAltitude or 1000 + self.PatrolCeilingAltitude = PatrolCeilingAltitude or 1500 + self.PatrolMinSpeed = PatrolMinSpeed or SpeedMax * 0.5 + self.PatrolMaxSpeed = PatrolMaxSpeed or SpeedMax * 0.75 + + -- defafult PatrolAltType to "RADIO" if not specified + self.PatrolAltType = PatrolAltType or "RADIO" + + self:AddTransition( { "Started", "Airborne", "Refuelling" }, "Patrol", "Patrolling" ) + + --- OnBefore Transition Handler for Event Patrol. + -- @function [parent=#AI_A2G_PATROL] OnBeforePatrol + -- @param #AI_A2G_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Patrol. + -- @function [parent=#AI_A2G_PATROL] OnAfterPatrol + -- @param #AI_A2G_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Patrol. + -- @function [parent=#AI_A2G_PATROL] Patrol + -- @param #AI_A2G_PATROL self + + --- Asynchronous Event Trigger for Event Patrol. + -- @function [parent=#AI_A2G_PATROL] __Patrol + -- @param #AI_A2G_PATROL self + -- @param #number Delay The delay in seconds. + + --- OnLeave Transition Handler for State Patrolling. + -- @function [parent=#AI_A2G_PATROL] OnLeavePatrolling + -- @param #AI_A2G_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnEnter Transition Handler for State Patrolling. + -- @function [parent=#AI_A2G_PATROL] OnEnterPatrolling + -- @param #AI_A2G_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + self:AddTransition( "Patrolling", "Route", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_PATROL. + + --- OnBefore Transition Handler for Event Route. + -- @function [parent=#AI_A2G_PATROL] OnBeforeRoute + -- @param #AI_A2G_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Route. + -- @function [parent=#AI_A2G_PATROL] OnAfterRoute + -- @param #AI_A2G_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Route. + -- @function [parent=#AI_A2G_PATROL] Route + -- @param #AI_A2G_PATROL self + + --- Asynchronous Event Trigger for Event Route. + -- @function [parent=#AI_A2G_PATROL] __Route + -- @param #AI_A2G_PATROL self + -- @param #number Delay The delay in seconds. + + + self:AddTransition( "*", "Reset", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_PATROL. + + return self +end + + +--- Set the Engage Range when the AI will engage with airborne enemies. +-- @param #AI_A2G_PATROL self +-- @param #number EngageRange The Engage Range. +-- @return #AI_A2G_PATROL self +function AI_A2G_PATROL:SetEngageRange( EngageRange ) + self:F2() + + if EngageRange then + self.EngageRange = EngageRange + else + self.EngageRange = nil + end +end + +--- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. +-- @param #AI_A2G_PATROL self +-- @return #AI_A2G_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_PATROL:onafterPatrol( AIPatrol, From, Event, To ) + self:F2() + + self:ClearTargetDistance() + + self:__Route( self.TaskDelay ) + + AIPatrol:OnReSpawn( + function( PatrolGroup ) + self:__Reset( self.TaskDelay ) + self:__Route( self.TaskDelay ) + end + ) +end + +--- @param Wrapper.Group#GROUP AIPatrol +-- This statis method is called from the route path within the last task at the last waaypoint of the AIPatrol. +-- Note that this method is required, as triggers the next route when patrolling for the AIPatrol. +function AI_A2G_PATROL.PatrolRoute( AIPatrol, Fsm ) + + AIPatrol:F( { "AI_A2G_PATROL.PatrolRoute:", AIPatrol:GetName() } ) + + if AIPatrol:IsAlive() then + Fsm:Route() + end + +end + +--- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. +-- @param #AI_A2G_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The Group managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_PATROL:onafterRoute( AIPatrol, From, Event, To ) + + self:F2() + + -- When RTB, don't allow anymore the routing. + if From == "RTB" then + return + end + + + if AIPatrol:IsAlive() then + + local PatrolRoute = {} + + --- Calculate the target route point. + + local CurrentCoord = AIPatrol:GetCoordinate() + + local ToTargetCoord = self.PatrolZone:GetRandomPointVec2() + ToTargetCoord:SetAlt( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ) ) + self:SetTargetDistance( ToTargetCoord ) -- For RTB status check + + local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) + + --- Create a route point of type air. + local ToPatrolRoutePoint = ToTargetCoord:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + PatrolRoute[#PatrolRoute+1] = ToPatrolRoutePoint + PatrolRoute[#PatrolRoute+1] = ToPatrolRoutePoint + + local Tasks = {} + Tasks[#Tasks+1] = AIPatrol:TaskFunction( "AI_A2G_PATROL.PatrolRoute", self ) + PatrolRoute[#PatrolRoute].task = AIPatrol:TaskCombo( Tasks ) + + AIPatrol:OptionROEReturnFire() + AIPatrol:OptionROTEvadeFire() + + AIPatrol:Route( PatrolRoute, self.TaskDelay ) + end + +end + +--- @param Wrapper.Group#GROUP AIPatrol +function AI_A2G_PATROL.Resume( AIPatrol, Fsm ) + + AIPatrol:F( { "AI_A2G_PATROL.Resume:", AIPatrol:GetName() } ) + if AIPatrol:IsAlive() then + Fsm:__Reset( self.TaskDelay ) + Fsm:__Route( self.TaskDelay ) + end + +end diff --git a/Moose Development/Moose/AI/AI_A2G_SEAD.lua b/Moose Development/Moose/AI/AI_A2G_SEAD.lua new file mode 100644 index 000000000..124fe5049 --- /dev/null +++ b/Moose Development/Moose/AI/AI_A2G_SEAD.lua @@ -0,0 +1,221 @@ +--- **AI** -- Models the process of air to ground SEAD engagement for airplanes and helicopters. +-- +-- This is a class used in the @{AI_A2G_Dispatcher}. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_A2G_SEAD +-- @image AI_Air_To_Ground_Engage.JPG + + + +--- @type AI_A2G_SEAD +-- @extends AI.AI_A2G_Patrol#AI_A2G_PATROL + + +--- Implements the core functions to SEAD intruders. Use the Engage trigger to intercept intruders. +-- +-- ![Process](..\Presentations\AI_GCI\Dia3.JPG) +-- +-- The AI_A2G_SEAD is assigned a @{Wrapper.Group} and this must be done before the AI_A2G_SEAD process can be started using the **Start** event. +-- +-- ![Process](..\Presentations\AI_GCI\Dia4.JPG) +-- +-- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. +-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. +-- +-- ![Process](..\Presentations\AI_GCI\Dia5.JPG) +-- +-- This cycle will continue. +-- +-- ![Process](..\Presentations\AI_GCI\Dia6.JPG) +-- +-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. +-- +-- ![Process](..\Presentations\AI_GCI\Dia9.JPG) +-- +-- When enemies are detected, the AI will automatically engage the enemy. +-- +-- ![Process](..\Presentations\AI_GCI\Dia10.JPG) +-- +-- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. +-- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- +-- ![Process](..\Presentations\AI_GCI\Dia13.JPG) +-- +-- ## 1. AI_A2G_SEAD constructor +-- +-- * @{#AI_A2G_SEAD.New}(): Creates a new AI_A2G_SEAD object. +-- +-- ## 3. Set the Range of Engagement +-- +-- ![Range](..\Presentations\AI_GCI\Dia11.JPG) +-- +-- An optional range can be set in meters, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- The range can be beyond or smaller than the range of the Patrol Zone. +-- The range is applied at the position of the AI. +-- Use the method @{AI.AI_GCI#AI_A2G_SEAD.SetEngageRange}() to define that range. +-- +-- ## 4. Set the Zone of Engagement +-- +-- ![Zone](..\Presentations\AI_GCI\Dia12.JPG) +-- +-- An optional @{Zone} can be set, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- Use the method @{AI.AI_Cap#AI_A2G_SEAD.SetEngageZone}() to define that Zone. +-- +-- === +-- +-- @field #AI_A2G_SEAD +AI_A2G_SEAD = { + ClassName = "AI_A2G_SEAD", +} + + + +--- Creates a new AI_A2G_SEAD object +-- @param #AI_A2G_SEAD self +-- @param Wrapper.Group#GROUP AIGroup +-- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. +-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @return #AI_A2G_SEAD +function AI_A2G_SEAD:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_A2G_PATROL:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_A2G_SEAD + + local RTBSpeedMax = AIGroup:GetSpeedMax() or 9999 + + self:SetRTBSpeed( RTBSpeedMax * 0.50, RTBSpeedMax * 0.75 ) + + return self +end + + + +--- @param #AI_A2G_SEAD self +-- @param Wrapper.Group#GROUP DefenderGroup The GroupGroup managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_SEAD:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit ) + + self:F( { DefenderGroup, From, Event, To, AttackSetUnit} ) + + local DefenderGroupName = DefenderGroup:GetName() + + self.AttackSetUnit = AttackSetUnit or self.AttackSetUnit -- Core.Set#SET_UNIT + + local AttackCount = self.AttackSetUnit:Count() + + if AttackCount > 0 then + + if DefenderGroup:IsAlive() then + + -- Determine the distance to the target. + -- If it is less than 50km, then attack without a route. + -- Otherwise perform a route attack. + + local EngageAltitude = math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) + local EngageSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) + + local DefenderCoord = DefenderGroup:GetPointVec3() + DefenderCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. + + local TargetCoord = self.AttackSetUnit:GetFirst():GetPointVec3() + TargetCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. + + local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) + + local EngageRoute = {} + + + --- Calculate the target route point. + + local FromWP = DefenderCoord:WaypointAir( + self.PatrolAltType or "RADIO", + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + EngageSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = FromWP + + self:SetTargetDistance( TargetCoord ) -- For RTB status check + + local FromEngageAngle = TargetCoord:GetAngleDegrees( TargetCoord:GetDirectionVec3( DefenderCoord ) ) + local EngageDistance = ( DefenderGroup:IsHelicopter() and 5000 ) or ( DefenderGroup:IsAirPlane() and 25000 ) + + --- Create a route point of type air, 50km from the center of the attack point. + + local ToWP = TargetCoord:Translate( EngageDistance, FromEngageAngle, true ):WaypointAir( + self.PatrolAltType or "RADIO", + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + EngageSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = ToWP + + local AttackTasks = {} + + self.AttackSetUnit.AttackIndex = self.AttackSetUnit.AttackIndex and self.AttackSetUnit.AttackIndex + 1 or 1 + if self.AttackSetUnit.AttackIndex > self.AttackSetUnit:Count() then + self.AttackSetUnit.AttackIndex = 1 + end + + local AttackSetUnitPerThreatLevel = self.AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) + + for AttackUnitID, AttackUnit in ipairs( AttackSetUnitPerThreatLevel ) do + if AttackUnitID >= self.AttackSetUnit.AttackIndex then + if AttackUnit then + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + local HasRadar = AttackUnit:HasSEAD() + if HasRadar then + self:F( { "SEAD Unit:", AttackUnit:GetName() } ) + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, false, false, nil, nil, EngageAltitude ) + end + end + end + end + end + + if #AttackTasks == 0 then + self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:Return() + self:__RTB( self.TaskDelay ) + else + DefenderGroup:OptionROEOpenFire() + DefenderGroup:OptionROTVertical() + DefenderGroup:OptionKeepWeaponsOnThreat() + --DefenderGroup:OptionRTBAmmo( Weapon.flag.AnyASM ) + + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) + EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) + end + + DefenderGroup:Route( EngageRoute, self.TaskDelay ) + + end + else + self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:Return() + self:__RTB( self.TaskDelay ) + end +end + diff --git a/Moose Development/Moose/AI/AI_Air.lua b/Moose Development/Moose/AI/AI_Air.lua new file mode 100644 index 000000000..91a6eaffa --- /dev/null +++ b/Moose Development/Moose/AI/AI_Air.lua @@ -0,0 +1,772 @@ +--- **AI** -- Models the process of AI air operations. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Air +-- @image MOOSE.JPG + +--- @type AI_AIR +-- @extends Core.Fsm#FSM_CONTROLLABLE + +--- The AI_AIR class implements the core functions to operate an AI @{Wrapper.Group}. +-- +-- +-- # 1) AI_AIR constructor +-- +-- * @{#AI_AIR.New}(): Creates a new AI_AIR object. +-- +-- # 2) AI_AIR is a Finite State Machine. +-- +-- This section must be read as follows. Each of the rows indicate a state transition, triggered through an event, and with an ending state of the event was executed. +-- The first column is the **From** state, the second column the **Event**, and the third column the **To** state. +-- +-- So, each of the rows have the following structure. +-- +-- * **From** => **Event** => **To** +-- +-- Important to know is that an event can only be executed if the **current state** is the **From** state. +-- This, when an **Event** that is being triggered has a **From** state that is equal to the **Current** state of the state machine, the event will be executed, +-- and the resulting state will be the **To** state. +-- +-- These are the different possible state transitions of this state machine implementation: +-- +-- * Idle => Start => Monitoring +-- +-- ## 2.1) AI_AIR States. +-- +-- * **Idle**: The process is idle. +-- +-- ## 2.2) AI_AIR Events. +-- +-- * **Start**: Start the transport process. +-- * **Stop**: Stop the transport process. +-- * **Monitor**: Monitor and take action. +-- +-- @field #AI_AIR +AI_AIR = { + ClassName = "AI_AIR", +} + +AI_AIR.TaskDelay = 0.5 -- The delay of each task given to the AI. + +--- Creates a new AI_AIR process. +-- @param #AI_AIR self +-- @param Wrapper.Group#GROUP AIGroup The group object to receive the A2G Process. +-- @return #AI_AIR +function AI_AIR:New( AIGroup ) + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- #AI_AIR + + self:SetControllable( AIGroup ) + + self:SetStartState( "Stopped" ) + + self:AddTransition( "*", "Queue", "Queued" ) + + self:AddTransition( "*", "Start", "Started" ) + + --- Start Handler OnBefore for AI_AIR + -- @function [parent=#AI_AIR] OnBeforeStart + -- @param #AI_AIR self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Start Handler OnAfter for AI_AIR + -- @function [parent=#AI_AIR] OnAfterStart + -- @param #AI_AIR self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Start Trigger for AI_AIR + -- @function [parent=#AI_AIR] Start + -- @param #AI_AIR self + + --- Start Asynchronous Trigger for AI_AIR + -- @function [parent=#AI_AIR] __Start + -- @param #AI_AIR self + -- @param #number Delay + + self:AddTransition( "*", "Stop", "Stopped" ) + +--- OnLeave Transition Handler for State Stopped. +-- @function [parent=#AI_AIR] OnLeaveStopped +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Stopped. +-- @function [parent=#AI_AIR] OnEnterStopped +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- OnBefore Transition Handler for Event Stop. +-- @function [parent=#AI_AIR] OnBeforeStop +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event Stop. +-- @function [parent=#AI_AIR] OnAfterStop +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event Stop. +-- @function [parent=#AI_AIR] Stop +-- @param #AI_AIR self + +--- Asynchronous Event Trigger for Event Stop. +-- @function [parent=#AI_AIR] __Stop +-- @param #AI_AIR self +-- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "Status", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR. + +--- OnBefore Transition Handler for Event Status. +-- @function [parent=#AI_AIR] OnBeforeStatus +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event Status. +-- @function [parent=#AI_AIR] OnAfterStatus +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event Status. +-- @function [parent=#AI_AIR] Status +-- @param #AI_AIR self + +--- Asynchronous Event Trigger for Event Status. +-- @function [parent=#AI_AIR] __Status +-- @param #AI_AIR self +-- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "RTB", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR. + +--- OnBefore Transition Handler for Event RTB. +-- @function [parent=#AI_AIR] OnBeforeRTB +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event RTB. +-- @function [parent=#AI_AIR] OnAfterRTB +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event RTB. +-- @function [parent=#AI_AIR] RTB +-- @param #AI_AIR self + +--- Asynchronous Event Trigger for Event RTB. +-- @function [parent=#AI_AIR] __RTB +-- @param #AI_AIR self +-- @param #number Delay The delay in seconds. + +--- OnLeave Transition Handler for State Returning. +-- @function [parent=#AI_AIR] OnLeaveReturning +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Returning. +-- @function [parent=#AI_AIR] OnEnterReturning +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + + self:AddTransition( "Patrolling", "Refuel", "Refuelling" ) + + --- Refuel Handler OnBefore for AI_AIR + -- @function [parent=#AI_AIR] OnBeforeRefuel + -- @param #AI_AIR self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Refuel Handler OnAfter for AI_AIR + -- @function [parent=#AI_AIR] OnAfterRefuel + -- @param #AI_AIR self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Refuel Trigger for AI_AIR + -- @function [parent=#AI_AIR] Refuel + -- @param #AI_AIR self + + --- Refuel Asynchronous Trigger for AI_AIR + -- @function [parent=#AI_AIR] __Refuel + -- @param #AI_AIR self + -- @param #number Delay + + self:AddTransition( "*", "Takeoff", "Airborne" ) + self:AddTransition( "*", "Return", "Returning" ) + self:AddTransition( "*", "Hold", "Holding" ) + self:AddTransition( "*", "Home", "Home" ) + self:AddTransition( "*", "LostControl", "LostControl" ) + self:AddTransition( "*", "Fuel", "Fuel" ) + self:AddTransition( "*", "Damaged", "Damaged" ) + self:AddTransition( "*", "Eject", "*" ) + self:AddTransition( "*", "Crash", "Crashed" ) + self:AddTransition( "*", "PilotDead", "*" ) + + self.IdleCount = 0 + + return self +end + +--- @param Wrapper.Group#GROUP self +-- @param Core.Event#EVENTDATA EventData +function GROUP:OnEventTakeoff( EventData, Fsm ) + Fsm:Takeoff() + self:UnHandleEvent( EVENTS.Takeoff ) +end + + + +function AI_AIR:SetDispatcher( Dispatcher ) + self.Dispatcher = Dispatcher +end + +function AI_AIR:GetDispatcher() + return self.Dispatcher +end + +function AI_AIR:SetTargetDistance( Coordinate ) + + local CurrentCoord = self.Controllable:GetCoordinate() + self.TargetDistance = CurrentCoord:Get2DDistance( Coordinate ) + + self.ClosestTargetDistance = ( not self.ClosestTargetDistance or self.ClosestTargetDistance > self.TargetDistance ) and self.TargetDistance or self.ClosestTargetDistance +end + + +function AI_AIR:ClearTargetDistance() + + self.TargetDistance = nil + self.ClosestTargetDistance = nil +end + + +--- Sets (modifies) the minimum and maximum speed of the patrol. +-- @param #AI_AIR self +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h. +-- @return #AI_AIR self +function AI_AIR:SetSpeed( PatrolMinSpeed, PatrolMaxSpeed ) + self:F2( { PatrolMinSpeed, PatrolMaxSpeed } ) + + self.PatrolMinSpeed = PatrolMinSpeed + self.PatrolMaxSpeed = PatrolMaxSpeed +end + + +--- Sets (modifies) the minimum and maximum RTB speed of the patrol. +-- @param #AI_AIR self +-- @param DCS#Speed RTBMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h. +-- @param DCS#Speed RTBMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h. +-- @return #AI_AIR self +function AI_AIR:SetRTBSpeed( RTBMinSpeed, RTBMaxSpeed ) + self:F2( { RTBMinSpeed, RTBMaxSpeed } ) + + self.RTBMinSpeed = RTBMinSpeed + self.RTBMaxSpeed = RTBMaxSpeed +end + + +--- Sets the floor and ceiling altitude of the patrol. +-- @param #AI_AIR self +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @return #AI_AIR self +function AI_AIR:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude ) + self:F2( { PatrolFloorAltitude, PatrolCeilingAltitude } ) + + self.PatrolFloorAltitude = PatrolFloorAltitude + self.PatrolCeilingAltitude = PatrolCeilingAltitude +end + + +--- Sets the home airbase. +-- @param #AI_AIR self +-- @param Wrapper.Airbase#AIRBASE HomeAirbase +-- @return #AI_AIR self +function AI_AIR:SetHomeAirbase( HomeAirbase ) + self:F2( { HomeAirbase } ) + + self.HomeAirbase = HomeAirbase +end + +--- Sets to refuel at the given tanker. +-- @param #AI_AIR self +-- @param Wrapper.Group#GROUP TankerName The group name of the tanker as defined within the Mission Editor or spawned. +-- @return #AI_AIR self +function AI_AIR:SetTanker( TankerName ) + self:F2( { TankerName } ) + + self.TankerName = TankerName +end + + +--- Sets the disengage range, that when engaging a target beyond the specified range, the engagement will be cancelled and the plane will RTB. +-- @param #AI_AIR self +-- @param #number DisengageRadius The disengage range. +-- @return #AI_AIR self +function AI_AIR:SetDisengageRadius( DisengageRadius ) + self:F2( { DisengageRadius } ) + + self.DisengageRadius = DisengageRadius +end + +--- Set the status checking off. +-- @param #AI_AIR self +-- @return #AI_AIR self +function AI_AIR:SetStatusOff() + self:F2() + + self.CheckStatus = false +end + + +--- When the AI is out of fuel, it is required that a new AI is started, before the old AI can return to the home base. +-- Therefore, with a parameter and a calculation of the distance to the home base, the fuel treshold is calculated. +-- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, while a new AIControllable is targetted to the AI_AIR. +-- Once the time is finished, the old AI will return to the base. +-- @param #AI_AIR self +-- @param #number FuelThresholdPercentage The treshold in percentage (between 0 and 1) when the AIControllable is considered to get out of fuel. +-- @param #number OutOfFuelOrbitTime The amount of seconds the out of fuel AIControllable will orbit before returning to the base. +-- @return #AI_AIR self +function AI_AIR:SetFuelThreshold( FuelThresholdPercentage, OutOfFuelOrbitTime ) + + self.FuelThresholdPercentage = FuelThresholdPercentage + self.OutOfFuelOrbitTime = OutOfFuelOrbitTime + + self.Controllable:OptionRTBBingoFuel( false ) + + return self +end + +--- When the AI is damaged beyond a certain treshold, it is required that the AI returns to the home base. +-- However, damage cannot be foreseen early on. +-- Therefore, when the damage treshold is reached, +-- the AI will return immediately to the home base (RTB). +-- Note that for groups, the average damage of the complete group will be calculated. +-- So, in a group of 4 airplanes, 2 lost and 2 with damage 0.2, the damage treshold will be 0.25. +-- @param #AI_AIR self +-- @param #number PatrolDamageThreshold The treshold in percentage (between 0 and 1) when the AI is considered to be damaged. +-- @return #AI_AIR self +function AI_AIR:SetDamageThreshold( PatrolDamageThreshold ) + + self.PatrolManageDamage = true + self.PatrolDamageThreshold = PatrolDamageThreshold + + return self +end + + + +--- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. +-- @param #AI_AIR self +-- @return #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_AIR:onafterStart( Controllable, From, Event, To ) + + self:__Status( 10 ) -- Check status status every 30 seconds. + + self:HandleEvent( EVENTS.PilotDead, self.OnPilotDead ) + self:HandleEvent( EVENTS.Crash, self.OnCrash ) + self:HandleEvent( EVENTS.Ejection, self.OnEjection ) + + Controllable:OptionROEHoldFire() + Controllable:OptionROTVertical() +end + + + +--- @param #AI_AIR self +function AI_AIR:onbeforeStatus() + + return self.CheckStatus +end + +--- @param #AI_AIR self +function AI_AIR:onafterStatus() + + if self.Controllable and self.Controllable:IsAlive() then + + local RTB = false + + local DistanceFromHomeBase = self.HomeAirbase:GetCoordinate():Get2DDistance( self.Controllable:GetCoordinate() ) + + if not self:Is( "Holding" ) and not self:Is( "Returning" ) then + local DistanceFromHomeBase = self.HomeAirbase:GetCoordinate():Get2DDistance( self.Controllable:GetCoordinate() ) + + if DistanceFromHomeBase > self.DisengageRadius then + self:E( self.Controllable:GetName() .. " is too far from home base, RTB!" ) + self:Hold( 300 ) + RTB = false + end + end + +-- I think this code is not requirement anymore after release 2.5. +-- if self:Is( "Fuel" ) or self:Is( "Damaged" ) or self:Is( "LostControl" ) then +-- if DistanceFromHomeBase < 5000 then +-- self:E( self.Controllable:GetName() .. " is near the home base, RTB!" ) +-- self:Home( "Destroy" ) +-- end +-- end + + + if not self:Is( "Fuel" ) and not self:Is( "Home" ) then + + local Fuel = self.Controllable:GetFuelMin() + + -- If the fuel in the controllable is below the treshold percentage, + -- then send for refuel in case of a tanker, otherwise RTB. + if Fuel < self.FuelThresholdPercentage then + + if self.TankerName then + self:E( self.Controllable:GetName() .. " is out of fuel: " .. Fuel .. " ... Refuelling at Tanker!" ) + self:Refuel() + else + self:E( self.Controllable:GetName() .. " is out of fuel: " .. Fuel .. " ... RTB!" ) + local OldAIControllable = self.Controllable + + local OrbitTask = OldAIControllable:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed ) + local TimedOrbitTask = OldAIControllable:TaskControlled( OrbitTask, OldAIControllable:TaskCondition(nil,nil,nil,nil,self.OutOfFuelOrbitTime,nil ) ) + OldAIControllable:SetTask( TimedOrbitTask, 10 ) + + self:Fuel() + RTB = true + end + else + end + end + + -- TODO: Check GROUP damage function. + local Damage = self.Controllable:GetLife() + local InitialLife = self.Controllable:GetLife0() + + -- If the group is damaged, then RTB. + -- Note that a group can consist of more units, so if one unit is damaged of a group, the mission may continue. + -- The damaged unit will RTB due to DCS logic, and the others will continue to engage. + if ( Damage / InitialLife ) < self.PatrolDamageThreshold then + self:E( self.Controllable:GetName() .. " is damaged: " .. Damage .. " ... RTB!" ) + self:Damaged() + RTB = true + self:SetStatusOff() + end + + -- Check if planes went RTB and are out of control. + -- We only check if planes are out of control, when they are in duty. + if self.Controllable:HasTask() == false then + if not self:Is( "Started" ) and + not self:Is( "Stopped" ) and + not self:Is( "Fuel" ) and + not self:Is( "Damaged" ) and + not self:Is( "Home" ) then + if self.IdleCount >= 10 then + if Damage ~= InitialLife then + self:Damaged() + else + self:E( self.Controllable:GetName() .. " control lost! " ) + + self:LostControl() + end + else + self.IdleCount = self.IdleCount + 1 + end + end + else + self.IdleCount = 0 + end + + if RTB == true then + self:__RTB( self.TaskDelay ) + end + + if not self:Is("Home") then + self:__Status( 10 ) + end + + end +end + + +--- @param Wrapper.Group#GROUP AIGroup +function AI_AIR.RTBRoute( AIGroup, Fsm ) + + AIGroup:F( { "AI_AIR.RTBRoute:", AIGroup:GetName() } ) + + if AIGroup:IsAlive() then + Fsm:RTB() + end + +end + +--- @param Wrapper.Group#GROUP AIGroup +function AI_AIR.RTBHold( AIGroup, Fsm ) + + AIGroup:F( { "AI_AIR.RTBHold:", AIGroup:GetName() } ) + if AIGroup:IsAlive() then + Fsm:__RTB( Fsm.TaskDelay ) + Fsm:Return() + local Task = AIGroup:TaskOrbitCircle( 4000, 400 ) + AIGroup:SetTask( Task ) + end + +end + + +--- @param #AI_AIR self +-- @param Wrapper.Group#GROUP AIGroup +function AI_AIR:onafterRTB( AIGroup, From, Event, To ) + self:F( { AIGroup, From, Event, To } ) + + + if AIGroup and AIGroup:IsAlive() then + + self:E( "Group " .. AIGroup:GetName() .. " ... RTB! ( " .. self:GetState() .. " )" ) + + self:ClearTargetDistance() + --AIGroup:ClearTasks() + + local EngageRoute = {} + + --- Calculate the target route point. + + local FromCoord = AIGroup:GetCoordinate() + local ToTargetCoord = self.HomeAirbase:GetCoordinate() + local ToTargetSpeed = math.random( self.RTBMinSpeed, self.RTBMaxSpeed ) + local ToAirbaseAngle = FromCoord:GetAngleDegrees( FromCoord:GetDirectionVec3( ToTargetCoord ) ) + + local Distance = FromCoord:Get2DDistance( ToTargetCoord ) + + local ToAirbaseCoord = FromCoord:Translate( 5000, ToAirbaseAngle ) + if Distance < 5000 then + self:E( "RTB and near the airbase!" ) + self:Home() + return + end + + --- Create a route point of type air. + local FromRTBRoutePoint = FromCoord:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + --- Create a route point of type air. + local ToRTBRoutePoint = ToAirbaseCoord:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = FromRTBRoutePoint + EngageRoute[#EngageRoute+1] = ToRTBRoutePoint + + local Tasks = {} + Tasks[#Tasks+1] = AIGroup:TaskFunction( "AI_AIR.RTBRoute", self ) + + EngageRoute[#EngageRoute].task = AIGroup:TaskCombo( Tasks ) + + AIGroup:OptionROEHoldFire() + AIGroup:OptionROTEvadeFire() + + --- NOW ROUTE THE GROUP! + AIGroup:Route( EngageRoute, self.TaskDelay ) + + end + +end + +--- @param #AI_AIR self +-- @param Wrapper.Group#GROUP AIGroup +function AI_AIR:onafterHome( AIGroup, From, Event, To ) + self:F( { AIGroup, From, Event, To } ) + + self:E( "Group " .. self.Controllable:GetName() .. " ... Home! ( " .. self:GetState() .. " )" ) + + if AIGroup and AIGroup:IsAlive() then + end + +end + + + +--- @param #AI_AIR self +-- @param Wrapper.Group#GROUP AIGroup +function AI_AIR:onafterHold( AIGroup, From, Event, To, HoldTime ) + self:F( { AIGroup, From, Event, To } ) + + self:E( "Group " .. self.Controllable:GetName() .. " ... Holding! ( " .. self:GetState() .. " )" ) + + if AIGroup and AIGroup:IsAlive() then + local OrbitTask = AIGroup:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed ) + local TimedOrbitTask = AIGroup:TaskControlled( OrbitTask, AIGroup:TaskCondition( nil, nil, nil, nil, HoldTime , nil ) ) + + local RTBTask = AIGroup:TaskFunction( "AI_AIR.RTBHold", self ) + + local OrbitHoldTask = AIGroup:TaskOrbitCircle( 4000, self.PatrolMinSpeed ) + + --AIGroup:SetState( AIGroup, "AI_AIR", self ) + + AIGroup:SetTask( AIGroup:TaskCombo( { TimedOrbitTask, RTBTask, OrbitHoldTask } ), 1 ) + end + +end + +--- @param Wrapper.Group#GROUP AIGroup +function AI_AIR.Resume( AIGroup, Fsm ) + + AIGroup:I( { "AI_AIR.Resume:", AIGroup:GetName() } ) + if AIGroup:IsAlive() then + Fsm:__RTB( Fsm.TaskDelay ) + end + +end + +--- @param #AI_AIR self +-- @param Wrapper.Group#GROUP AIGroup +function AI_AIR:onafterRefuel( AIGroup, From, Event, To ) + self:F( { AIGroup, From, Event, To } ) + + self:E( "Group " .. self.Controllable:GetName() .. " ... Refuelling! ( " .. self:GetState() .. " )" ) + + if AIGroup and AIGroup:IsAlive() then + local Tanker = GROUP:FindByName( self.TankerName ) + if Tanker:IsAlive() and Tanker:IsAirPlane() then + + local RefuelRoute = {} + + --- Calculate the target route point. + + local FromRefuelCoord = AIGroup:GetCoordinate() + local ToRefuelCoord = Tanker:GetCoordinate() + local ToRefuelSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) + + --- Create a route point of type air. + local FromRefuelRoutePoint = FromRefuelCoord:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToRefuelSpeed, + true + ) + + --- Create a route point of type air. + local ToRefuelRoutePoint = ToRefuelCoord:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToRefuelSpeed, + true + ) + + self:F( { ToRefuelSpeed = ToRefuelSpeed } ) + + RefuelRoute[#RefuelRoute+1] = FromRefuelRoutePoint + RefuelRoute[#RefuelRoute+1] = ToRefuelRoutePoint + + AIGroup:OptionROEHoldFire() + AIGroup:OptionROTEvadeFire() + + local Tasks = {} + Tasks[#Tasks+1] = AIGroup:TaskRefueling() + Tasks[#Tasks+1] = AIGroup:TaskFunction( self:GetClassName() .. ".Resume", self ) + RefuelRoute[#RefuelRoute].task = AIGroup:TaskCombo( Tasks ) + + AIGroup:Route( RefuelRoute, self.TaskDelay ) + else + self:RTB() + end + end + +end + + + +--- @param #AI_AIR self +function AI_AIR:onafterDead() + self:SetStatusOff() +end + + +--- @param #AI_AIR self +-- @param Core.Event#EVENTDATA EventData +function AI_AIR:OnCrash( EventData ) + + if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then + self:E( self.Controllable:GetUnits() ) + if #self.Controllable:GetUnits() == 1 then + self:__Crash( self.TaskDelay, EventData ) + end + end +end + +--- @param #AI_AIR self +-- @param Core.Event#EVENTDATA EventData +function AI_AIR:OnEjection( EventData ) + + if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then + self:__Eject( self.TaskDelay, EventData ) + end +end + +--- @param #AI_AIR self +-- @param Core.Event#EVENTDATA EventData +function AI_AIR:OnPilotDead( EventData ) + + if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then + self:__PilotDead( self.TaskDelay, EventData ) + end +end diff --git a/Moose Development/Moose/AI/AI_Formation.lua b/Moose Development/Moose/AI/AI_Formation.lua index 489e69cf3..c02096609 100644 --- a/Moose Development/Moose/AI/AI_Formation.lua +++ b/Moose Development/Moose/AI/AI_Formation.lua @@ -36,6 +36,7 @@ -- @field #boolean ReportTargets If true, nearby targets are reported. -- @Field DCSTypes#AI.Option.Air.val.ROE OptionROE Which ROE is set to the FollowGroup. -- @field DCSTypes#AI.Option.Air.val.REACTION_ON_THREAT OptionReactionOnThreat Which REACTION_ON_THREAT is set to the FollowGroup. +-- @field #number dtFollow Time step between position updates. --- Build large formations, make AI follow a @{Wrapper.Client#CLIENT} (player) leader or a @{Wrapper.Unit#UNIT} (AI) leader. @@ -106,6 +107,7 @@ AI_FORMATION = { FollowScheduler = nil, OptionROE = AI.Option.Air.val.ROE.OPEN_FIRE, OptionReactionOnThreat = AI.Option.Air.val.REACTION_ON_THREAT.ALLOW_ABORT_MISSION, + dtFollow = 0.5, } --- AI_FORMATION.Mode class @@ -125,6 +127,7 @@ AI_FORMATION = { -- @param Wrapper.Unit#UNIT FollowUnit The UNIT leading the FolllowGroupSet. -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. -- @param #string FollowName Name of the escort. +-- @param #string FollowBriefing Briefing. -- @return #AI_FORMATION self function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefing ) --R2.1 local self = BASE:Inherit( self, FSM_SET:New( FollowGroupSet ) ) @@ -139,7 +142,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin self:AddTransition( "*", "Stop", "Stopped" ) - self:AddTransition( "None", "Start", "Following" ) + self:AddTransition( {"None", "Stopped"}, "Start", "Following" ) self:AddTransition( "*", "FormationLine", "*" ) --- FormationLine Handler OnBefore for AI_FORMATION @@ -620,6 +623,16 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin return self end + +--- Set time interval between updates of the formation. +-- @param #AI_FORMATION self +-- @param #number dt Time step in seconds between formation updates. Default is every 0.5 seconds. +-- @return #AI_FORMATION +function AI_FORMATION:SetFollowTimeInterval(dt) --R2.1 + self.dtFollow=dt or 0.5 + return self +end + --- This function is for test, it will put on the frequency of the FollowScheduler a red smoke at the direction vector calculated for the escort to fly to. -- This allows to visualize where the escort is flying to. -- @param #AI_FORMATION self @@ -893,7 +906,30 @@ function AI_FORMATION:SetFlightRandomization( FlightRandomization ) --R2.1 end ---- @param Follow#AI_FORMATION self +--- Stop function. Formation will not be updated any more. +-- @param #AI_FORMATION self +-- @param Core.Set#SET_GROUP FollowGroupSet The following set of groups. +-- @param #string From From state. +-- @param #string Event Event. +-- @pram #string To The to state. +function AI_FORMATION:onafterStop(FollowGroupSet, From, Event, To) --R2.1 + self:E("Stopping formation.") +end + +--- Follow event fuction. Check if coming from state "stopped". If so the transition is rejected. +-- @param #AI_FORMATION self +-- @param Core.Set#SET_GROUP FollowGroupSet The following set of groups. +-- @param #string From From state. +-- @param #string Event Event. +-- @pram #string To The to state. +function AI_FORMATION:onbeforeFollow( FollowGroupSet, From, Event, To ) --R2.1 + if From=="Stopped" then + return false -- Deny transition. + end + return true +end + +--- @param #AI_FORMATION self function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 self:F( ) @@ -1032,8 +1068,8 @@ function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 end, self, ClientUnit, CT1, CV1, CT2, CV2 ) - - self:__Follow( -0.5 ) + + self:__Follow( -self.dtFollow ) end end diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index 528ac9a84..2a5c8d0ee 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -342,18 +342,18 @@ do -- COORDINATE return x - Precision <= self.x and x + Precision >= self.x and z - Precision <= self.z and z + Precision >= self.z end - --- Returns if the 2 coordinates are at the same 2D position. + --- Scan/find objects (units, statics, scenery) within a certain radius around the coordinate using the world.searchObjects() DCS API function. -- @param #COORDINATE self -- @param #number radius (Optional) Scan radius in meters. Default 100 m. -- @param #boolean scanunits (Optional) If true scan for units. Default true. -- @param #boolean scanstatics (Optional) If true scan for static objects. Default true. -- @param #boolean scanscenery (Optional) If true scan for scenery objects. Default false. - -- @return True if units were found. - -- @return True if statics were found. - -- @return True if scenery objects were found. - -- @return Unit objects found. - -- @return Static objects found. - -- @return Scenery objects found. + -- @return #boolean True if units were found. + -- @return #boolean True if statics were found. + -- @return #boolean True if scenery objects were found. + -- @return #table Table of MOOSE @[#Wrapper.Unit#UNIT} objects found. + -- @return #table Table of DCS static objects found. + -- @return #table Table of DCS scenery objects found. function COORDINATE:ScanObjects(radius, scanunits, scanstatics, scanscenery) self:F(string.format("Scanning in radius %.1f m.", radius)) @@ -405,18 +405,17 @@ do -- COORDINATE local ObjectCategory = ZoneObject:getCategory() -- Check for unit or static objects - --if (ObjectCategory == Object.Category.UNIT and ZoneObject:isExist() and ZoneObject:isActive()) then - if (ObjectCategory == Object.Category.UNIT and ZoneObject:isExist()) then + if ObjectCategory==Object.Category.UNIT and ZoneObject:isExist() then table.insert(Units, UNIT:Find(ZoneObject)) gotunits=true - elseif (ObjectCategory == Object.Category.STATIC and ZoneObject:isExist()) then + elseif ObjectCategory==Object.Category.STATIC and ZoneObject:isExist() then table.insert(Statics, ZoneObject) gotstatics=true - elseif ObjectCategory == Object.Category.SCENERY then + elseif ObjectCategory==Object.Category.SCENERY then table.insert(Scenery, ZoneObject) gotscenery=true @@ -460,18 +459,47 @@ do -- COORDINATE --- Add a Distance in meters from the COORDINATE orthonormal plane, with the given angle, and calculate the new COORDINATE. -- @param #COORDINATE self -- @param DCS#Distance Distance The Distance to be added in meters. - -- @param DCS#Angle Angle The Angle in degrees. - -- @return #COORDINATE The new calculated COORDINATE. - function COORDINATE:Translate( Distance, Angle ) + -- @param DCS#Angle Angle The Angle in degrees. Defaults to 0 if not specified (nil). + -- @param #boolean Keepalt If true, keep altitude of original coordinate. Default is that the new coordinate is created at the translated land height. + -- @return Core.Point#COORDINATE The new calculated COORDINATE. + function COORDINATE:Translate( Distance, Angle, Keepalt ) local SX = self.x local SY = self.z - local Radians = Angle / 180 * math.pi + local Radians = (Angle or 0) / 180 * math.pi local TX = Distance * math.cos( Radians ) + SX local TY = Distance * math.sin( Radians ) + SY - - return COORDINATE:NewFromVec2( { x = TX, y = TY } ) + + if Keepalt then + return COORDINATE:NewFromVec3( { x = TX, y=self.y, z = TY } ) + else + return COORDINATE:NewFromVec2( { x = TX, y = TY } ) + end end + --- Rotate coordinate in 2D (x,z) space. + -- @param #COORDINATE self + -- @param DCS#Angle Angle Angle of rotation in degrees. + -- @return Core.Point#COORDINATE The rotated coordinate. + function COORDINATE:Rotate2D(Angle) + + if not Angle then + return self + end + + local phi=math.rad(Angle) + + local X=self.z + local Y=self.x + + --slocal R=math.sqrt(X*X+Y*Y) + + local x=X*math.cos(phi)-Y*math.sin(phi) + local y=X*math.sin(phi)+Y*math.cos(phi) + + -- Coordinate assignment looks bit strange but is correct. + return COORDINATE:NewFromVec3({x=y, y=self.y, z=x}) + end + --- Return a random Vec2 within an Outer Radius and optionally NOT within an Inner Radius of the COORDINATE. -- @param #COORDINATE self -- @param DCS#Distance OuterRadius @@ -1003,11 +1031,15 @@ do -- COORDINATE function COORDINATE:WaypointAir( AltType, Type, Action, Speed, SpeedLocked, airbase, DCSTasks, description ) self:F2( { AltType, Type, Action, Speed, SpeedLocked } ) - -- Defaults + -- Set alttype or "RADIO" which is AGL. AltType=AltType or "RADIO" + + -- Speedlocked by default if SpeedLocked==nil then SpeedLocked=true end + + -- Speed or default 500 km/h. Speed=Speed or 500 -- Waypoint array. @@ -1016,19 +1048,26 @@ do -- COORDINATE -- Coordinates. RoutePoint.x = self.x RoutePoint.y = self.z + -- Altitude. RoutePoint.alt = self.y RoutePoint.alt_type = AltType + -- Waypoint type. RoutePoint.type = Type or nil RoutePoint.action = Action or nil - -- Set speed/ETA. + + -- Speed. RoutePoint.speed = Speed/3.6 RoutePoint.speed_locked = SpeedLocked - RoutePoint.ETA=nil - RoutePoint.ETA_locked = false + + -- ETA. + RoutePoint.ETA=0 + RoutePoint.ETA_locked=true + -- Waypoint description. RoutePoint.name=description + -- Airbase parameters for takeoff and landing points. if airbase then local AirbaseID = airbase:GetID() @@ -1037,31 +1076,29 @@ do -- COORDINATE RoutePoint.linkUnit = AirbaseID RoutePoint.helipadId = AirbaseID elseif AirbaseCategory == Airbase.Category.AIRDROME then - RoutePoint.airdromeId = AirbaseID + RoutePoint.airdromeId = AirbaseID else self:T("ERROR: Unknown airbase category in COORDINATE:WaypointAir()!") - end - end + end + + --self:MarkToAll(string.format("Landing waypoint at airbase %s, ID=%d, Category=%d", airbase:GetName(), AirbaseID, AirbaseCategory )) + end - - -- ["task"] = - -- { - -- ["id"] = "ComboTask", - -- ["params"] = - -- { - -- ["tasks"] = - -- { - -- }, -- end of ["tasks"] - -- }, -- end of ["params"] - -- }, -- end of ["task"] - -- Waypoint tasks. RoutePoint.task = {} RoutePoint.task.id = "ComboTask" RoutePoint.task.params = {} RoutePoint.task.params.tasks = DCSTasks or {} + + --RoutePoint.properties={} + --RoutePoint.properties.addopt={} + + --RoutePoint.formation_template="" + -- Debug. self:T({RoutePoint=RoutePoint}) + + -- Return waypoint. return RoutePoint end @@ -1121,6 +1158,9 @@ do -- COORDINATE --- Build a Waypoint Air "Landing". -- @param #COORDINATE self -- @param DCS#Speed Speed Airspeed in km/h. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase for takeoff and landing points. + -- @param #table DCSTasks A table of @{DCS#Task} items which are executed at the waypoint. + -- @param #string description A text description of the waypoint, which will be shown on the F10 map. -- @return #table The route point. -- @usage -- @@ -1129,8 +1169,8 @@ do -- COORDINATE -- LandingWaypoint = LandingCoord:WaypointAirLanding( 60 ) -- HeliGroup:Route( { LandWaypoint }, 1 ) -- Start landing the helicopter in one second. -- - function COORDINATE:WaypointAirLanding( Speed ) - return self:WaypointAir( nil, COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, Speed ) + function COORDINATE:WaypointAirLanding( Speed, airbase, DCSTasks, description ) + return self:WaypointAir(nil, COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, Speed, false, airbase, DCSTasks, description) end diff --git a/Moose Development/Moose/Core/Radio.lua b/Moose Development/Moose/Core/Radio.lua index 954fc51a9..6155aa036 100644 --- a/Moose Development/Moose/Core/Radio.lua +++ b/Moose Development/Moose/Core/Radio.lua @@ -9,12 +9,12 @@ -- -- The Radio contains 2 classes : RADIO and BEACON -- --- What are radio communications in DCS ? +-- What are radio communications in DCS? -- -- * Radio transmissions consist of **sound files** that are broadcasted on a specific **frequency** (e.g. 115MHz) and **modulation** (e.g. AM), -- * They can be **subtitled** for a specific **duration**, the **power** in Watts of the transmiter's antenna can be set, and the transmission can be **looped**. -- --- How to supply DCS my own Sound Files ? +-- How to supply DCS my own Sound Files? -- -- * Your sound files need to be encoded in **.ogg** or .wav, -- * Your sound files should be **as tiny as possible**. It is suggested you encode in .ogg with low bitrate and sampling settings, @@ -23,7 +23,7 @@ -- -- Due to weird DCS quirks, **radio communications behave differently** if sent by a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP} or by any other @{Wrapper.Positionable#POSITIONABLE} -- --- * If the transmitter is a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}, DCS will set the power of the transmission automatically, +-- * If the transmitter is a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}, DCS will set the power of the transmission automatically, -- * If the transmitter is any other @{Wrapper.Positionable#POSITIONABLE}, the transmisison can't be subtitled or looped. -- -- Note that obviously, the **frequency** and the **modulation** of the transmission are important only if the players are piloting an **Advanced System Modelling** enabled aircraft, @@ -33,7 +33,7 @@ -- -- === -- --- ### Author: Hugues "Grey_Echo" Bousquet +-- ### Authors: Hugues "Grey_Echo" Bousquet, funkyfranky -- -- @module Core.Radio -- @image Core_Radio.JPG @@ -66,24 +66,25 @@ -- * @{#RADIO.SetPower}() : Sets the power of the antenna in Watts -- * @{#RADIO.NewGenericTransmission}() : Shortcut to set all the relevant parameters in one method call -- --- What is this power thing ? +-- What is this power thing? -- -- * If your transmission is sent by a @{Wrapper.Positionable#POSITIONABLE} other than a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}, you can set the power of the antenna, -- * Otherwise, DCS sets it automatically, depending on what's available on your Unit, --- * If the player gets **too far** from the transmiter, or if the antenna is **too weak**, the transmission will **fade** and **become noisyer**, +-- * If the player gets **too far** from the transmitter, or if the antenna is **too weak**, the transmission will **fade** and **become noisyer**, -- * This an automated DCS calculation you have no say on, --- * For reference, a standard VOR station has a 100W antenna, a standard AA TACAN has a 120W antenna, and civilian ATC's antenna usually range between 300 and 500W, +-- * For reference, a standard VOR station has a 100 W antenna, a standard AA TACAN has a 120 W antenna, and civilian ATC's antenna usually range between 300 and 500 W, -- * Note that if the transmission has a subtitle, it will be readable, regardless of the quality of the transmission. -- -- @type RADIO --- @field Positionable#POSITIONABLE Positionable The transmiter --- @field #string FileName Name of the sound file --- @field #number Frequency Frequency of the transmission in Hz --- @field #number Modulation Modulation of the transmission (either radio.modulation.AM or radio.modulation.FM) --- @field #string Subtitle Subtitle of the transmission --- @field #number SubtitleDuration Duration of the Subtitle in seconds --- @field #number Power Power of the antenna is Watts --- @field #boolean Loop (default true) +-- @field Wrapper.Controllable#CONTROLLABLE Positionable The @{#CONTROLLABLE} that will transmit the radio calls. +-- @field #string FileName Name of the sound file played. +-- @field #number Frequency Frequency of the transmission in Hz. +-- @field #number Modulation Modulation of the transmission (either radio.modulation.AM or radio.modulation.FM). +-- @field #string Subtitle Subtitle of the transmission. +-- @field #number SubtitleDuration Duration of the Subtitle in seconds. +-- @field #number Power Power of the antenna is Watts. +-- @field #boolean Loop Transmission is repeated (default true). +-- @field #string alias Name of the radio transmitter. -- @extends Core.Base#BASE RADIO = { ClassName = "RADIO", @@ -93,19 +94,19 @@ RADIO = { Subtitle = "", SubtitleDuration = 0, Power = 100, - Loop = true, + Loop = false, + alias=nil, } ---- Create a new RADIO Object. This doesn't broadcast a transmission, though, use @{#RADIO.Broadcast} to actually broadcast --- If you want to create a RADIO, you probably should use @{Wrapper.Positionable#POSITIONABLE.GetRadio}() instead +--- Create a new RADIO Object. This doesn't broadcast a transmission, though, use @{#RADIO.Broadcast} to actually broadcast. +-- If you want to create a RADIO, you probably should use @{Wrapper.Positionable#POSITIONABLE.GetRadio}() instead. -- @param #RADIO self -- @param Wrapper.Positionable#POSITIONABLE Positionable The @{Positionable} that will receive radio capabilities. --- @return #RADIO Radio --- @return #nil If Positionable is invalid +-- @return #RADIO The RADIO object or #nil if Positionable is invalid. function RADIO:New(Positionable) + + -- Inherit base local self = BASE:Inherit( self, BASE:New() ) -- Core.Radio#RADIO - - self.Loop = true -- default Loop to true (not sure the above RADIO definition actually is working) self:F(Positionable) if Positionable:GetPointVec2() then -- It's stupid, but the only way I found to make sure positionable is valid @@ -113,11 +114,27 @@ function RADIO:New(Positionable) return self end - self:E({"The passed positionable is invalid, no RADIO created", Positionable}) + self:E({error="The passed positionable is invalid, no RADIO created!", positionable=Positionable}) return nil end ---- Check validity of the filename passed and sets RADIO.FileName +--- Set alias of the transmitter. +-- @param #RADIO self +-- @param #string alias Name of the radio transmitter. +-- @return #RADIO self +function RADIO:SetAlias(alias) + self.alias=tostring(alias) + return self +end + +--- Get alias of the transmitter. +-- @param #RADIO self +-- @return #string Name of the transmitter. +function RADIO:GetAlias() + return tostring(self.alias) +end + +--- Set the file name for the radio transmission. -- @param #RADIO self -- @param #string FileName File name of the sound file (i.e. "Noise.ogg") -- @return #RADIO self @@ -125,49 +142,63 @@ function RADIO:SetFileName(FileName) self:F2(FileName) if type(FileName) == "string" then + if FileName:find(".ogg") or FileName:find(".wav") then if not FileName:find("l10n/DEFAULT/") then FileName = "l10n/DEFAULT/" .. FileName end + self.FileName = FileName return self end end - self:E({"File name invalid. Maybe something wrong with the extension ?", self.FileName}) + self:E({"File name invalid. Maybe something wrong with the extension?", FileName}) return self end ---- Check validity of the frequency passed and sets RADIO.Frequency +--- Set the frequency for the radio transmission. +-- If the transmitting positionable is a unit or group, this also set the command "SetFrequency" with the defined frequency and modulation. -- @param #RADIO self --- @param #number Frequency in MHz (Ranges allowed for radio transmissions in DCS : 30-88 / 108-152 / 225-400MHz) +-- @param #number Frequency Frequency in MHz. Ranges allowed for radio transmissions in DCS : 30-88 / 108-152 / 225-400MHz. -- @return #RADIO self function RADIO:SetFrequency(Frequency) self:F2(Frequency) + if type(Frequency) == "number" then + -- If frequency is in range if (Frequency >= 30 and Frequency < 88) or (Frequency >= 108 and Frequency < 152) or (Frequency >= 225 and Frequency < 400) then - self.Frequency = Frequency * 1000000 -- Conversion in Hz + + -- Convert frequency from MHz to Hz + self.Frequency = Frequency * 1000000 + -- If the RADIO is attached to a UNIT or a GROUP, we need to send the DCS Command "SetFrequency" to change the UNIT or GROUP frequency if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then - self.Positionable:SetCommand({ + + local commandSetFrequency={ id = "SetFrequency", params = { - frequency = self.Frequency, + frequency = self.Frequency, modulation = self.Modulation, } - }) + } + + self:T2(commandSetFrequency) + self.Positionable:SetCommand(commandSetFrequency) end + return self end end - self:E({"Frequency is outside of DCS Frequency ranges (30-80, 108-152, 225-400). Frequency unchanged.", self.Frequency}) + + self:E({"Frequency is outside of DCS Frequency ranges (30-80, 108-152, 225-400). Frequency unchanged.", Frequency}) return self end ---- Check validity of the frequency passed and sets RADIO.Modulation +--- Set AM or FM modulation of the radio transmitter. -- @param #RADIO self --- @param #number Modulation either radio.modulation.AM or radio.modulation.FM +-- @param #number Modulation Modulation is either radio.modulation.AM or radio.modulation.FM. -- @return #RADIO self function RADIO:SetModulation(Modulation) self:F2(Modulation) @@ -183,23 +214,24 @@ end --- Check validity of the power passed and sets RADIO.Power -- @param #RADIO self --- @param #number Power in W +-- @param #number Power Power in W. -- @return #RADIO self function RADIO:SetPower(Power) self:F2(Power) + if type(Power) == "number" then self.Power = math.floor(math.abs(Power)) --TODO Find what is the maximum power allowed by DCS and limit power to that - return self + else + self:E({"Power is invalid. Power unchanged.", self.Power}) end - self:E({"Power is invalid. Power unchanged.", self.Power}) + return self end ---- Check validity of the loop passed and sets RADIO.Loop +--- Set message looping on or off. -- @param #RADIO self --- @param #boolean Loop +-- @param #boolean Loop If true, message is repeated indefinitely. -- @return #RADIO self --- @usage function RADIO:SetLoop(Loop) self:F2(Loop) if type(Loop) == "boolean" then @@ -232,13 +264,12 @@ function RADIO:SetSubtitle(Subtitle, SubtitleDuration) self:E({"Subtitle is invalid. Subtitle reset.", self.Subtitle}) end if type(SubtitleDuration) == "number" then - if math.floor(math.abs(SubtitleDuration)) == SubtitleDuration then - self.SubtitleDuration = SubtitleDuration - return self - end + self.SubtitleDuration = SubtitleDuration + else + self.SubtitleDuration = 0 + self:E({"SubtitleDuration is invalid. SubtitleDuration reset.", self.SubtitleDuration}) end - self.SubtitleDuration = 0 - self:E({"SubtitleDuration is invalid. SubtitleDuration reset.", self.SubtitleDuration}) + return self end --- Create a new transmission, that is to say, populate the RADIO with relevant data @@ -246,10 +277,10 @@ end -- but it will work with a UNIT or a GROUP anyway. -- Only the #RADIO and the Filename are mandatory -- @param #RADIO self --- @param #string FileName --- @param #number Frequency in MHz --- @param #number Modulation either radio.modulation.AM or radio.modulation.FM --- @param #number Power in W +-- @param #string FileName Name of the sound file that will be transmitted. +-- @param #number Frequency Frequency in MHz. +-- @param #number Modulation Modulation of frequency, which is either radio.modulation.AM or radio.modulation.FM. +-- @param #number Power Power in W. -- @return #RADIO self function RADIO:NewGenericTransmission(FileName, Frequency, Modulation, Power, Loop) self:F({FileName, Frequency, Modulation, Power}) @@ -269,31 +300,43 @@ end -- but it will work for any @{Wrapper.Positionable#POSITIONABLE}. -- Only the RADIO and the Filename are mandatory. -- @param #RADIO self --- @param #string FileName --- @param #string Subtitle --- @param #number SubtitleDuration in s --- @param #number Frequency in MHz --- @param #number Modulation either radio.modulation.AM or radio.modulation.FM --- @param #boolean Loop +-- @param #string FileName Name of sound file. +-- @param #string Subtitle Subtitle to be displayed with sound file. +-- @param #number SubtitleDuration Duration of subtitle display in seconds. +-- @param #number Frequency Frequency in MHz. +-- @param #number Modulation Modulation which can be either radio.modulation.AM or radio.modulation.FM +-- @param #boolean Loop If true, loop message. -- @return #RADIO self function RADIO:NewUnitTransmission(FileName, Subtitle, SubtitleDuration, Frequency, Modulation, Loop) self:F({FileName, Subtitle, SubtitleDuration, Frequency, Modulation, Loop}) + -- Set file name. self:SetFileName(FileName) - local Duration = 5 - if SubtitleDuration then Duration = SubtitleDuration end - -- SubtitleDuration argument was missing, adding it - if Subtitle then self:SetSubtitle(Subtitle, Duration) end - -- self:SetSubtitleDuration is non existent, removing faulty line - -- if SubtitleDuration then self:SetSubtitleDuration(SubtitleDuration) end - if Frequency then self:SetFrequency(Frequency) end - if Modulation then self:SetModulation(Modulation) end - if Loop then self:SetLoop(Loop) end + + -- Set modulation AM/FM. + if Modulation then + self:SetModulation(Modulation) + end + + -- Set frequency. + if Frequency then + self:SetFrequency(Frequency) + end + + -- Set subtitle. + if Subtitle then + self:SetSubtitle(Subtitle, SubtitleDuration or 0) + end + + -- Set Looping. + if Loop then + self:SetLoop(Loop) + end return self end ---- Actually Broadcast the transmission +--- Broadcast the transmission. -- * The Radio has to be populated with the new transmission before broadcasting. -- * Please use RADIO setters or either @{#RADIO.NewGenericTransmission} or @{#RADIO.NewUnitTransmission} -- * This class is in fact pretty smart, it determines the right DCS function to use depending on the type of POSITIONABLE @@ -302,31 +345,38 @@ end -- * If your POSITIONABLE is a UNIT or a GROUP, the Power is ignored. -- * If your POSITIONABLE is not a UNIT or a GROUP, the Subtitle, SubtitleDuration are ignored -- @param #RADIO self +-- @param #boolean viatrigger Use trigger.action.radioTransmission() in any case, i.e. also for UNITS and GROUPS. -- @return #RADIO self -function RADIO:Broadcast() - self:F() +function RADIO:Broadcast(viatrigger) + self:F({viatrigger=viatrigger}) - -- If the POSITIONABLE is actually a UNIT or a GROUP, use the more complicated DCS command system - if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then - self:T2("Broadcasting from a UNIT or a GROUP") - self.Positionable:SetCommand({ + -- If the POSITIONABLE is actually a UNIT or a GROUP, use the more complicated DCS command system. + if (self.Positionable.ClassName=="UNIT" or self.Positionable.ClassName=="GROUP") and (not viatrigger) then + self:T("Broadcasting from a UNIT or a GROUP") + + local commandTransmitMessage={ id = "TransmitMessage", params = { file = self.FileName, duration = self.SubtitleDuration, subtitle = self.Subtitle, loop = self.Loop, - } - }) + }} + + self:T3(commandTransmitMessage) + self.Positionable:SetCommand(commandTransmitMessage) else -- If the POSITIONABLE is anything else, we revert to the general singleton function -- I need to give it a unique name, so that the transmission can be stopped later. I use the class ID - self:T2("Broadcasting from a POSITIONABLE") + self:T("Broadcasting from a POSITIONABLE") trigger.action.radioTransmission(self.FileName, self.Positionable:GetPositionVec3(), self.Modulation, self.Loop, self.Frequency, self.Power, tostring(self.ID)) end + return self end + + --- Stops a transmission -- This function is especially usefull to stop the broadcast of looped transmissions -- @param #RADIO self @@ -335,10 +385,10 @@ function RADIO:StopBroadcast() self:F() -- If the POSITIONABLE is a UNIT or a GROUP, stop the transmission with the DCS "StopTransmission" command if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then - self.Positionable:SetCommand({ - id = "StopTransmission", - params = {} - }) + + local commandStopTransmission={id="StopTransmission", params={}} + + self.Positionable:SetCommand(commandStopTransmission) else -- Else, we use the appropriate singleton funciton trigger.action.stopRadioTransmission(tostring(self.ID)) @@ -364,22 +414,86 @@ end -- Use @{#BEACON:StopRadioBeacon}() to stop it. -- -- @type BEACON +-- @field #string ClassName Name of the class "BEACON". +-- @field Wrapper.Controllable#CONTROLLABLE Positionable The @{#CONTROLLABLE} that will receive radio capabilities. -- @extends Core.Base#BASE BEACON = { ClassName = "BEACON", + Positionable = nil, } ---- Create a new BEACON Object. This doesn't activate the beacon, though, use @{#BEACON.AATACAN} or @{#BEACON.Generic} +--- Beacon types supported by DCS. +-- @type BEACON.Type +-- @field #number NULL +-- @field #number VOR +-- @field #number DME +-- @field #number VOR_DME +-- @field #number TACAN +-- @field #number VORTAC +-- @field #number RSBN +-- @field #number BROADCAST_STATION +-- @field #number HOMER +-- @field #number AIRPORT_HOMER +-- @field #number AIRPORT_HOMER_WITH_MARKER +-- @field #number ILS_FAR_HOMER +-- @field #number ILS_NEAR_HOMER +-- @field #number ILS_LOCALIZER +-- @field #number ILS_GLIDESLOPE +-- @field #number NAUTICAL_HOMER +-- @field #number ICLS +BEACON.Type={ + NULL = 0, + VOR = 1, + DME = 2, + VOR_DME = 3, + TACAN = 4, + VORTAC = 5, + RSBN = 32, + BROADCAST_STATION = 1024, + HOMER = 8, + AIRPORT_HOMER = 4104, + AIRPORT_HOMER_WITH_MARKER = 4136, + ILS_FAR_HOMER = 16408, + ILS_NEAR_HOMER = 16456, + ILS_LOCALIZER = 16640, + ILS_GLIDESLOPE = 16896, + NAUTICAL_HOMER = 32776, + ICLS = 131584, +} + +--- Beacon systems supported by DCS. https://wiki.hoggitworld.com/view/DCS_command_activateBeacon +-- @type BEACON.System +-- @field #number PAR_10 +-- @field #number RSBN_5 +-- @field #number TACAN +-- @field #number TACAN_TANKER +-- @field #number ILS_LOCALIZER (This is the one to be used for AA TACAN Tanker!) +-- @field #number ILS_GLIDESLOPE +-- @field #number BROADCAST_STATION +BEACON.System={ + PAR_10 = 1, + RSBN_5 = 2, + TACAN = 3, + TACAN_TANKER = 4, + ILS_LOCALIZER = 5, + ILS_GLIDESLOPE = 6, + BROADCAST_STATION = 7, +} + +--- Create a new BEACON Object. This doesn't activate the beacon, though, use @{#BEACON.ActivateTACAN} etc. -- If you want to create a BEACON, you probably should use @{Wrapper.Positionable#POSITIONABLE.GetBeacon}() instead. -- @param #BEACON self -- @param Wrapper.Positionable#POSITIONABLE Positionable The @{Positionable} that will receive radio capabilities. --- @return #BEACON Beacon --- @return #nil If Positionable is invalid +-- @return #BEACON Beacon object or #nil if the positionable is invalid. function BEACON:New(Positionable) - local self = BASE:Inherit(self, BASE:New()) + + -- Inherit BASE. + local self=BASE:Inherit(self, BASE:New()) --#BEACON + -- Debug. self:F(Positionable) + -- Set positionable. if Positionable:GetPointVec2() then -- It's stupid, but the only way I found to make sure positionable is valid self.Positionable = Positionable return self @@ -390,44 +504,95 @@ function BEACON:New(Positionable) end ---- Converts a TACAN Channel/Mode couple into a frequency in Hz +--- Activates a TACAN BEACON. -- @param #BEACON self --- @param #number TACANChannel --- @param #string TACANMode --- @return #number Frequecy --- @return #nil if parameters are invalid -function BEACON:_TACANToFrequency(TACANChannel, TACANMode) - self:F3({TACANChannel, TACANMode}) - - if type(TACANChannel) ~= "number" then - if TACANMode ~= "X" and TACANMode ~= "Y" then - return nil -- error in arguments - end +-- @param #number Channel TACAN channel, i.e. the "10" part in "10Y". +-- @param #string Mode TACAN mode, i.e. the "Y" part in "10Y". +-- @param #string Message The Message that is going to be coded in Morse and broadcasted by the beacon. +-- @param #boolean Bearing If true, beacon provides bearing information. If false (or nil), only distance information is available. +-- @param #number Duration How long will the beacon last in seconds. Omit for forever. +-- @return #BEACON self +-- @usage +-- -- Let's create a TACAN Beacon for a tanker +-- local myUnit = UNIT:FindByName("MyUnit") +-- local myBeacon = myUnit:GetBeacon() -- Creates the beacon +-- +-- myBeacon:TACAN(20, "Y", "TEXACO", true) -- Activate the beacon +function BEACON:ActivateTACAN(Channel, Mode, Message, Bearing, Duration) + self:T({channel=Channel, mode=Mode, callsign=Message, bearing=Bearing, duration=Duration}) + + -- Get frequency. + local Frequency=UTILS.TACANToFrequency(Channel, Mode) + + -- Check. + if not Frequency then + self:E({"The passed TACAN channel is invalid, the BEACON is not emitting"}) + return self end --- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137. --- I have no idea what it does but it seems to work - local A = 1151 -- 'X', channel >= 64 - local B = 64 -- channel >= 64 + -- Beacon type. + local Type=BEACON.Type.TACAN - if TACANChannel < 64 then - B = 1 - end + -- Beacon system. + local System=BEACON.System.TACAN - if TACANMode == 'Y' then - A = 1025 - if TACANChannel < 64 then - A = 1088 - end - else -- 'X' - if TACANChannel < 64 then - A = 962 + -- Check if unit is an aircraft and set system accordingly. + local AA=self.Positionable:IsAir() + if AA then + System=5 --NOTE: 5 is how you cat the correct tanker behaviour! --BEACON.System.TACAN_TANKER + -- Check if "Y" mode is selected for aircraft. + if Mode~="Y" then + self:E({"WARNING: The POSITIONABLE you want to attach the AA Tacan Beacon is an aircraft: Mode should Y !The BEACON is not emitting.", self.Positionable}) end end - return (A + TACANChannel - B) * 1000000 + -- Attached unit. + local UnitID=self.Positionable:GetID() + + -- Debug. + self:T({"TACAN BEACON started!"}) + + -- Start beacon. + self.Positionable:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, Mode, AA, Message, Bearing) + + -- Stop sheduler. + if Duration then + self.Positionable:DeactivateBeacon(Duration) + end + + return self end +--- Activates an ICLS BEACON. The unit the BEACON is attached to should be an aircraft carrier supporting this system. +-- @param #BEACON self +-- @param #number Channel ICLS channel. +-- @param #string Callsign The Message that is going to be coded in Morse and broadcasted by the beacon. +-- @param #number Duration How long will the beacon last in seconds. Omit for forever. +-- @return #BEACON self +function BEACON:ActivateICLS(Channel, Callsign, Duration) + self:F({Channel=Channel, Callsign=Callsign, Duration=Duration}) + + -- Attached unit. + local UnitID=self.Positionable:GetID() + + -- Debug + self:T2({"ICLS BEACON started!"}) + + -- Start beacon. + self.Positionable:CommandActivateICLS(Channel, UnitID, Callsign) + + -- Stop sheduler + if Duration then -- Schedule the stop of the BEACON if asked by the MD + self.Positionable:DeactivateBeacon(Duration) + end + + return self +end + + + + + --- Activates a TACAN BEACON on an Aircraft. -- @param #BEACON self @@ -480,7 +645,7 @@ function BEACON:AATACAN(TACANChannel, Message, Bearing, BeaconDuration) }) if BeaconDuration then -- Schedule the stop of the BEACON if asked by the MD - SCHEDULER:New( nil, + SCHEDULER:New(nil, function() self:StopAATACAN() end, {}, BeaconDuration) @@ -591,4 +756,44 @@ function BEACON:StopRadioBeacon() self:F() -- The unique name of the transmission is the class ID trigger.action.stopRadioTransmission(tostring(self.ID)) -end \ No newline at end of file + return self +end + +--- Converts a TACAN Channel/Mode couple into a frequency in Hz +-- @param #BEACON self +-- @param #number TACANChannel +-- @param #string TACANMode +-- @return #number Frequecy +-- @return #nil if parameters are invalid +function BEACON:_TACANToFrequency(TACANChannel, TACANMode) + self:F3({TACANChannel, TACANMode}) + + if type(TACANChannel) ~= "number" then + if TACANMode ~= "X" and TACANMode ~= "Y" then + return nil -- error in arguments + end + end + +-- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137. +-- I have no idea what it does but it seems to work + local A = 1151 -- 'X', channel >= 64 + local B = 64 -- channel >= 64 + + if TACANChannel < 64 then + B = 1 + end + + if TACANMode == 'Y' then + A = 1025 + if TACANChannel < 64 then + A = 1088 + end + else -- 'X' + if TACANChannel < 64 then + A = 962 + end + end + + return (A + TACANChannel - B) * 1000000 +end + diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index 0864fb6c1..e4e2c3edf 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -409,9 +409,9 @@ do -- SET_BASE for ObjectID, ObjectData in pairs( self.Set ) do if NearestObject == nil then NearestObject = ObjectData - ClosestDistance = PointVec2:DistanceFromPointVec2( ObjectData:GetVec2() ) + ClosestDistance = PointVec2:DistanceFromPointVec2( ObjectData:GetCoordinate() ) else - local Distance = PointVec2:DistanceFromPointVec2( ObjectData:GetVec2() ) + local Distance = PointVec2:DistanceFromPointVec2( ObjectData:GetCoordinate() ) if Distance < ClosestDistance then NearestObject = ObjectData ClosestDistance = Distance @@ -2033,6 +2033,54 @@ do -- SET_UNIT return self end + + --- Get the SET of the SET_UNIT **sorted per Threat Level**. + -- + -- @param #SET_UNIT self + -- @param #number FromThreatLevel The TreatLevel to start the evaluation **From** (this must be a value between 0 and 10). + -- @param #number ToThreatLevel The TreatLevel to stop the evaluation **To** (this must be a value between 0 and 10). + -- @return #SET_UNIT self + -- @usage + -- + -- + function SET_UNIT:GetSetPerThreatLevel( FromThreatLevel, ToThreatLevel ) + self:F2( arg ) + + local ThreatLevelSet = {} + + if self:Count() ~= 0 then + for UnitName, UnitObject in pairs( self.Set ) do + local Unit = UnitObject -- Wrapper.Unit#UNIT + + local ThreatLevel = Unit:GetThreatLevel() + ThreatLevelSet[ThreatLevel] = ThreatLevelSet[ThreatLevel] or {} + ThreatLevelSet[ThreatLevel].Set = ThreatLevelSet[ThreatLevel].Set or {} + ThreatLevelSet[ThreatLevel].Set[UnitName] = UnitObject + self:F( { ThreatLevel = ThreatLevel, ThreatLevelSet = ThreatLevelSet[ThreatLevel].Set } ) + end + + + local OrderedPerThreatLevelSet = {} + + local ThreatLevelIncrement = FromThreatLevel <= ToThreatLevel and 1 or -1 + + + for ThreatLevel = FromThreatLevel, ToThreatLevel, ThreatLevelIncrement do + self:F( { ThreatLevel = ThreatLevel } ) + local ThreatLevelItem = ThreatLevelSet[ThreatLevel] + if ThreatLevelItem then + for UnitName, UnitObject in pairs( ThreatLevelItem.Set ) do + table.insert( OrderedPerThreatLevelSet, UnitObject ) + end + end + end + + return OrderedPerThreatLevelSet + end + + end + + --- Iterate the SET_UNIT **sorted *per Threat Level** and call an interator function for each **alive** UNIT, providing the UNIT and optional parameters. -- -- @param #SET_UNIT self diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 466e6a70f..d65aae7a2 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -8,7 +8,7 @@ -- * Schedule spawning of new groups. -- * Put limits on the amount of groups that can be spawned, and the amount of units that can be alive at the same time. -- * Randomize the spawning location between different zones. --- * Randomize the intial positions within the zones. +-- * Randomize the initial positions within the zones. -- * Spawn in array formation. -- * Spawn uncontrolled (for planes or helos only). -- * Clean up inactive helicopters that "crashed". @@ -322,6 +322,10 @@ function SPAWN:New( SpawnTemplatePrefix ) self.Grouping = nil -- No grouping. self.SpawnInitLivery = nil -- No special livery. self.SpawnInitSkill = nil -- No special skill. + self.SpawnInitFreq = nil -- No special frequency. + self.SpawnInitModu = nil -- No special modulation. + self.SpawnInitRadio = nil -- No radio comms setting. + self.SpawnInitModex = nil self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. else @@ -370,7 +374,11 @@ function SPAWN:NewWithAlias( SpawnTemplatePrefix, SpawnAliasPrefix ) self.Grouping = nil -- No grouping. self.SpawnInitLivery = nil -- No special livery. self.SpawnInitSkill = nil -- No special skill. - + self.SpawnInitFreq = nil -- No special frequency. + self.SpawnInitModu = nil -- No special modulation. + self.SpawnInitRadio = nil -- No radio comms setting. + self.SpawnInitModex = nil + self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. else error( "SPAWN:New: There is no group declared in the mission editor with SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) @@ -421,7 +429,11 @@ function SPAWN:NewFromTemplate( SpawnTemplate, SpawnTemplatePrefix, SpawnAliasPr self.Grouping = nil -- No grouping. self.SpawnInitLivery = nil -- No special livery. self.SpawnInitSkill = nil -- No special skill. - + self.SpawnInitFreq = nil -- No special frequency. + self.SpawnInitModu = nil -- No special modulation. + self.SpawnInitRadio = nil -- No radio comms setting. + self.SpawnInitModex = nil + self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. else error( "There is no template provided for SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) @@ -597,6 +609,55 @@ function SPAWN:InitSkill( Skill ) return self end +--- Sets the radio comms on or off. Same as checking/unchecking the COMM box in the mission editor. +-- @param #SPAWN self +-- @param #number switch If true (or nil), enables the radio comms. If false, disables the radio for the spawned group. +-- @return #SPAWN self +function SPAWN:InitRadioCommsOnOff(switch) + self:F({switch=switch} ) + self.SpawnInitRadio=switch or true + return self +end + +--- Sets the radio frequency of the group. +-- @param #SPAWN self +-- @param #number frequency The frequency in MHz. +-- @return #SPAWN self +function SPAWN:InitRadioFrequency(frequency) + self:F({frequency=frequency} ) + + self.SpawnInitFreq=frequency + + return self +end + +--- Set radio modulation. Default is AM. +-- @param #SPAWN self +-- @param #string modulation Either "FM" or "AM". If no value is given, modulation is set to AM. +-- @return #SPAWN self +function SPAWN:InitRadioModulation(modulation) + self:F({modulation=modulation}) + if modulation and modulation:lower()=="fm" then + self.SpawnInitModu=radio.modulation.FM + else + self.SpawnInitModu=radio.modulation.AM + end + return self +end + +--- Sets the modex of the first unit of the group. If more units are in the group, the number is increased by one with every unit. +-- @param #SPAWN self +-- @param #number modex Modex of the first unit. +-- @return #SPAWN self +function SPAWN:InitModex(modex) + + if modex then + self.SpawnInitModex=tonumber(modex) + end + + return self +end + --- Randomizes the defined route of the SpawnTemplatePrefix group in the ME. This is very useful to define extra variation of the behaviour of groups. -- @param #SPAWN self @@ -1173,6 +1234,28 @@ function SPAWN:SpawnWithIndex( SpawnIndex ) SpawnTemplate.units[UnitID].skill = self.SpawnInitSkill end end + + -- Set tail number. + if self.SpawnInitModex then + for UnitID = 1, #SpawnTemplate.units do + SpawnTemplate.units[UnitID].onboard_num = string.format("%03d", self.SpawnInitModex+(UnitID-1)) + end + end + + -- Set radio comms on/off. + if self.SpawnInitRadio then + SpawnTemplate.communication=self.SpawnInitRadio + end + + -- Set radio frequency. + if self.SpawnInitFreq then + SpawnTemplate.frequency=self.SpawnInitFreq + end + + -- Set radio modulation. + if self.SpawnInitModu then + SpawnTemplate.modulation=self.SpawnInitModu + end -- Set country, coaliton and categroy. SpawnTemplate.CategoryID = self.SpawnInitCategory or SpawnTemplate.CategoryID @@ -2611,7 +2694,10 @@ function SPAWN:_OnLand( EventData ) if self.RepeatOnLanding then local SpawnGroupIndex = self:GetSpawnIndexFromGroup( SpawnGroup ) self:T( { "Landed:", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } ) - self:ReSpawn( SpawnGroupIndex ) + --self:ReSpawn( SpawnGroupIndex ) + -- Delay respawn by three seconds due to DCS 2.5.4.26368 OB bug https://github.com/FlightControl-Master/MOOSE/issues/1076 + -- Bug was initially only for engine shutdown event but after ED "fixed" it, it now happens on landing events. + SCHEDULER:New(nil, self.ReSpawn, {self, SpawnGroupIndex}, 3) end end end @@ -2637,7 +2723,9 @@ function SPAWN:_OnEngineShutDown( EventData ) if Landed and self.RepeatOnEngineShutDown then local SpawnGroupIndex = self:GetSpawnIndexFromGroup( SpawnGroup ) self:T( { "EngineShutDown: ", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } ) - self:ReSpawn( SpawnGroupIndex ) + --self:ReSpawn( SpawnGroupIndex ) + -- Delay respawn by three seconds due to DCS 2.5.4 OB bug https://github.com/FlightControl-Master/MOOSE/issues/1076 + SCHEDULER:New(nil, self.ReSpawn, {self, SpawnGroupIndex}, 3) end end end diff --git a/Moose Development/Moose/Core/SpawnStatic.lua b/Moose Development/Moose/Core/SpawnStatic.lua index 0081195a5..e5a7b4e59 100644 --- a/Moose Development/Moose/Core/SpawnStatic.lua +++ b/Moose Development/Moose/Core/SpawnStatic.lua @@ -195,6 +195,49 @@ function SPAWNSTATIC:SpawnFromPointVec2( PointVec2, Heading, NewName ) --R2.1 end +--- Creates a new @{Static} from a COORDINATE. +-- @param #SPAWNSTATIC self +-- @param Core.Point#COORDINATE Coordinate The 3D coordinate where to spawn the static. +-- @param #number Heading (Optional) Heading The heading of the static, which is a number in degrees from 0 to 360. Default is 0 degrees. +-- @param #string NewName (Optional) The name of the new static. +-- @return #SPAWNSTATIC +function SPAWNSTATIC:SpawnFromCoordinate(Coordinate, Heading, NewName) --R2.4 + self:F( { PointVec2, Heading, NewName } ) + + local StaticTemplate, CoalitionID, CategoryID, CountryID = _DATABASE:GetStaticGroupTemplate( self.SpawnTemplatePrefix ) + + if StaticTemplate then + + Heading=Heading or 0 + + local StaticUnitTemplate = StaticTemplate.units[1] + + StaticUnitTemplate.x = Coordinate.x + StaticUnitTemplate.y = Coordinate.z + StaticUnitTemplate.alt = Coordinate.y + + StaticTemplate.route = nil + StaticTemplate.groupId = nil + + StaticTemplate.name = NewName or string.format("%s#%05d", self.SpawnTemplatePrefix, self.SpawnIndex ) + StaticUnitTemplate.name = StaticTemplate.name + StaticUnitTemplate.heading = ( Heading / 180 ) * math.pi + + _DATABASE:_RegisterStaticTemplate( StaticTemplate, CoalitionID, CategoryID, CountryID) + + self:F({StaticTemplate = StaticTemplate}) + + local Static = coalition.addStaticObject( self.CountryID or CountryID, StaticTemplate.units[1] ) + + self.SpawnIndex = self.SpawnIndex + 1 + + return _DATABASE:FindStatic(Static:getName()) + end + + return nil +end + + --- Respawns the original @{Static}. -- @param #SPAWNSTATIC self -- @return #SPAWNSTATIC diff --git a/Moose Development/Moose/Core/UserFlag.lua b/Moose Development/Moose/Core/UserFlag.lua index 88c1d0f60..bef5cefff 100644 --- a/Moose Development/Moose/Core/UserFlag.lua +++ b/Moose Development/Moose/Core/UserFlag.lua @@ -70,7 +70,7 @@ do -- UserFlag -- local BlueVictory = USERFLAG:New( "VictoryBlue" ) -- local BlueVictoryValue = BlueVictory:Get() -- Get the UserFlag VictoryBlue value. -- - function USERFLAG:Get( Number ) --R2.3 + function USERFLAG:Get() --R2.3 return trigger.misc.getUserFlag( self.UserFlagName ) end diff --git a/Moose Development/Moose/Core/UserSound.lua b/Moose Development/Moose/Core/UserSound.lua index a0547a5cf..b0f6fb393 100644 --- a/Moose Development/Moose/Core/UserSound.lua +++ b/Moose Development/Moose/Core/UserSound.lua @@ -118,15 +118,21 @@ do -- UserSound --- Play the usersound to the given @{Wrapper.Group}. -- @param #USERSOUND self -- @param Wrapper.Group#GROUP Group The @{Wrapper.Group} to play the usersound to. + -- @param #number Delay (Optional) Delay in seconds, before the sound is played. Default 0. -- @return #USERSOUND The usersound instance. -- @usage -- local BlueVictory = USERSOUND:New( "BlueVictory.ogg" ) -- local PlayerGroup = GROUP:FindByName( "PlayerGroup" ) -- Search for the active group named "PlayerGroup", that contains a human player. -- BlueVictory:ToGroup( PlayerGroup ) -- Play the sound that Blue has won to the player group. -- - function USERSOUND:ToGroup( Group ) --R2.3 - - trigger.action.outSoundForGroup( Group:GetID(), self.UserSoundFileName ) + function USERSOUND:ToGroup( Group, Delay ) --R2.3 + + Delay=Delay or 0 + if Delay>0 then + SCHEDULER:New(nil, USERSOUND.ToGroup,{self, Group}, Delay) + else + trigger.action.outSoundForGroup( Group:GetID(), self.UserSoundFileName ) + end return self end diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 244d5de66..2c00f48f5 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -535,7 +535,7 @@ function ZONE_RADIUS:FlareZone( FlareColor, Points, Azimuth, AddHeight ) local Vec2 = self:GetVec2() AddHeight = AddHeight or 0 - + Points = Points and Points or 360 local Angle @@ -618,6 +618,9 @@ function ZONE_RADIUS:GetVec3( Height ) end + + + --- Scan the zone for the presence of units of the given ObjectCategories. -- Note that after a zone has been scanned, the zone can be evaluated by: -- @@ -629,11 +632,11 @@ end -- @{#ZONE_RADIUS. -- @param #ZONE_RADIUS self -- @param ObjectCategories --- @param Coalition +-- @param UnitCategories -- @usage -- self.Zone:Scan() -- local IsAttacked = self.Zone:IsSomeInZoneOfCoalition( self.Coalition ) -function ZONE_RADIUS:Scan( ObjectCategories ) +function ZONE_RADIUS:Scan( ObjectCategories, UnitCategories ) self.ScanData = {} self.ScanData.Coalitions = {} @@ -660,9 +663,24 @@ function ZONE_RADIUS:Scan( ObjectCategories ) if ( ObjectCategory == Object.Category.UNIT and ZoneObject:isExist() and ZoneObject:isActive() ) or (ObjectCategory == Object.Category.STATIC and ZoneObject:isExist()) then local CoalitionDCSUnit = ZoneObject:getCoalition() - self.ScanData.Coalitions[CoalitionDCSUnit] = true - self.ScanData.Units[ZoneObject] = ZoneObject - self:F2( { Name = ZoneObject:getName(), Coalition = CoalitionDCSUnit } ) + local Include = false + if not UnitCategories then + Include = true + else + local CategoryDCSUnit = ZoneObject:getDesc().category + for UnitCategoryID, UnitCategory in pairs( UnitCategories ) do + if UnitCategory == CategoryDCSUnit then + Include = true + break + end + end + end + if Include then + local CoalitionDCSUnit = ZoneObject:getCoalition() + self.ScanData.Coalitions[CoalitionDCSUnit] = true + self.ScanData.Units[ZoneObject] = ZoneObject + self:F2( { Name = ZoneObject:getName(), Coalition = CoalitionDCSUnit } ) + end end if ObjectCategory == Object.Category.SCENERY then local SceneryType = ZoneObject:getTypeName() @@ -1380,16 +1398,15 @@ end --- Smokes the zone boundaries in a color. -- @param #ZONE_POLYGON_BASE self -- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. +-- @param #number Segments (Optional) Number of segments within boundary line. Default 10. -- @return #ZONE_POLYGON_BASE self -function ZONE_POLYGON_BASE:SmokeZone( SmokeColor ) +function ZONE_POLYGON_BASE:SmokeZone( SmokeColor, Segments ) self:F2( SmokeColor ) - local i - local j - local Segments = 10 + Segments=Segments or 10 - i = 1 - j = #self._.Polygon + local i=1 + local j=#self._.Polygon while i <= #self._.Polygon do self:T( { i, j, self._.Polygon[i], self._.Polygon[j] } ) @@ -1410,6 +1427,42 @@ function ZONE_POLYGON_BASE:SmokeZone( SmokeColor ) end +--- Flare the zone boundaries in a color. +-- @param #ZONE_POLYGON_BASE self +-- @param Utilities.Utils#FLARECOLOR FlareColor The flare color. +-- @param #number Segments (Optional) Number of segments within boundary line. Default 10. +-- @param DCS#Azimuth Azimuth (optional) Azimuth The azimuth of the flare. +-- @param #number AddHeight (optional) The height to be added for the smoke. +-- @return #ZONE_POLYGON_BASE self +function ZONE_POLYGON_BASE:FlareZone( FlareColor, Segments, Azimuth, AddHeight ) + self:F2(FlareColor) + + Segments=Segments or 10 + + AddHeight = AddHeight or 0 + + local i=1 + local j=#self._.Polygon + + while i <= #self._.Polygon do + self:T( { i, j, self._.Polygon[i], self._.Polygon[j] } ) + + local DeltaX = self._.Polygon[j].x - self._.Polygon[i].x + local DeltaY = self._.Polygon[j].y - self._.Polygon[i].y + + for Segment = 0, Segments do -- We divide each line in 5 segments and smoke a point on the line. + local PointX = self._.Polygon[i].x + ( Segment * DeltaX / Segments ) + local PointY = self._.Polygon[i].y + ( Segment * DeltaY / Segments ) + POINT_VEC2:New( PointX, PointY, AddHeight ):Flare(FlareColor, Azimuth) + end + j = i + i = i + 1 + end + + return self +end + + --- Returns if a location is within the zone. diff --git a/Moose Development/Moose/DCS.lua b/Moose Development/Moose/DCS.lua index d4af6e979..0d508793c 100644 --- a/Moose Development/Moose/DCS.lua +++ b/Moose Development/Moose/DCS.lua @@ -430,6 +430,8 @@ do -- Types end -- + + do -- Object --- [DCS Class Object](https://wiki.hoggitworld.com/view/DCS_Class_Object) @@ -527,6 +529,126 @@ do -- CoalitionObject end -- CoalitionObject +do -- Weapon + + --- [DCS Class Weapon](https://wiki.hoggitworld.com/view/DCS_Class_Weapon) + -- @type Weapon + -- @extends #CoalitionObject + -- @field #Weapon.flag flag enum stores weapon flags. Some of them are combination of another flags. + -- @field #Weapon.Category Category enum that stores weapon categories. + -- @field #Weapon.GuidanceType GuidanceType enum that stores guidance methods. Available only for guided weapon (Weapon.Category.MISSILE and some Weapon.Category.BOMB). + -- @field #Weapon.MissileCategory MissileCategory enum that stores missile category. Available only for missiles (Weapon.Category.MISSILE). + -- @field #Weapon.WarheadType WarheadType enum that stores warhead types. + -- @field #Weapon.Desc Desc The descriptor of a weapon. + + --- enum stores weapon flags. Some of them are combination of another flags. + -- @type Weapon.flag + -- @field LGB + -- @field TvGB + -- @field SNSGB + -- @field HEBomb + -- @field Penetrator + -- @field NapalmBomb + -- @field FAEBomb + -- @field ClusterBomb + -- @field Dispencer + -- @field CandleBomb + -- @field ParachuteBomb + -- @field GuidedBomb = LGB + TvGB + SNSGB + -- @field AnyUnguidedBomb = HEBomb + Penetrator + NapalmBomb + FAEBomb + ClusterBomb + Dispencer + CandleBomb + ParachuteBomb + -- @field AnyBomb = GuidedBomb + AnyUnguidedBomb + -- @field LightRocket + -- @field MarkerRocket + -- @field CandleRocket + -- @field HeavyRocket + -- @field AnyRocket = LightRocket + HeavyRocket + MarkerRocket + CandleRocket + -- @field AntiRadarMissile + -- @field AntiShipMissile + -- @field AntiTankMissile + -- @field FireAndForgetASM + -- @field LaserASM + -- @field TeleASM + -- @field CruiseMissile + -- @field GuidedASM = LaserASM + TeleASM + -- @field TacticASM = GuidedASM + FireAndForgetASM + -- @field AnyASM = AntiRadarMissile + AntiShipMissile + AntiTankMissile + FireAndForgetASM + GuidedASM + CruiseMissile + -- @field SRAAM + -- @field MRAAM + -- @field LRAAM + -- @field IR_AAM + -- @field SAR_AAM + -- @field AR_AAM + -- @field AnyAAM = IR_AAM + SAR_AAM + AR_AAM + SRAAM + MRAAM + LRAAM + -- @field AnyMissile = AnyASM + AnyAAM + -- @field AnyAutonomousMissile = IR_AAM + AntiRadarMissile + AntiShipMissile + FireAndForgetASM + CruiseMissile + -- @field GUN_POD + -- @field BuiltInCannon + -- @field Cannons = GUN_POD + BuiltInCannon + -- @field AnyAGWeapon = BuiltInCannon + GUN_POD + AnyBomb + AnyRocket + AnyASM + -- @field AnyAAWeapon = BuiltInCannon + GUN_POD + AnyAAM + -- @field UnguidedWeapon = Cannons + BuiltInCannon + GUN_POD + AnyUnguidedBomb + AnyRocket + -- @field GuidedWeapon = GuidedBomb + AnyASM + AnyAAM + -- @field AnyWeapon = AnyBomb + AnyRocket + AnyMissile + Cannons + -- @field MarkerWeapon = MarkerRocket + CandleRocket + CandleBomb + -- @field ArmWeapon = AnyWeapon - MarkerWeapon + + --- Weapon.Category enum that stores weapon categories. + -- @type Weapon.Category + -- @field SHELL + -- @field MISSILE + -- @field ROCKET + -- @field BOMB + + + --- Weapon.GuidanceType enum that stores guidance methods. Available only for guided weapon (Weapon.Category.MISSILE and some Weapon.Category.BOMB). + -- @type Weapon.GuidanceType + -- @field INS + -- @field IR + -- @field RADAR_ACTIVE + -- @field RADAR_SEMI_ACTIVE + -- @field RADAR_PASSIVE + -- @field TV + -- @field LASER + -- @field TELE + + + --- Weapon.MissileCategory enum that stores missile category. Available only for missiles (Weapon.Category.MISSILE). + -- @type Weapon.MissileCategory + -- @field AAM + -- @field SAM + -- @field BM + -- @field ANTI_SHIP + -- @field CRUISE + -- @field OTHER + + --- Weapon.WarheadType enum that stores warhead types. + -- @type Weapon.WarheadType + -- @field AP + -- @field HE + -- @field SHAPED_EXPLOSIVE + + --- Returns the unit that launched the weapon. + -- @function [parent=#Weapon] getLauncher + -- @param #Weapon self + -- @return #Unit + + --- returns target of the guided weapon. Unguided weapons and guided weapon that is targeted at the point on the ground will return nil. + -- @function [parent=#Weapon] getTarget + -- @param #Weapon self + -- @return #Object + + --- returns weapon descriptor. Descriptor type depends on weapon category. + -- @function [parent=#Weapon] getDesc + -- @param #Weapon self + -- @return #Weapon.Desc + + + + Weapon = {} --#Weapon + +end -- Weapon + + do -- Airbase --- [DCS Class Airbase](https://wiki.hoggitworld.com/view/DCS_Class_Airbase) @@ -1196,7 +1318,4 @@ do -- AI AI = {} --#AI -end -- AI - - - +end -- AI \ No newline at end of file diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index 8509928fb..637560d03 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -216,7 +216,7 @@ -- One way to determin which types of ammo the unit carries, one can use the debug mode of the arty class via @{#ARTY.SetDebugON}(). -- In debug mode, the all ammo types of the group are printed to the monitor as message and can be found in the DCS.log file. -- --- ## Empoying Selected Weapons +-- ## Employing Selected Weapons -- -- If an ARTY group carries multiple weapons, which can be used for artillery task, a certain weapon type can be selected to attack the target. -- This is done via the *weapontype* parameter of the @{#ARTY.AssignTargetCoord}(..., *weapontype*, ...) function. @@ -674,11 +674,13 @@ ARTY.id="ARTY | " --- Arty script version. -- @field #string version -ARTY.version="1.0.6" +ARTY.version="1.0.7" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list: +-- TODO: Add hit event and make the arty group relocate. +-- TODO: Handle rearming for ships. How? -- DONE: Delete targets from queue user function. -- DONE: Delete entire target queue user function. -- DONE: Add weapon types. Done but needs improvements. @@ -697,11 +699,9 @@ ARTY.version="1.0.6" -- DONE: Add command move to make arty group move. -- DONE: remove schedulers for status event. -- DONE: Improve handling of special weapons. When winchester if using selected weapons? --- TODO: Handle rearming for ships. How? -- DONE: Make coordinate after rearming general, i.e. also work after the group has moved to anonther location. -- DONE: Add set commands via markers. E.g. set rearming place. -- DONE: Test stationary types like mortas ==> rearming etc. --- TODO: Add hit event and make the arty group relocate. -- DONE: Add illumination and smoke. --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2878,7 +2878,7 @@ function ARTY:onafterCeaseFire(Controllable, From, Event, To, target) self.Controllable:ClearTasks() else - self:E(ARTY.id.."ERROR: No target in cease fire for group %s.", self.groupname) + self:E(ARTY.id..string.format("ERROR: No target in cease fire for group %s.", self.groupname)) end -- Set number of shots to zero. @@ -4253,101 +4253,116 @@ end -- @param #ARTY self function ARTY:_CheckTargetsInRange() + local targets2delete={} + for i=1,#self.targets do local _target=self.targets[i] self:T3(ARTY.id..string.format("Before: Target %s - in range = %s", _target.name, tostring(_target.inrange))) -- Check if target is in range. - local _inrange,_toofar,_tooclose=self:_TargetInRange(_target) + local _inrange,_toofar,_tooclose,_remove=self:_TargetInRange(_target) self:T3(ARTY.id..string.format("Inbetw: Target %s - in range = %s, toofar = %s, tooclose = %s", _target.name, tostring(_target.inrange), tostring(_toofar), tostring(_tooclose))) - -- Init default for assigning moves into range. - local _movetowards=false - local _moveaway=false + if _remove then - if _target.inrange==nil then - - -- First time the check is performed. We call the function again and send a message. - _target.inrange,_toofar,_tooclose=self:_TargetInRange(_target, self.report or self.Debug) + -- The ARTY group is immobile and not cargo but the target is not in range! + table.insert(targets2delete, _target.name) - -- Send group towards/away from target. - if _toofar then - _movetowards=true - elseif _tooclose then - _moveaway=true - end + else - elseif _target.inrange==true then - - -- Target was in range at previous check... - - if _toofar then --...but is now too far away. - _movetowards=true - elseif _tooclose then --...but is now too close. - _moveaway=true - end - - elseif _target.inrange==false then - - -- Target was out of range at previous check. + -- Init default for assigning moves into range. + local _movetowards=false + local _moveaway=false - if _inrange then - -- Inform coalition that target is now in range. - local text=string.format("%s, target %s is now in range.", self.alias, _target.name) - self:T(ARTY.id..text) - MESSAGE:New(text,10):ToCoalitionIf(self.Controllable:GetCoalition(), self.report or self.Debug) - end - - end - - -- Assign a relocation command so that the unit will be in range of the requested target. - if self.autorelocate and (_movetowards or _moveaway) then - - -- Get current position. - local _from=self.Controllable:GetCoordinate() - local _dist=_from:Get2DDistance(_target.coord) + if _target.inrange==nil then - if _dist<=self.autorelocatemaxdist then - - local _tocoord --Core.Point#COORDINATE - local _name="" - local _safetymargin=500 - - if _movetowards then + -- First time the check is performed. We call the function again and send a message. + _target.inrange,_toofar,_tooclose=self:_TargetInRange(_target, self.report or self.Debug) - -- Target was in range on previous check but now we are too far away. - local _waytogo=_dist-self.maxrange+_safetymargin - local _heading=self:_GetHeading(_from,_target.coord) - _tocoord=_from:Translate(_waytogo, _heading) - _name=string.format("%s, relocation to within max firing range of target %s", self.alias, _target.name) - - elseif _moveaway then - - -- Target was in range on previous check but now we are too far away. - local _waytogo=_dist-self.minrange+_safetymargin - local _heading=self:_GetHeading(_target.coord,_from) - _tocoord=_from:Translate(_waytogo, _heading) - _name=string.format("%s, relocation to within min firing range of target %s", self.alias, _target.name) - + -- Send group towards/away from target. + if _toofar then + _movetowards=true + elseif _tooclose then + _moveaway=true end - - -- Send info message. - MESSAGE:New(_name.." assigned.", 10):ToCoalitionIf(self.Controllable:GetCoalition(), self.report or self.Debug) - - -- Assign relocation move. - self:AssignMoveCoord(_tocoord, nil, nil, self.autorelocateonroad, false, _name, true) + + elseif _target.inrange==true then + + -- Target was in range at previous check... + + if _toofar then --...but is now too far away. + _movetowards=true + elseif _tooclose then --...but is now too close. + _moveaway=true + end + + elseif _target.inrange==false then + + -- Target was out of range at previous check. + if _inrange then + -- Inform coalition that target is now in range. + local text=string.format("%s, target %s is now in range.", self.alias, _target.name) + self:T(ARTY.id..text) + MESSAGE:New(text,10):ToCoalitionIf(self.Controllable:GetCoalition(), self.report or self.Debug) + end + end + + -- Assign a relocation command so that the unit will be in range of the requested target. + if self.autorelocate and (_movetowards or _moveaway) then + + -- Get current position. + local _from=self.Controllable:GetCoordinate() + local _dist=_from:Get2DDistance(_target.coord) + + if _dist<=self.autorelocatemaxdist then + + local _tocoord --Core.Point#COORDINATE + local _name="" + local _safetymargin=500 + + if _movetowards then + + -- Target was in range on previous check but now we are too far away. + local _waytogo=_dist-self.maxrange+_safetymargin + local _heading=self:_GetHeading(_from,_target.coord) + _tocoord=_from:Translate(_waytogo, _heading) + _name=string.format("%s, relocation to within max firing range of target %s", self.alias, _target.name) + elseif _moveaway then + + -- Target was in range on previous check but now we are too far away. + local _waytogo=_dist-self.minrange+_safetymargin + local _heading=self:_GetHeading(_target.coord,_from) + _tocoord=_from:Translate(_waytogo, _heading) + _name=string.format("%s, relocation to within min firing range of target %s", self.alias, _target.name) + + end + + -- Send info message. + MESSAGE:New(_name.." assigned.", 10):ToCoalitionIf(self.Controllable:GetCoalition(), self.report or self.Debug) + + -- Assign relocation move. + self:AssignMoveCoord(_tocoord, nil, nil, self.autorelocateonroad, false, _name, true) + + end + + end + + -- Update value. + _target.inrange=_inrange + + self:T3(ARTY.id..string.format("After: Target %s - in range = %s", _target.name, tostring(_target.inrange))) end - - -- Update value. - _target.inrange=_inrange - - self:T3(ARTY.id..string.format("After: Target %s - in range = %s", _target.name, tostring(_target.inrange))) - end + + -- Remove targets not in range. + for _,targetname in pairs(targets2delete) do + self:RemoveTarget(targetname) + end + end --- Check all normal (untimed) targets and return the target with the highest priority which has been engaged the fewest times. @@ -4728,6 +4743,7 @@ end -- @return #boolean True if target is in range, false otherwise. -- @return #boolean True if ARTY group is too far away from the target, i.e. distance > max firing range. -- @return #boolean True if ARTY group is too close to the target, i.e. distance < min finring range. +-- @return #boolean True if target should be removed since ARTY group is immobile and not cargo. function ARTY:_TargetInRange(target, message) self:F3(target) @@ -4763,11 +4779,13 @@ function ARTY:_TargetInRange(target, message) end -- Remove target if ARTY group cannot move, e.g. Mortas. No chance to be ever in range - unless they are cargo. + local _remove=false if not (self.ismobile or self.iscargo) and _inrange==false then - self:RemoveTarget(target.name) + --self:RemoveTarget(target.name) + _remove=true end - return _inrange,_toofar,_tooclose + return _inrange,_toofar,_tooclose,_remove end --- Get the weapon type name, which should be used to attack the target. diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index 7402729ab..087a02207 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -1012,7 +1012,7 @@ do -- DETECTION_BASE --- Set the parameters to calculate to optimal intercept point. -- @param #DETECTION_BASE self -- @param #boolean Intercept Intercept is true if an intercept point is calculated. Intercept is false if it is disabled. The default Intercept is false. - -- @param #number IntereptDelay If Intercept is true, then InterceptDelay is the average time it takes to get airplanes airborne. + -- @param #number InterceptDelay If Intercept is true, then InterceptDelay is the average time it takes to get airplanes airborne. -- @return #DETECTION_BASE self function DETECTION_BASE:SetIntercept( Intercept, InterceptDelay ) self:F2() @@ -1233,7 +1233,7 @@ do -- DETECTION_BASE -- @param DCS#Unit.Category Category The category of the unit. -- @return #boolean true if there are friendlies nearby function DETECTION_BASE:IsFriendliesNearBy( DetectedItem, Category ) - --self:F( { "FriendliesNearBy Test", DetectedItem.FriendliesNearBy } ) +-- self:F( { "FriendliesNearBy Test", DetectedItem.FriendliesNearBy } ) return ( DetectedItem.FriendliesNearBy and DetectedItem.FriendliesNearBy[Category] ~= nil ) or false end diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 5b1616c81..dc5563546 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -546,7 +546,7 @@ RAT.id="RAT | " --- RAT version. -- @list version RAT.version={ - version = "2.3.4", + version = "2.3.5", print = true, } @@ -717,6 +717,11 @@ function RAT:Spawn(naircraft) self.FLcruise=005*RAT.unit.FL2m end end + + -- Enable helos to go to destinations 100 meters away. + if self.category==RAT.cat.heli then + self.mindist=50 + end -- Run consistency checks. self:_CheckConsistency() @@ -1812,14 +1817,14 @@ function RAT:ATC_Delay(time) end --- Set minimum distance between departure and destination. Default is 5 km. --- Minimum distance should not be smaller than maybe ~500 meters to ensure that departure and destination are different. +-- Minimum distance should not be smaller than maybe ~100 meters to ensure that departure and destination are different. -- @param #RAT self -- @param #number dist Distance in km. -- @return #RAT RAT self object. function RAT:SetMinDistance(dist) self:F2(dist) -- Distance in meters. Absolute minimum is 500 m. - self.mindist=math.max(500, dist*1000) + self.mindist=math.max(100, dist*1000) return self end @@ -2446,7 +2451,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) local VxCruiseMax if self.Vcruisemax then -- User input. - VxCruiseMax = min(self.Vcruisemax, self.aircraft.Vmax) + VxCruiseMax = math.min(self.Vcruisemax, self.aircraft.Vmax) else -- Max cruise speed 90% of Vmax or 900 km/h whichever is lower. VxCruiseMax = math.min(self.aircraft.Vmax*0.90, 250) @@ -5435,7 +5440,7 @@ function RAT:_ATCInit(airports_map) if not RAT.ATC.init then local text text="Starting RAT ATC.\nSimultanious = "..RAT.ATC.Nclearance.."\n".."Delay = "..RAT.ATC.delay - self:T(RAT.id..text) + BASE:T(RAT.id..text) RAT.ATC.init=true for _,ap in pairs(airports_map) do local name=ap:GetName() @@ -5458,7 +5463,7 @@ end -- @param #string name Group name of the flight. -- @param #string dest Name of the destination airport. function RAT:_ATCAddFlight(name, dest) - self:T(string.format("%sATC %s: Adding flight %s with destination %s.", RAT.id, dest, name, dest)) + BASE:T(string.format("%sATC %s: Adding flight %s with destination %s.", RAT.id, dest, name, dest)) RAT.ATC.flight[name]={} RAT.ATC.flight[name].destination=dest RAT.ATC.flight[name].Tarrive=-1 @@ -5483,7 +5488,7 @@ end -- @param #string name Group name of the flight. -- @param #number time Time the fight first registered. function RAT:_ATCRegisterFlight(name, time) - self:T(RAT.id.."Flight ".. name.." registered at ATC for landing clearance.") + BASE:T(RAT.id.."Flight ".. name.." registered at ATC for landing clearance.") RAT.ATC.flight[name].Tarrive=time RAT.ATC.flight[name].holding=0 end @@ -5514,7 +5519,7 @@ function RAT:_ATCStatus() -- Aircraft is holding. local text=string.format("ATC %s: Flight %s is holding for %i:%02d. %s.", dest, name, hold/60, hold%60, busy) - self:T(RAT.id..text) + BASE:T(RAT.id..text) elseif hold==RAT.ATC.onfinal then @@ -5522,7 +5527,7 @@ function RAT:_ATCStatus() local Tfinal=Tnow-RAT.ATC.flight[name].Tonfinal local text=string.format("ATC %s: Flight %s is on final. Waiting %i:%02d for landing event.", dest, name, Tfinal/60, Tfinal%60) - self:T(RAT.id..text) + BASE:T(RAT.id..text) elseif hold==RAT.ATC.unregistered then @@ -5530,7 +5535,7 @@ function RAT:_ATCStatus() --self:T(string.format("ATC %s: Flight %s is not registered yet (hold %d).", dest, name, hold)) else - self:E(RAT.id.."ERROR: Unknown holding time in RAT:_ATCStatus().") + BASE:E(RAT.id.."ERROR: Unknown holding time in RAT:_ATCStatus().") end end @@ -5572,12 +5577,12 @@ function RAT:_ATCCheck() -- Debug message. local text=string.format("ATC %s: Flight %s runway is busy. You are #%d of %d in landing queue. Your holding time is %i:%02d.", name, flight,qID, nqueue, RAT.ATC.flight[flight].holding/60, RAT.ATC.flight[flight].holding%60) - self:T(RAT.id..text) + BASE:T(RAT.id..text) else local text=string.format("ATC %s: Flight %s was cleared for landing. Your holding time was %i:%02d.", name, flight, RAT.ATC.flight[flight].holding/60, RAT.ATC.flight[flight].holding%60) - self:T(RAT.id..text) + BASE:T(RAT.id..text) -- Clear flight for landing. RAT:_ATCClearForLanding(name, flight) @@ -5705,12 +5710,7 @@ function RAT:_ATCQueue() for k,v in ipairs(_queue) do table.insert(RAT.ATC.airport[airport].queue, v[1]) end - - --fvh - --for k,v in ipairs(RAT.ATC.airport[airport].queue) do - --print(string.format("queue #%02i flight \"%s\" holding %d seconds",k, v, RAT.ATC.flight[v].holding)) - --end - + end end diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 95b800521..a3e73b21a 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -11,7 +11,7 @@ -- -- ## Features: -- --- * Impact points of bombs, rockets and missils are recorded and distance to closest range target is measured and reported to the player. +-- * Impact points of bombs, rockets and missiles are recorded and distance to closest range target is measured and reported to the player. -- * Number of hits on strafing passes are counted and reported. Also the percentage of hits w.r.t fired shots is evaluated. -- * Results of all bombing and strafing runs are stored and top 10 results can be displayed. -- * Range targets can be marked by smoke. @@ -56,9 +56,9 @@ -- @field #table strafeStatus Table containing the current strafing target a player as assigned to. -- @field #table strafePlayerResults Table containing the strafing results of each player. -- @field #table bombPlayerResults Table containing the bombing results of each player. --- @field #table PlayerSettings Indiviual player settings. +-- @field #table PlayerSettings Individual player settings. -- @field #number dtBombtrack Time step [sec] used for tracking released bomb/rocket positions. Default 0.005 seconds. --- @field #number BombtrackThreshold Bombs/rockets/missiles are only tracked if player-range distance is smaller than this threashold [m]. Default 25000 m. +-- @field #number BombtrackThreshold Bombs/rockets/missiles are only tracked if player-range distance is smaller than this threshold [m]. Default 25000 m. -- @field #number Tmsg Time [sec] messages to players are displayed. Default 30 sec. -- @field #string examinergroupname Name of the examiner group which should get all messages. -- @field #boolean examinerexclusive If true, only the examiner gets messages. If false, clients and examiner get messages. @@ -75,10 +75,11 @@ -- @field #boolean trackbombs If true (default), all bomb types are tracked and impact point to closest bombing target is evaluated. -- @field #boolean trackrockets If true (default), all rocket types are tracked and impact point to closest bombing target is evaluated. -- @field #boolean trackmissiles If true (default), all missile types are tracked and impact point to closest bombing target is evaluated. +-- @field #boolean defaultsmokebomb If true, initialize player settings to smoke bomb. -- @extends Core.Base#BASE --- Enables a mission designer to easily set up practice ranges in DCS. A new RANGE object can be created with the @{#RANGE.New}(rangename) contructor. --- The parameter "rangename" defindes the name of the range. It has to be unique since this is also the name displayed in the radio menu. +-- The parameter "rangename" defines the name of the range. It has to be unique since this is also the name displayed in the radio menu. -- -- Generally, a range consists of strafe pits and bombing targets. For strafe pits the number of hits for each pass is counted and tabulated. -- For bombing targets, the distance from the impact point of the bomb, rocket or missile to the closest range target is measured and tabulated. @@ -89,12 +90,12 @@ -- **IMPORTANT** -- -- Due to a DCS bug, it is not possible to directly monitor when a player enters a plane. So in a mission with client slots, it is vital that --- a player first enters as spector and **after that** jumps into the slot of his aircraft! +-- a player first enters as spectator or hits ESC twice and **after that** jumps into the slot of his aircraft! -- If that is not done, the script is not started correctly. This can be checked by looking at the radio menues. If the mission was entered correctly, -- there should be an "On the Range" menu items in the "F10. Other..." menu. -- -- ## Strafe Pits --- Each strafe pit can consist of multiple targets. Often one findes two or three strafe targets next to each other. +-- Each strafe pit can consist of multiple targets. Often one finds two or three strafe targets next to each other. -- -- A strafe pit can be added to the range by the @{#RANGE.AddStrafePit}(*targetnames, boxlength, boxwidth, heading, inverseheading, goodpass, foulline*) function. -- @@ -104,7 +105,7 @@ -- If the parameter *heading* is passed as **nil**, the heading is automatically taken from the heading of the first target unit as defined in the ME. -- The parameter *inverseheading* turns the heading around by 180 degrees. This is sometimes useful, since the default heading of strafe target units point in the -- wrong/opposite direction. --- * The parameter *goodpass* defines the number of hits a pilot has to achive during a run to be judged as a "good" pass. +-- * The parameter *goodpass* defines the number of hits a pilot has to achieve during a run to be judged as a "good" pass. -- * The last parameter *foulline* sets the distance from the pit targets to the foul line. Hit from closer than this line are not counted! -- -- Another function to add a strafe pit is @{#RANGE.AddStrafePitGroup}(*group, boxlength, boxwidth, heading, inverseheading, goodpass, foulline*). Here, @@ -151,7 +152,7 @@ -- * "F2. My Settings": Player specific settings. -- * "F3. Stats" Player: statistics and scores. -- * "Range Information": Information about the range, such as bearing and range. Also range and player specific settings are displayed. --- * "Weather Report": Temperatur, wind and QFE pressure information is provided. +-- * "Weather Report": Temperature, wind and QFE pressure information is provided. -- -- ## Examples -- @@ -243,6 +244,7 @@ RANGE={ trackbombs=true, trackrockets=true, trackmissiles=true, + defaultsmokebomb=true, } --- Default range parameters. @@ -266,19 +268,25 @@ RANGE.Defaults={ -- @field #table Names RANGE.Names={} ---- Main radio menu. --- @field #table MenuF10 +--- Main radio menu on group level. +-- @field #table MenuF10 Root menu table on group level. RANGE.MenuF10={} +--- Main radio menu on mission level. +-- @field #table MenuF10Root Root menu on mission level. +RANGE.MenuF10Root=nil + --- Some ID to identify who we are in output of the DCS.log file. -- @field #string id RANGE.id="RANGE | " --- Range script version. -- @field #string version -RANGE.version="1.2.1" +RANGE.version="1.2.4" --TODO list: +--TODO: Verbosity level for messages. +--TODO: Add option for default settings such as smoke off. --TODO: Add custom weapons, which can be specified by the user. --TODO: Check if units are still alive. --DONE: Add statics for strafe pits. @@ -310,6 +318,9 @@ function RANGE:New(rangename) local text=string.format("RANGE script version %s - creating new RANGE object of name: %s.", RANGE.version, self.rangename) self:E(RANGE.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) + + -- Defaults + self:SetDefaultPlayerSmokeBomb() -- Return object. return self @@ -317,93 +328,102 @@ end --- Initializes number of targets and location of the range. Starts the event handlers. -- @param #RANGE self -function RANGE:Start() +-- @param #number delay Delay in seconds, before the RANGE is started. Default immediately. +-- @return self +function RANGE:Start(delay) self:F() - -- Location/coordinate of range. - local _location=nil + if delay and delay>0 then + SCHEDULER:New(nil, self.Start, {self}, delay) + else - -- Count bomb targets. - local _count=0 - for _,_target in pairs(self.bombingTargets) do - _count=_count+1 + -- Location/coordinate of range. + local _location=nil - -- Get range location. - if _location==nil then - _location=_target.target:GetCoordinate() --Core.Point#COORDINATE - end - end - self.nbombtargets=_count - - -- Count strafing targets. - _count=0 - for _,_target in pairs(self.strafeTargets) do - _count=_count+1 - - for _,_unit in pairs(_target.targets) do + -- Count bomb targets. + local _count=0 + for _,_target in pairs(self.bombingTargets) do + _count=_count+1 + + -- Get range location. if _location==nil then - _location=_unit:GetCoordinate() + _location=_target.target:GetCoordinate() --Core.Point#COORDINATE end end - end - self.nstrafetargets=_count - - -- Location of the range. We simply take the first unit/target we find if it was not explicitly specified by the user. - if self.location==nil then - self.location=_location - end - - if self.location==nil then - local text=string.format("ERROR! No range location found. Number of strafe targets = %d. Number of bomb targets = %d.", self.rangename, self.nstrafetargets, self.nbombtargets) - self:E(RANGE.id..text) - return - end - - -- Define a MOOSE zone of the range. - if self.rangezone==nil then - self.rangezone=ZONE_RADIUS:New(self.rangename, {x=self.location.x, y=self.location.z}, self.rangeradius) - end - - -- Starting range. - local text=string.format("Starting RANGE %s. Number of strafe targets = %d. Number of bomb targets = %d.", self.rangename, self.nstrafetargets, self.nbombtargets) - self:E(RANGE.id..text) - MESSAGE:New(text,10):ToAllIf(self.Debug) - - -- Event handling. - if self.eventmoose then - -- Events are handled my MOOSE. - self:T(RANGE.id.."Events are handled by MOOSE.") - self:HandleEvent(EVENTS.Birth) - self:HandleEvent(EVENTS.Hit) - self:HandleEvent(EVENTS.Shot) - else - -- Events are handled directly by DCS. - self:T(RANGE.id.."Events are handled directly by DCS.") - world.addEventHandler(self) - end - - -- Make bomb target move randomly within the range zone. - for _,_target in pairs(self.bombingTargets) do - - -- Check if it is a static object. - local _static=self:_CheckStatic(_target.target:GetName()) + self.nbombtargets=_count - if _target.move and _static==false and _target.speed>1 then - local unit=_target.target --Wrapper.Unit#UNIT - _target.target:PatrolZones({self.rangezone}, _target.speed*0.75, "Off road") + -- Count strafing targets. + _count=0 + for _,_target in pairs(self.strafeTargets) do + _count=_count+1 + + for _,_unit in pairs(_target.targets) do + if _location==nil then + _location=_unit:GetCoordinate() + end + end + end + self.nstrafetargets=_count + + -- Location of the range. We simply take the first unit/target we find if it was not explicitly specified by the user. + if self.location==nil then + self.location=_location + end + + if self.location==nil then + local text=string.format("ERROR! No range location found. Number of strafe targets = %d. Number of bomb targets = %d.", self.rangename, self.nstrafetargets, self.nbombtargets) + self:E(RANGE.id..text) + return + end + + -- Define a MOOSE zone of the range. + if self.rangezone==nil then + self.rangezone=ZONE_RADIUS:New(self.rangename, {x=self.location.x, y=self.location.z}, self.rangeradius) + end + + -- Starting range. + local text=string.format("Starting RANGE %s. Number of strafe targets = %d. Number of bomb targets = %d.", self.rangename, self.nstrafetargets, self.nbombtargets) + self:I(RANGE.id..text) + MESSAGE:New(text,10):ToAllIf(self.Debug) + + -- Event handling. + if self.eventmoose then + -- Events are handled my MOOSE. + self:T(RANGE.id.."Events are handled by MOOSE.") + self:HandleEvent(EVENTS.Birth) + self:HandleEvent(EVENTS.Hit) + self:HandleEvent(EVENTS.Shot) + else + -- Events are handled directly by DCS. + self:T(RANGE.id.."Events are handled directly by DCS.") + world.addEventHandler(self) + end + + -- Make bomb target move randomly within the range zone. + for _,_target in pairs(self.bombingTargets) do + + -- Check if it is a static object. + local _static=self:_CheckStatic(_target.target:GetName()) + + if _target.move and _static==false and _target.speed>1 then + local unit=_target.target --Wrapper.Unit#UNIT + _target.target:PatrolZones({self.rangezone}, _target.speed*0.75, "Off road") + end + + end + + -- Debug mode: smoke all targets and range zone. + if self.Debug then + self:_MarkTargetsOnMap() + self:_SmokeBombTargets() + self:_SmokeStrafeTargets() + self:_SmokeStrafeTargetBoxes() + self.rangezone:SmokeZone(SMOKECOLOR.White) end end - -- Debug mode: smoke all targets and range zone. - if self.Debug then - self:_MarkTargetsOnMap() - self:_SmokeBombTargets() - self:_SmokeStrafeTargets() - self:_SmokeStrafeTargetBoxes() - self.rangezone:SmokeZone(SMOKECOLOR.White) - end - + return self end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -412,143 +432,199 @@ end --- Set maximal strafing altitude. Player entering a strafe pit above that altitude are not registered for a valid pass. -- @param #RANGE self -- @param #number maxalt Maximum altitude AGL in meters. Default is 914 m= 3000 ft. +-- @return #RANGE self function RANGE:SetMaxStrafeAlt(maxalt) self.strafemaxalt=maxalt or RANGE.Defaults.strafemaxalt + return self end --- Set time interval for tracking bombs. A smaller time step increases accuracy but needs more CPU time. -- @param #RANGE self -- @param #number dt Time interval in seconds. Default is 0.005 s. +-- @return #RANGE self function RANGE:SetBombtrackTimestep(dt) self.dtBombtrack=dt or RANGE.Defaults.dtBombtrack + return self end --- Set time how long (most) messages are displayed. -- @param #RANGE self -- @param #number time Time in seconds. Default is 30 s. +-- @return #RANGE self function RANGE:SetMessageTimeDuration(time) self.Tmsg=time or RANGE.Defaults.Tmsg + return self end --- Set messages to examiner. The examiner will receive messages from all clients. -- @param #RANGE self -- @param #string examinergroupname Name of the group of the examiner. -- @param #boolean exclusively If true, messages are send exclusively to the examiner, i.e. not to the clients. +-- @return #RANGE self function RANGE:SetMessageToExaminer(examinergroupname, exclusively) self.examinergroupname=examinergroupname self.examinerexclusive=exclusively + return self end --- Set max number of player results that are displayed. -- @param #RANGE self -- @param #number nmax Number of results. Default is 10. +-- @return #RANGE self function RANGE:SetDisplayedMaxPlayerResults(nmax) self.ndisplayresult=nmax or RANGE.Defaults.ndisplayresult + return self end --- Set range radius. Defines the area in which e.g. bomb impacts are smoked. -- @param #RANGE self -- @param #number radius Radius in km. Default 5 km. +-- @return #RANGE self function RANGE:SetRangeRadius(radius) self.rangeradius=radius*1000 or RANGE.Defaults.rangeradius + return self +end + +--- Set player setting whether bomb impact points are smoked or not +-- @param #RANGE self +-- @param #boolean If true nor nil default is to smoke impact points of bombs. +-- @return #RANGE self +function RANGE:SetDefaultPlayerSmokeBomb(switch) + if switch==true or switch==nil then + self.defaultsmokebomb=true + else + self.defaultsmokebomb=false + end + return self end --- Set bomb track threshold distance. Bombs/rockets/missiles are only tracked if player-range distance is less than this distance. Default 25 km. -- @param #RANGE self -- @param #number distance Threshold distance in km. Default 25 km. +-- @return #RANGE self function RANGE:SetBombtrackThreshold(distance) self.BombtrackThreshold=distance*1000 or 25*1000 + return self end ---- Set range location. If this is not done, one (random) unit position of the range is used to determine the center of the range. +--- Set range location. If this is not done, one (random) unit position of the range is used to determine the location of the range. +-- The range location determines the position at which the weather data is evaluated. -- @param #RANGE self --- @param Core.Point#COORDINATE coordinate Coordinate of the center of the range. +-- @param Core.Point#COORDINATE coordinate Coordinate of the range. +-- @return #RANGE self function RANGE:SetRangeLocation(coordinate) self.location=coordinate + return self end --- Set range zone. For example, no bomb impact points are smoked if a bomb falls outside of this zone. -- If a zone is not explicitly specified, the range zone is determined by its location and radius. -- @param #RANGE self -- @param Core.Zone#ZONE zone MOOSE zone defining the range perimeters. -function RANGE:SetRangeLocation(zone) +-- @return #RANGE self +function RANGE:SetRangeZone(zone) self.rangezone=zone + return self end --- Set smoke color for marking bomb targets. By default bomb targets are marked by red smoke. -- @param #RANGE self -- @param Utilities.Utils#SMOKECOLOR colorid Color id. Default SMOKECOLOR.Red. +-- @return #RANGE self function RANGE:SetBombTargetSmokeColor(colorid) self.BombSmokeColor=colorid or SMOKECOLOR.Red + return self end --- Set smoke color for marking strafe targets. By default strafe targets are marked by green smoke. -- @param #RANGE self -- @param Utilities.Utils#SMOKECOLOR colorid Color id. Default SMOKECOLOR.Green. +-- @return #RANGE self function RANGE:SetStrafeTargetSmokeColor(colorid) self.StrafeSmokeColor=colorid or SMOKECOLOR.Green + return self end --- Set smoke color for marking strafe pit approach boxes. By default strafe pit boxes are marked by white smoke. -- @param #RANGE self -- @param Utilities.Utils#SMOKECOLOR colorid Color id. Default SMOKECOLOR.White. +-- @return #RANGE self function RANGE:SetStrafePitSmokeColor(colorid) self.StrafePitSmokeColor=colorid or SMOKECOLOR.White + return self end --- Set time delay between bomb impact and starting to smoke the impact point. -- @param #RANGE self -- @param #number delay Time delay in seconds. Default is 3 seconds. +-- @return #RANGE self function RANGE:SetSmokeTimeDelay(delay) self.TdelaySmoke=delay or RANGE.Defaults.TdelaySmoke + return self end --- Enable debug modus. -- @param #RANGE self +-- @return #RANGE self function RANGE:DebugON() self.Debug=true + return self end --- Disable debug modus. -- @param #RANGE self +-- @return #RANGE self function RANGE:DebugOFF() self.Debug=false + return self end --- Enables tracking of all bomb types. Note that this is the default setting. -- @param #RANGE self +-- @return #RANGE self function RANGE:TrackBombsON() self.trackbombs=true + return self end --- Disables tracking of all bomb types. -- @param #RANGE self +-- @return #RANGE self function RANGE:TrackBombsOFF() self.trackbombs=false + return self end --- Enables tracking of all rocket types. Note that this is the default setting. -- @param #RANGE self +-- @return #RANGE self function RANGE:TrackRocketsON() self.trackrockets=true + return self end --- Disables tracking of all rocket types. -- @param #RANGE self +-- @return #RANGE self function RANGE:TrackRocketsOFF() self.trackrockets=false + return self end --- Enables tracking of all missile types. Note that this is the default setting. -- @param #RANGE self +-- @return #RANGE self function RANGE:TrackMissilesON() self.trackmissiles=true + return self end --- Disables tracking of all missile types. -- @param #RANGE self +-- @return #RANGE self function RANGE:TrackMissilesOFF() self.trackmissiles=false + return self end @@ -563,6 +639,7 @@ end -- @param #boolean inverseheading (Optional) Take inverse heading (heading --> heading - 180 Degrees). Default is false. -- @param #number goodpass (Optional) Number of hits for a "good" strafing pass. Default is 20. -- @param #number foulline (Optional) Foul line distance. Hits from closer than this distance are not counted. Default 610 m = 2000 ft. Set to 0 for no foul line. +-- @return #RANGE self function RANGE:AddStrafePit(targetnames, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) self:F({targetnames=targetnames, boxlength=boxlength, boxwidth=boxwidth, heading=heading, inverseheading=inverseheading, goodpass=goodpass, foulline=foulline}) @@ -680,6 +757,8 @@ function RANGE:AddStrafePit(targetnames, boxlength, boxwidth, heading, inversehe local text=string.format("Adding new strafe target %s with %d targets: heading = %03d, box_L = %.1f, box_W = %.1f, goodpass = %d, foul line = %.1f", _name, ntargets, heading, l, w, goodpass, foulline) self:T(RANGE.id..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) + + return self end @@ -695,6 +774,7 @@ end -- @param #boolean inverseheading (Optional) Take inverse heading (heading --> heading - 180 Degrees). Default is false. -- @param #number goodpass (Optional) Number of hits for a "good" strafing pass. Default is 20. -- @param #number foulline (Optional) Foul line distance. Hits from closer than this distance are not counted. Default 610 m = 2000 ft. Set to 0 for no foul line. +-- @return #RANGE self function RANGE:AddStrafePitGroup(group, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) self:F({group=group, boxlength=boxlength, boxwidth=boxwidth, heading=heading, inverseheading=inverseheading, goodpass=goodpass, foulline=foulline}) @@ -720,6 +800,7 @@ function RANGE:AddStrafePitGroup(group, boxlength, boxwidth, heading, inversehea self:AddStrafePit(_names, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) end + return self end --- Add bombing target(s) to range. @@ -727,6 +808,7 @@ end -- @param #table targetnames Table containing names of unit or static objects serving as bomb targets. -- @param #number goodhitrange (Optional) Max distance from target unit (in meters) which is considered as a good hit. Default is 25 m. -- @param #boolean randommove If true, unit will move randomly within the range. Default is false. +-- @return #RANGE self function RANGE:AddBombingTargets(targetnames, goodhitrange, randommove) self:F({targetnames=targetnames, goodhitrange=goodhitrange, randommove=randommove}) @@ -756,6 +838,8 @@ function RANGE:AddBombingTargets(targetnames, goodhitrange, randommove) end end + + return self end --- Add a unit or static object as bombing target. @@ -763,6 +847,7 @@ end -- @param Wrapper.Positionable#POSITIONABLE unit Positionable (unit or static) of the strafe target. -- @param #number goodhitrange Max distance from unit which is considered as a good hit. -- @param #boolean randommove If true, unit will move randomly within the range. Default is false. +-- @return #RANGE self function RANGE:AddBombingTargetUnit(unit, goodhitrange, randommove) self:F({unit=unit, goodhitrange=goodhitrange, randommove=randommove}) @@ -797,6 +882,8 @@ function RANGE:AddBombingTargetUnit(unit, goodhitrange, randommove) -- Insert target to table. table.insert(self.bombingTargets, {name=name, target=unit, goodhitrange=goodhitrange, move=randommove, speed=speed}) + + return self end --- Add all units of a group as bombing targets. @@ -804,6 +891,7 @@ end -- @param Wrapper.Group#GROUP group Group of bombing targets. -- @param #number goodhitrange Max distance from unit which is considered as a good hit. -- @param #boolean randommove If true, unit will move randomly within the range. Default is false. +-- @return #RANGE self function RANGE:AddBombingTargetGroup(group, goodhitrange, randommove) self:F({group=group, goodhitrange=goodhitrange, randommove=randommove}) @@ -818,6 +906,7 @@ function RANGE:AddBombingTargetGroup(group, goodhitrange, randommove) end end + return self end --- Measures the foule line distance between two unit or static objects. @@ -970,11 +1059,12 @@ function RANGE:OnEventBirth(EventData) -- By default, some bomb impact points and do not flare each hit on target. self.PlayerSettings[_playername]={} - self.PlayerSettings[_playername].smokebombimpact=true + self.PlayerSettings[_playername].smokebombimpact=self.defaultsmokebomb self.PlayerSettings[_playername].flaredirecthits=false self.PlayerSettings[_playername].smokecolor=SMOKECOLOR.Blue self.PlayerSettings[_playername].flarecolor=FLARECOLOR.Red self.PlayerSettings[_playername].delaysmoke=true + self.PlayerSettings[_playername].messages=true -- Start check in zone timer. if self.planes[_uid] ~= true then @@ -1041,7 +1131,7 @@ function RANGE:OnEventHit(EventData) if _currentTarget.pastfoulline==false and _unit and _playername then local _d=_currentTarget.zone.foulline local text=string.format("%s, Invalid hit!\nYou already passed foul line distance of %d m for target %s.", self:_myname(_unitName), _d, targetname) - self:_DisplayMessageToGroup(_unit, text, 10) + self:_DisplayMessageToGroup(_unit, text) self:T2(RANGE.id..text) _currentTarget.pastfoulline=true end @@ -1163,11 +1253,19 @@ function RANGE:OnEventShot(EventData) -- Coordinate of impact point. local impactcoord=COORDINATE:NewFromVec3(_lastBombPos) + -- Check if impact happend in range zone. + local insidezone=self.rangezone:IsCoordinateInZone(impactcoord) + -- Distance from range. We dont want to smoke targets outside of the range. local impactdist=impactcoord:Get2DDistance(self.location) + -- Impact point of bomb. + if self.Debug then + impactcoord:MarkToAll("Bomb impact point") + end + -- Smoke impact point of bomb. - if self.PlayerSettings[_playername].smokebombimpact and impactdist/ - if RANGE.MenuF10[_gid] == nil then - RANGE.MenuF10[_gid]=missionCommands.addSubMenuForGroup(_gid, "On the Range") - end - local _rangePath = missionCommands.addSubMenuForGroup(_gid, self.rangename, RANGE.MenuF10[_gid]) + + -- Range root menu path. + local _rangePath=nil + + if RANGE.MenuF10Root then + + ------------------- + -- MISSION LEVEL -- + ------------------- + + _rangePath = missionCommands.addSubMenuForGroup(_gid, self.rangename, RANGE.MenuF10Root) + + else + + ----------------- + -- GROUP LEVEL -- + ----------------- + + -- Main F10 menu: F10/On the Range// + if RANGE.MenuF10[_gid] == nil then + RANGE.MenuF10[_gid]=missionCommands.addSubMenuForGroup(_gid, "On the Range") + end + _rangePath = missionCommands.addSubMenuForGroup(_gid, self.rangename, RANGE.MenuF10[_gid]) + + end + + local _statsPath = missionCommands.addSubMenuForGroup(_gid, "Statistics", _rangePath) local _markPath = missionCommands.addSubMenuForGroup(_gid, "Mark Targets", _rangePath) local _settingsPath = missionCommands.addSubMenuForGroup(_gid, "My Settings", _rangePath) @@ -1921,9 +2042,11 @@ function RANGE:_AddF10Commands(_unitName) missionCommands.addCommandForGroup(_gid, "White Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.White) missionCommands.addCommandForGroup(_gid, "Yellow Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.Yellow) -- F10/On the Range//My Settings/ - missionCommands.addCommandForGroup(_gid, "Smoke Delay On/Off", _settingsPath, self._SmokeBombDelayOnOff, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Smoke Delay On/Off", _settingsPath, self._SmokeBombDelayOnOff, self, _unitName) missionCommands.addCommandForGroup(_gid, "Smoke Impact On/Off", _settingsPath, self._SmokeBombImpactOnOff, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Flare Hits On/Off", _settingsPath, self._FlareDirectHitsOnOff, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Flare Hits On/Off", _settingsPath, self._FlareDirectHitsOnOff, self, _unitName) + missionCommands.addCommandForGroup(_gid, "All Messages On/Off", _settingsPath, self._MessagesToPlayerOnOff, self, _unitName) + -- F10/On the Range//Range Information missionCommands.addCommandForGroup(_gid, "General Info", _infoPath, self._DisplayRangeInfo, self, _unitName) missionCommands.addCommandForGroup(_gid, "Weather Report", _infoPath, self._DisplayRangeWeather, self, _unitName) @@ -2103,7 +2226,7 @@ function RANGE:_ResetRangeStats(_unitName) self.strafePlayerResults[_playername] = nil self.bombPlayerResults[_playername] = nil local text=string.format("%s, %s, your range stats were cleared.", self.rangename, _playername) - self:DisplayMessageToGroup(_unit, text, 5) + self:DisplayMessageToGroup(_unit, text, 5, false, true) end end @@ -2113,33 +2236,35 @@ end -- @param #string _text Message text. -- @param #number _time Duration how long the message is displayed. -- @param #boolean _clear Clear up old messages. -function RANGE:_DisplayMessageToGroup(_unit, _text, _time, _clear) +-- @param #boolean display If true, display message regardless of player setting "Messages Off". +function RANGE:_DisplayMessageToGroup(_unit, _text, _time, _clear, display) self:F({unit=_unit, text=_text, time=_time, clear=_clear}) + -- Defaults _time=_time or self.Tmsg - if _clear==nil then + if _clear==nil or _clear==false then _clear=false + else + _clear=true end -- Group ID. local _gid=_unit:GetGroup():GetID() - if _gid and not self.examinerexclusive then - if _clear == true then - trigger.action.outTextForGroup(_gid, _text, _time, _clear) - else - trigger.action.outTextForGroup(_gid, _text, _time) - end + -- Get playername and player settings + local _, playername=self:_GetPlayerUnitAndName(_unit:GetName()) + local playermessage=self.PlayerSettings[playername].messages + + -- Send message to player if messages enabled and not only for the examiner. + if _gid and (playermessage==true or display) and (not self.examinerexclusive) then + trigger.action.outTextForGroup(_gid, _text, _time, _clear) end + -- Send message to examiner. if self.examinergroupname~=nil then local _examinerid=GROUP:FindByName(self.examinergroupname):GetID() if _examinerid then - if _clear == true then - trigger.action.outTextForGroup(_examinerid, _text, _time, _clear) - else - trigger.action.outTextForGroup(_examinerid, _text, _time) - end + trigger.action.outTextForGroup(_examinerid, _text, _time, _clear) end end @@ -2161,7 +2286,7 @@ function RANGE:_SmokeBombImpactOnOff(unitname) self.PlayerSettigs[playername].smokebombimpact=true text=string.format("%s, %s, smoking impact points of bombs is now ON.", self.rangename, playername) end - self:_DisplayMessageToGroup(unit, text, 5) + self:_DisplayMessageToGroup(unit, text, 5, false, true) end end @@ -2182,7 +2307,27 @@ function RANGE:_SmokeBombDelayOnOff(unitname) self.PlayerSettigs[playername].delaysmoke=true text=string.format("%s, %s, delayed smoke of bombs is now ON.", self.rangename, playername) end - self:_DisplayMessageToGroup(unit, text, 5) + self:_DisplayMessageToGroup(unit, text, 5, false, true) + end + +end + +--- Toggle display messages to player. +-- @param #RANGE self +-- @param #string unitname Name of the player unit. +function RANGE:_MessagesToPlayerOnOff(unitname) + self:F(unitname) + + local unit, playername = self:_GetPlayerUnitAndName(unitname) + if unit and playername then + local text + if self.PlayerSettings[playername].messages==true then + text=string.format("%s, %s, display of ALL messages is now OFF.", self.rangename, playername) + else + text=string.format("%s, %s, display of ALL messages is now ON.", self.rangename, playername) + end + self:_DisplayMessageToGroup(unit, text, 5, false, true) + self.PlayerSettings[playername].messages=not self.PlayerSettings[playername].messages end end @@ -2203,7 +2348,7 @@ function RANGE:_FlareDirectHitsOnOff(unitname) self.PlayerSettings[playername].flaredirecthits=true text=string.format("%s, %s, flaring direct hits is now ON.", self.rangename, playername) end - self:_DisplayMessageToGroup(unit, text, 5) + self:_DisplayMessageToGroup(unit, text, 5, false, true) end end @@ -2321,7 +2466,7 @@ function RANGE:_smokecolor2text(color) elseif color==SMOKECOLOR.White then txt="white" else - txt=string.format("unkown color (%s)", tostring(color)) + txt=string.format("unknown color (%s)", tostring(color)) end return txt @@ -2344,7 +2489,7 @@ function RANGE:_flarecolor2text(color) elseif color==FLARECOLOR.Yellow then txt="yellow" else - txt=string.format("unkown color (%s)", tostring(color)) + txt=string.format("unknown color (%s)", tostring(color)) end return txt diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 499e0683e..c2b21cdd7 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -1,12 +1,12 @@ --- **Functional** - Simulation of logistic operations. --- +-- -- === --- +-- -- ## Features: -- --- * Holds (virtual) assests in stock and spawns them upon request. +-- * Holds (virtual) assets in stock and spawns them upon request. -- * Manages requests of assets from other warehouses. --- * Queueing system with optional priorization of requests. +-- * Queueing system with optional prioritization of requests. -- * Realistic transportation of assets between warehouses. -- * Different means of automatic transportation (planes, helicopters, APCs, self propelled). -- * Strategic components such as capturing, defending and destroying warehouses and their associated infrastructure. @@ -15,18 +15,18 @@ -- * Persistence of assets. Warehouse assets can be saved and loaded from file. -- * Can be easily interfaced to other MOOSE classes. -- --- === --- --- ## Missions: --- -- === --- --- The MOOSE warehouse concept simulates the organization and implementation of complex operations regarding the flow of assets between the point of origin and the point of consumption --- in order to meet requirements of a potential conflict. In particular, this class is concerned with maintaining army supply lines while disrupting those of the enemy, since an armed +-- +-- ## Missions: +-- +-- === +-- +-- The MOOSE warehouse concept simulates the organization and implementation of complex operations regarding the flow of assets between the point of origin and the point of consumption +-- in order to meet requirements of a potential conflict. In particular, this class is concerned with maintaining army supply lines while disrupting those of the enemy, since an armed -- force without resources and transportation is defenseless. -- -- Please note that his class is work in progress and in an **alpha** stage. --- +-- -- === -- -- ### Author: **funkyfranky** @@ -35,14 +35,14 @@ -- === -- -- @module Functional.Warehouse --- @image MOOSE.JPG +-- @image Warehouse.JPG --- WAREHOUSE class. -- @type WAREHOUSE -- @field #string ClassName Name of the class. -- @field #boolean Debug If true, send debug messages to all. -- @field #boolean Report If true, send status messages to coalition. --- @field Wrapper.Static#STATIC warehouse The phyical warehouse structure. +-- @field Wrapper.Static#STATIC warehouse The phyical warehouse structure. -- @field #string alias Alias of the warehouse. Name its called when sending messages. -- @field Core.Zone#ZONE zone Zone around the warehouse. If this zone is captured, the warehouse and all its assets goes to the capturing coaliton. -- @field Wrapper.Airbase#AIRBASE airbase Airbase the warehouse belongs to. @@ -63,12 +63,14 @@ -- @field #table defending Table holding all defending requests, i.e. self requests that were if the warehouse is under attack. Table elements are of type @{#WAREHOUSE.Pendingitem}. -- @field Core.Zone#ZONE portzone Zone defining the port of a warehouse. This is where naval assets are spawned. -- @field #table shippinglanes Table holding the user defined shipping between warehouses. --- @field #table offroadpaths Table holding user defined paths from one warehouse to another. +-- @field #table offroadpaths Table holding user defined paths from one warehouse to another. -- @field #boolean autodefence When the warehouse is under attack, automatically spawn assets to defend the warehouse. -- @field #number spawnzonemaxdist Max distance between warehouse and spawn zone. Default 5000 meters. -- @field #boolean autosave Automatically save assets to file when mission ends. -- @field #string autosavepath Path where the asset file is saved on auto save. --- @field #string autosavefilename File name of the auto asset save file. Default is auto generated from warehouse id and name. +-- @field #string autosavefile File name of the auto asset save file. Default is auto generated from warehouse id and name. +-- @field #boolean safeparking If true, parking spots for aircraft are considered as occupied if e.g. a client aircraft is parked there. Default false. +-- @field #boolean isunit If true, warehouse is represented by a unit instead of a static. -- @extends Core.Fsm#FSM --- Have your assets at the right place at the right time - or not! @@ -76,233 +78,233 @@ -- === -- -- # The Warehouse Concept --- +-- -- The MOOSE warehouse adds a new logistic component to the DCS World. *Assets*, i.e. ground, airborne and naval units, can be transferred from one place -- to another in a realistic and highly automatic fashion. In contrast to a "DCS warehouse" these assets have a physical representation in game. In particular, -- this means they can be destroyed during the transport and add more life to the DCS world. --- +-- -- This comes along with some additional interesting stategic aspects since capturing/defending and destroying/protecting an enemy or your --- own warehous becomes of critical importance for the development of a conflict. --- +-- own warehouse becomes of critical importance for the development of a conflict. +-- -- In essence, creating an efficient network of warehouses is vital for the success of a battle or even the whole war. Likewise, of course, cutting off the enemy --- of important supply lines by capturing or destroying warehouses or their associated infrastructure is equally important. --- +-- of important supply lines by capturing or destroying warehouses or their associated infrastructure is equally important. +-- -- ## What is a warehouse? --- +-- -- A warehouse is an abstract object represented by a physical (static) building that can hold virtual assets in stock. -- It can (but it must not) be associated with a particular airbase. The associated airbase can be an airdrome, a Helipad/FARP or a ship. --- +-- -- If another warehouse requests assets, the corresponding troops are spawned at the warehouse and being transported to the requestor or go their -- by themselfs. Once arrived at the requesting warehouse, the assets go into the stock of the requestor and can be activated/deployed when necessary. --- +-- -- ## What assets can be stored? --- +-- -- Any kind of ground, airborne or naval asset can be stored and are spawned upon request. -- The fact that the assets live only virtually in stock and are put into the game only when needed has a positive impact on the game performance. --- It also alliviates the problem of limited parking spots at smaller airbases. --- +-- It also alliviates the problem of limited parking spots at smaller airbases. +-- -- ## What means of transportation are available? --- +-- -- Firstly, all mobile assets can be send from warehouse to another on their own. --- +-- -- * Ground vehicles will use the road infrastructure. So a good road connection for both warehouses is important but also off road connections can be added if necessary. -- * Airborne units get a flightplan from the airbase of the sending warehouse to the airbase of the receiving warehouse. This already implies that for airborne -- assets both warehouses need an airbase. If either one of the warehouses does not have an associated airbase, direct transportation of airborne assest is not possible. -- * Naval units can be exchanged between warehouses which possess a port, which can be defined by the user. Also shipping lanes must be specified manually but the user since DCS does not provide these. --- * Trains (would) use the available railroad infrastructure and both warehouses must have a connection to the railroad. Unfortunately, however, trains are not yet implemented to +-- * Trains (would) use the available railroad infrastructure and both warehouses must have a connection to the railroad. Unfortunately, however, trains are not yet implemented to -- a reasonable degree in DCS at the moment and hence cannot be used yet. --- +-- -- Furthermore, ground assets can be transferred between warehouses by transport units. These are APCs, helicopters and airplanes. The transportation process is modelled -- in a realistic way by using the corresponding cargo dispatcher classes, i.e. --- +-- -- * @{AI.AI_Cargo_Dispatcher_APC#AI_DISPATCHER_APC} --- * @{AI.AI_Cargo_Dispatcher_Helicopter#AI_DISPATCHER_HELICOPTER} +-- * @{AI.AI_Cargo_Dispatcher_Helicopter#AI_DISPATCHER_HELICOPTER} -- * @{AI.AI_Cargo_Dispatcher_Airplane#AI_DISPATCHER_AIRPLANE} --- +-- -- Depending on which cargo dispatcher is used (ground or airbore), similar considerations like in the self propelled case are necessary. Howver, note that -- the dispatchers as of yet cannot use user defined off road paths for example since they are classes of their own and use a different routing logic. --- +-- -- === --- +-- -- # Creating a Warehouse --- +-- -- A MOOSE warehouse must be represented in game by a physical *static* object. For example, the mission editor already has warehouse as static object available. -- This would be a good first choice but any static object will do. --- +-- -- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Static.png) --- +-- -- The positioning of the warehouse static object is very important for a couple of reasons. Firstly, a warehouse needs a good infrastructure so that spawned assets -- have a proper road connection or can reach the associated airbase easily. --- +-- -- ## Constructor and Start --- +-- -- Once the static warehouse object is placed in the mission editor it can be used as a MOOSE warehouse by the @{#WAREHOUSE.New}(*warehousestatic*, *alias*) constructor, -- like for example: --- +-- -- warehouseBatumi=WAREHOUSE:New(STATIC:FindByName("Warehouse Batumi"), "My optional Warehouse Alias") -- warehouseBatumi:Start() --- +-- -- The first parameter *warehousestatic* is the static MOOSE object. By default, the name of the warehouse will be the same as the name given to the static object. --- The second parameter *alias* is optional and can be used to choose a more convenient name if desired. This will be the name the warehouse calls itself when reporting messages. --- +-- The second parameter *alias* is optional and can be used to choose a more convenient name if desired. This will be the name the warehouse calls itself when reporting messages. +-- -- Note that a warehouse also needs to be started in order to be in service. This is done with the @{#WAREHOUSE.Start}() or @{#WAREHOUSE.__Start}(*delay*) functions. -- The warehouse is now fully operational and requests are being processed. --- +-- -- # Adding Assets --- +-- -- Assets can be added to the warehouse stock by using the @{#WAREHOUSE.AddAsset}(*group*, *ngroups*, *forceattribute*, *forcecargobay*, *forceweight*, *loadradius*, *skill*, *liveries*, *assignment*) function. -- The parameter *group* has to be a MOOSE @{Wrapper.Group#GROUP}. This is also the only mandatory parameters. All other parameters are optional and can be used for fine tuning if -- nessary. The parameter *ngroups* specifies how many clones of this group are added to the stock. --- +-- -- infrantry=GROUP:FindByName("Some Infantry Group") -- warehouseBatumi:AddAsset(infantry, 5) --- --- This will add five infantry groups to the warehouse stock. Note that the group should normally be a late activated template group, +-- +-- This will add five infantry groups to the warehouse stock. Note that the group should normally be a late activated template group, -- which was defined in the mission editor. But you can also add other groups which are already spawned and present in the mission. --- +-- -- Also note that the coalition of the template group (red, blue or neutral) does not matter. The coalition of the assets is determined by the coalition of the warehouse owner. --- In other words, it is no problem to add red groups to blue warehouses and vice versa. The assets will automatically have the coalition of the warehouse. --- +-- In other words, it is no problem to add red groups to blue warehouses and vice versa. The assets will automatically have the coalition of the warehouse. +-- -- You can add assets with a delay by using the @{#WAREHOUSE.__AddAsset}(*delay*, *group*, *ngroups*, *forceattribute*, *forcecargobay*, *forceweight*, *loadradius*, *skill*, *liveries*, *assignment*), -- where *delay* is the delay in seconds before the asset is added. --- +-- -- In game, the warehouse will get a mark which is regularly updated and showing the currently available assets in stock. --- +-- -- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Stock-Marker.png) --- +-- -- ## Optional Parameters for Fine Tuning --- +-- -- By default, the generalized attribute of the asset is determined automatically from the DCS descriptor attributes. However, this might not always result in the desired outcome. -- Therefore, it is possible, to force a generalized attribute for the asset with the third optional parameter *forceattribute*, which is of type @{#WAREHOUSE.Attribute}. --- +-- -- ### Setting the Generalized Attibute -- For example, a UH-1H Huey has in DCS the attibute of an attack helicopter. But of course, it can also transport cargo. If you want to use it for transportation, you can specify this -- manually when the asset is added --- +-- -- warehouseBatumi:AddAsset("Huey", 5, WAREHOUSE.Attribute.AIR_TRANSPORTHELO) --- --- This becomes important when assets are requested from other warehouses as described below. In this case, the five Hueys are now marked as transport helicopters and +-- +-- This becomes important when assets are requested from other warehouses as described below. In this case, the five Hueys are now marked as transport helicopters and -- not attack helicopters. --- --- ### Setting the Cargo Bay Weight Limit +-- +-- ### Setting the Cargo Bay Weight Limit -- You can ajust the cargo bay weight limit, in case it is not calculated correctly automatically. For example, the cargo bay of a C-17A is much smaller in DCS than that of a C-130, which is -- unrealistic. This can be corrected by the *forcecargobay* parmeter which is here set to 77,000 kg --- +-- -- warehouseBatumi:AddAsset("C-17A", nil, nil, 77000) --- +-- -- The size of the cargo bay is only important when the group is used as transport carrier for other assets. --- +-- -- ### Setting the Weight -- If an asset shall be transported by a carrier it important to note that - as in real life - a carrier can only carry cargo up to a certain weight. The weight of the -- units is automatically determined from the DCS descriptor table. -- However, in the current DCS version (2.5.3) a mortar unit has a weight of 5 tons. This confuses the transporter logic, because it appears to be too have for, e.g. all APCs. --- +-- -- As a workaround, you can manually adjust the weight by the optional *forceweight* parameter: --- +-- -- warehouseBatumi:AddAsset("Mortar Alpha", nil, nil, nil, 210) --- +-- -- In this case we set it to 210 kg. Note, the weight value set is meant for *each* unit in the group. Therefore, a group consisting of three mortars will have a total weight --- of 630 kg. This is important as groups cannot be split between carrier units when transporting, i.e. the total weight of the whole group must be smaller than the +-- of 630 kg. This is important as groups cannot be split between carrier units when transporting, i.e. the total weight of the whole group must be smaller than the -- cargo bay of the transport carrier. --- +-- -- ### Setting the Load Radius -- Boading and loading of cargo into a carrier is modeled in a realistic fashion in the AI\_CARGO\DISPATCHER classes, which are used inernally by the WAREHOUSE class. -- Meaning that troops (cargo) will board, i.e. run or drive to the carrier, and only once they are in close proximity to the transporter they will be loaded (disappear). --- +-- -- Unfortunately, there are some situations where problems can occur. For example, in DCS tanks have the strong tentendcy not to drive around obstacles but rather to roll over them. -- I have seen cases where an aircraft of the same coalition as the tank was in its way and the tank drove right through the plane waiting on a parking spot and destroying it. --- +-- -- As a workaround it is possible to set a larger load radius so that the cargo units are despawned further away from the carrier via the optional **loadradius** parameter: --- +-- -- warehouseBatumi:AddAsset("Leopard 2", nil, nil, nil, nil, 250) --- +-- -- Adding the asset like this will cause the units to be loaded into the carrier already at a distance of 250 meters. --- +-- -- ### Setting the AI Skill --- --- By default, the asset has the skill of its template group. The optional parameter *skill* allows to set a different skill when the asset is added. See the +-- +-- By default, the asset has the skill of its template group. The optional parameter *skill* allows to set a different skill when the asset is added. See the -- [hoggit page](https://wiki.hoggitworld.com/view/DCS_enum_AI) possible values of this enumerator. -- For example you can use --- +-- -- warehouseBatumi:AddAsset("Leopard 2", nil, nil, nil, nil, nil, AI.Skill.EXCELLENT) --- +-- -- do set the skill of the asset to excellent. --- +-- -- ### Setting Liveries --- +-- -- By default ,the asset uses the livery of its template group. The optional parameter *liveries* allows to define one or multiple liveries. -- If multiple liveries are given in form of a table of livery names, each asset gets a random one. --- +-- -- For example --- +-- -- warehouseBatumi:AddAsset("Mi-8", nil, nil, nil, nil, nil, nil, "China UN") --- +-- -- would spawn the asset with a chinese UN livery. --- +-- -- Or --- +-- -- warehouseBatumi:AddAsset("Mi-8", nil, nil, nil, nil, nil, nil, {"China UN", "German"}) --- +-- -- would spawn the asset with either a chinese UN or German livery. Mind the curly brackets **{}** when you want to specify multiple liveries. --- +-- -- Four each unit type, the livery names can be found in the DCS root folder under Bazar\Liveries. You have to use the name of the livery subdirectory. The names of the liveries -- as displayed in the mission editor might be different and won't work in general. --- +-- -- ### Setting an Assignment --- +-- -- Assets can be added with a specific assignment given as a text, e.g. --- +-- -- warehouseBatumi:AddAsset("Mi-8", nil, nil, nil, nil, nil, nil, nil, "Go to Warehouse Kobuleti") --- +-- -- This is helpful to establish supply chains once an asset has arrived at its (first) destination and is meant to be forwarded to another warehouse. --- +-- -- ## Retrieving the Asset --- +-- -- Once a an asset is added to a warehouse, the @{#WAREHOUSE.NewAsset} event is triggered. You can hook into this event with the @{#WAREHOUSE.OnAfterNewAsset}(*asset*, *assignment*) function. --- +-- -- The first parameter *asset* is a table of type @{#WAREHOUSE.Assetitem} and contains a lot of information about the asset. The seconed parameter *assignment* is optional and is the specific -- assignment the asset got when it was added. --- +-- -- Note that the assignment is can also be the assignment that was specified when adding a request (see next section). Once an asset that was requested from another warehouse and an assignment -- was specified in the @{#WAREHOUSE.AddRequest} function, the assignment can be checked when the asset has arrived and is added to the receiving warehouse. --- +-- -- === -- -- # Requesting Assets --- +-- -- Assets of the warehouse can be requested by other MOOSE warehouses. A request will first be scrutinized to check if can be fulfilled at all. If the request is valid, it is -- put into the warehouse queue and processed as soon as possible. --- +-- -- A request can be added by the @{#WAREHOUSE.AddRequest}(*warehouse*, *AssetDescriptor*, *AssetDescriptorValue*, *nAsset*, *TransportType*, *nTransport*, *Prio*, *Assignment*) function. -- The parameters are --- +-- -- * *warehouse*: The requesting MOOSE @{#WAREHOUSE}. Assets will be delivered there. --- * *AssetDescriptor*: The descriptor to describe the asset "type". See the @{#WAREHOUSE.Descriptor} enumerator. For example, assets requested by their generalized attibute. +-- * *AssetDescriptor*: The descriptor to describe the asset "type". See the @{#WAREHOUSE.Descriptor} enumerator. For example, assets requested by their generalized attibute. -- * *AssetDescriptorValue*: The value of the asset descriptor. -- * *nAsset*: (Optional) Number of asset group requested. Default is one group. -- * *TransportType*: (Optional) The transport method used to deliver the assets to the requestor. Default is that assets go to the requesting warehouse on their own. -- * *nTransport*: (Optional) Number of asset groups used to transport the cargo assets from A to B. Default is one group. -- * *Prio*: (Optional) A number between 1 (high) and 100 (low) describing the priority of the request. Request with high priority are processed first. Default is 50, i.e. medium priority. --- * *Assignment*: (Optional) A free to choose string describing the assignment. For self requests, this can be used to assign the spawned groups to specific tasks. --- +-- * *Assignment*: (Optional) A free to choose string describing the assignment. For self requests, this can be used to assign the spawned groups to specific tasks. +-- -- ## Requesting by Generalized Attribute --- --- Generalized attributes are similar to [DCS attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes). However, they are a bit more general and +-- +-- Generalized attributes are similar to [DCS attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes). However, they are a bit more general and -- an asset can only have one generalized attribute by which it is characterized. --- +-- -- For example: --- +-- -- warehouseBatumi:AddRequest(warehouseKobuleti, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 5, WAREHOUSE.TransportType.APC, 2) -- -- Here, warehouse Kobuleti requests 5 infantry groups from warehouse Batumi. These "cargo" assets should be transported from Batumi to Kobuleti by 2 APCS. -- Note that the warehouse at Batumi needs to have at least five infantry groups and two APC groups in their stock if the request can be processed. -- If either to few infantry or APC groups are available when the request is made, the request is held in the warehouse queue until enough cargo and -- transport assets are available. --- +-- -- Also note that the above request is for five infantry groups. So any group in stock that has the generalized attribute "GROUND_INFANTRY" can be selected for the request. --- +-- -- ### Generalized Attributes --- +-- -- Currently implemented are: -- -- * @{#WAREHOUSE.Attribute.AIR_TRANSPORTPLANE} Airplane with transport capability. This can be used to transport other assets. @@ -332,76 +334,76 @@ -- * @{#WAREHOUSE.Attribute.OTHER_UNKNOWN} Anything that does not fall into any other category. -- -- ## Requesting a Specific Unit Type --- +-- -- A more specific request could look like: --- +-- -- warehouseBatumi:AddRequest(warehouseKobuleti, WAREHOUSE.Descriptor.UNITTYPE, "A-10C", 2) --- +-- -- Here, Kobuleti requests a specific unit type, in particular two groups of A-10Cs. Note that the spelling is important as it must exacly be the same as -- what one get's when using the DCS unit type. --- +-- -- ## Requesting a Specific Group --- +-- -- An even more specific request would be: --- +-- -- warehouseBatumi:AddRequest(warehouseKobuleti, WAREHOUSE.Descriptor.GROUPNAME, "Group Name as in ME", 3) --- +-- -- In this case three groups named "Group Name as in ME" are requested. This explicitly request the groups named like that in the Mission Editor. --- +-- -- ## Requesting a General Category --- +-- -- On the other hand, very general and unspecifc requests can be made by the categroy descriptor. The descriptor value parameter can be any [group category](https://wiki.hoggitworld.com/view/DCS_Class_Group), i.e. --- +-- -- * Group.Category.AIRPLANE for fixed wing aircraft, -- * Group.Category.HELICOPTER for helicopters, -- * Group.Category.GROUND for all ground troops, -- * Group.Category.SHIP for naval assets, -- * Group.Category.TRAIN for trains (not implemented and not working in DCS yet). --- +-- -- For example, --- +-- -- warehouseBatumi:AddRequest(warehouseKobuleti, WAREHOUSE.Descriptor.CATEGORY, Group.Category.GROUND, 10) --- +-- -- means that Kubuleti requests 10 ground groups and does not care which ones. This could be a mix of infantry, APCs, trucks etc. --- +-- -- **Note** that these general requests should be made with *great care* due to the fact, that depending on what a warehouse has in stock a lot of different unit types can be spawned. --- +-- -- ## Requesting Relative Quantities --- --- In addition to requesting absolute numbers of assets it is possible to request relative amounts of assets currently in stock. To this end the @{#WAREHOUSE.Quantity} enumerator +-- +-- In addition to requesting absolute numbers of assets it is possible to request relative amounts of assets currently in stock. To this end the @{#WAREHOUSE.Quantity} enumerator -- was introduced: --- +-- -- * @{#WAREHOUSE.Quantity.ALL} -- * @{#WAREHOUSE.Quantity.HALF} -- * @{#WAREHOUSE.Quantity.QUARTER} -- * @{#WAREHOUSE.Quantity.THIRD} -- * @{#WAREHOUSE.Quantity.THREEQUARTERS} --- +-- -- For example, --- +-- -- warehouseBatumi:AddRequest(warehouseKobuleti, WAREHOUSE.Descriptor.CATEGORY, Group.Category.HELICOPTER, WAREHOUSE.Quantity.HALF) --- +-- -- means that Kobuleti warehouse requests half of all available helicopters which Batumi warehouse currently has in stock. --- +-- -- # Employing Assets - The Self Request --- --- Transferring assets from one warehouse to another is important but of course once the the assets are at the "right" place it is equally important that they +-- +-- Transferring assets from one warehouse to another is important but of course once the the assets are at the "right" place it is equally important that they -- can be employed for specific tasks and assignments. --- +-- -- Assets in the warehouses stock can be used for user defined tasks quite easily. They can be spawned into the game by a "***self request***", i.e. the warehouse -- requests the assets from itself: --- +-- -- warehouseBatumi:AddRequest(warehouseBatumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 5) --- +-- -- Note that the *sending* and *requesting* warehouses are *identical* in this case. --- +-- -- This would simply spawn five infantry groups in the spawn zone of the Batumi warehouse if/when they are available. --- +-- -- ## Accessing the Assets --- --- If a warehouse requests assets from itself, it triggers the event **SelfReqeuest**. The mission designer can capture this event with the associated +-- +-- If a warehouse requests assets from itself, it triggers the event **SelfReqeuest**. The mission designer can capture this event with the associated -- @{#WAREHOUSE.OnAfterSelfRequest}(*From*, *Event*, *To*, *groupset*, *request*) function. --- +-- -- --- OnAfterSelfRequest user function. Access groups spawned from the warehouse for further tasking. -- -- @param #WAREHOUSE self -- -- @param #string From From state. @@ -412,112 +414,112 @@ -- function WAREHOUSE:OnAfterSelfRequest(From, Event, To, groupset, request) -- local groupset=groupset --Core.Set#SET_GROUP -- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem --- +-- -- for _,group in pairs(groupset:GetSetObjects()) do -- local group=group --Wrapper.Group#GROUP -- group:SmokeGreen() -- end --- +-- -- end --- +-- -- The variable *groupset* is a @{Core.Set#SET_GROUP} object and holds all asset groups from the request. The code above shows, how the mission designer can access the groups -- for further tasking. Here, the groups are only smoked but, of course, you can use them for whatever assignment you fancy. --- +-- -- Note that airborne groups are spawned in **uncontrolled state** and need to be activated first before they can begin with their assigned tasks and missions. -- This can be done with the @{Wrapper.Controllable#CONTROLLABLE.StartUncontrolled} function as demonstrated in the example section below. --- +-- -- === --- +-- -- # Infrastructure --- +-- -- A good infrastructure is important for a warehouse to be efficient. Therefore, the location of a warehouse should be chosen with care. -- This can also help to avoid many DCS related issues such as units getting stuck in buildings, blocking taxi ways etc. --- +-- -- ## Spawn Zone --- +-- -- By default, the zone were ground assets are spawned is a circular zone around the physical location of the warehouse with a radius of 200 meters. However, the location of the -- spawn zone can be set by the @{#WAREHOUSE.SetSpawnZone}(*zone*) functions. It is advisable to choose a zone which is clear of obstacles. --- +-- -- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Batumi.png) --- +-- -- The parameter *zone* is a MOOSE @{Core.Zone#ZONE} object. So one can, e.g., use trigger zones defined in the mission editor. If a cicular zone is not desired, one -- can use a polygon zone (see @{Core.Zone#ZONE_POLYGON}). --- +-- -- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_SpawnPolygon.png) --- +-- -- ## Road Connections --- +-- -- Ground assets will use a road connection to travel from one warehouse to another. Therefore, a proper road connection is necessary. --- +-- -- By default, the closest point on road to the center of the spawn zone is chosen as road connection automatically. But only, if distance between the spawn zone -- and the road connection is less than 3 km. --- +-- -- The user can set the road connection manually with the @{#WAREHOUSE.SetRoadConnection} function. This is only functional for self propelled assets at the moment -- and not if using the AI dispatcher classes since these have a different logic to find the route. --- +-- -- ## Off Road Connections --- +-- -- For ground troops it is also possible to define off road paths between warehouses if no proper road connection is available or should not be used. --- +-- -- An off road path can be defined via the @{#WAREHOUSE.AddOffRoadPath}(*remotewarehouse*, *group*, *oneway*) function, where -- *remotewarehouse* is the warehouse to which the path leads. -- The parameter *group* is a *late activated* template group. The waypoints of this group are used to define the path between the two warehouses. -- By default, the reverse paths is automatically added to get *from* the remote warehouse *to* this warehouse unless the parameter *oneway* is set to *true*. --- +-- -- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Off-Road_Paths.png) --- +-- -- **Note** that if an off road connection is defined between two warehouses this becomes the default path, i.e. even if there is a path *on road* possible -- this will not be used. --- +-- -- Also note that you can define multiple off road connections between two warehouses. If there are multiple paths defined, the connection is chosen randomly. -- It is also possible to add the same path multiple times. By this you can influence the probability of the chosen path. For example Path1(A->B) has been -- added two times while Path2(A->B) was added only once. Hence, the group will choose Path1 with a probability of 66.6 % while Path2 is only chosen with --- a probability of 33.3 %. --- +-- a probability of 33.3 %. +-- -- ## Rail Connections --- +-- -- A rail connection is automatically defined as the closest point on a railway measured from the center of the spawn zone. But only, if the distance is less than 3 km. --- +-- -- The mission designer can manually specify a rail connection with the @{#WAREHOUSE.SetRailConnection} function. --- +-- -- **NOTE** however, that trains in DCS are currently not implemented in a way so that they can be used. --- +-- -- ## Air Connections --- +-- -- In order to use airborne assets, a warehouse needs to have an associated airbase. This can be an airdrome, a FARP/HELOPAD or a ship. --- +-- -- If there is an airbase within 3 km range of the warehouse it is automatically set as the associated airbase. A user can set an airbase manually -- with the @{#WAREHOUSE.SetAirbase} function. Keep in mind that sometimes ground units need to walk/drive from the spawn zone to the airport -- to get to their transport carriers. --- +-- -- ## Naval Connections --- +-- -- Natively, DCS does not have the concept of a port/habour or shipping lanes. So in order to have a meaningful transfer of naval units between warehouses, these have to be -- defined by the mission designer. --- +-- -- ### Defining a Port --- +-- -- A port in this context is the zone where all naval assets are spawned. This zone can be defined with the function @{#WAREHOUSE.SetPortZone}(*zone*), where the parameter -- *zone* is a MOOSE zone. So again, this can be create from a trigger zone defined in the mission editor or if a general shape is desired by a @{Core.Zone#ZONE_POLYGON}. --- +-- -- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_PortZone.png) --- +-- -- ### Defining Shipping Lanes --- +-- -- A shipping lane between to warehouses can be defined by the @{#WAREHOUSE.AddShippingLane}(*remotewarehouse*, *group*, *oneway*) function. The first parameter *remotewarehouse* -- is the warehouse which should be connected to the present warehouse. --- +-- -- The parameter *group* should be a late activated group defined in the mission editor. The waypoints of this group are used as waypoints of the shipping lane. --- +-- -- By default, the reverse lane is automatically added to the remote warehouse. This can be disabled by setting the *oneway* parameter to *true*. --- +-- -- Similar to off road connections, you can also define multiple shipping lanes between two warehouse ports. If there are multiple lanes defined, one is chosen randomly. -- It is possible to add the same lane multiple times. By this you can influence the probability of the chosen lane. For example Lane_1(A->B) has been -- added two times while Lane_2(A->B) was added only once. Therefore, the ships will choose Lane_1 with a probability of 66.6 % while Path_2 is only chosen with --- a probability of 33.3 %. --- --- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_ShippingLane.png) --- +-- a probability of 33.3 %. +-- +-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_ShippingLane.png) +-- -- === -- -- # Why is my request not processed? @@ -525,14 +527,14 @@ -- For each request, the warehouse class logic does a lot of consistancy and validation checks under the hood. -- This helps to circumvent a lot of DCS issues and shortcomings. For example, it is checked that enough free -- parking spots at an airport are available *before* the assets are spawned. --- However, this also means that sometimes a request is deemed to be *invalid* in which case they are deleted +-- However, this also means that sometimes a request is deemed to be *invalid* in which case they are deleted -- from the queue or considered to be valid but cannot be executed at this very moment. --- +-- -- ## Invalid Requests --- --- Invalid request are requests which can **never** be processes because there is some logical or physical argument against it. +-- +-- Invalid request are requests which can **never** be processes because there is some logical or physical argument against it. -- (Or simply because that feature was not implemented (yet).) --- +-- -- * All airborne assets need an associated airbase of any kind on the sending *and* receiving warhouse. -- * Airplanes need an airdrome at the sending and receiving warehouses. -- * Not enough parking spots of the right terminal type at the sending warehouse. This avoids planes spawning on runways or on top of each other. @@ -545,112 +547,113 @@ -- * If transport by airplane, both warehouses must have and airdrome. -- * If transport by APC, both warehouses must have a road connection. -- * If transport by helicopter, the sending airbase must have an associated airbase (airdrome or FARP). --- +-- -- All invalid requests are cancelled and **removed** from the warehouse queue! --- +-- -- ## Temporarily Unprocessable Requests --- +-- -- Temporarily unprocessable requests are possible in priciple, but cannot be processed at the given time the warehouse checks its queue. --- +-- -- * No enough parking spaces are available for all requested assets but the airbase has enough parking spots in total so that this request is possible once other aircraft have taken off. -- * The requesting warehouse is not in state "Running" (could be paused, not yet started or under attack). -- * Not enough cargo assets available at this moment. -- * Not enough free parking spots for all cargo or transport airborne assets at the moment. -- * Not enough transport assets to carry all cargo assets. --- +-- -- Temporarily unprocessable requests are held in the queue. If at some point in time, the situation changes so that these requests can be processed, they are executed. --- +-- -- ## Cargo Bay and Weight Limitations --- +-- -- The transporation of cargo is handled by the AI\_Dispatcher classes. These take the cargo bay of a carrier and the weight of -- the cargo into account so that a carrier can only load a realistic amount of cargo. --- +-- -- However, if troops are supposed to be transported between warehouses, there is one important limitations one has to keep in mind. -- This is that **cargo asset groups cannot be split** and devided into separate carrier units! --- +-- -- For example, a TPz Fuchs has a cargo bay large enough to carry up to 10 soldiers at once, which is a realistic number. -- If a group consisting of more than ten soldiers needs to be transported, it cannot be loaded into the APC. --- Even if two APCs are available, which could in principle carry up to 20 soldiers, a group of, let's say 12 soldiers will not --- be split into a group of ten soldiers using the first APC and a group two soldiers using the second APC. --- +-- Even if two APCs are available, which could in principle carry up to 20 soldiers, a group of, let's say 12 soldiers will not +-- be split into a group of ten soldiers using the first APC and a group two soldiers using the second APC. +-- -- In other words, **there must be at least one carrier unit available that has a cargo bay large enough to load the heaviest cargo group!** -- The warehouse logic will automatically search all available transport assets for a large enough carrier. -- But if none is available, the request will be queued until a suitable carrier becomes available. --- +-- -- The only realistic solution in this case is to either provide a transport carrier with a larger cargo bay or to reduce the number of soldiers -- in the group. --- +-- -- A better way would be to have two groups of max. 10 soldiers each and one TPz Fuchs for transport. In this case, the first group is -- loaded and transported to the receiving warehouse. Once this is done, the carrier will drive back and pick up the remaining -- group. --- +-- -- As an artificial workaround one can manually set the cargo bay size to a larger value or alternatively reduce the weight of the cargo -- when adding the assets via the @{#WAREHOUSE.AddAsset} function. This might even be unavoidable if, for example, a SAM group -- should be transported since SAM sites only work when all units are in the same group. --- +-- -- ## Processing Speed --- +-- -- A warehouse has a limited speed to process requests. Each time the status of the warehouse is updated only one requests is processed. -- The time interval between status updates is 30 seconds by default and can be adjusted via the @{#WAREHOUSE.SetStatusUpdate}(*interval*) function. -- However, the status is also updated on other occasions, e.g. when a new request was added. --- +-- -- === --- +-- -- # Strategic Considerations --- +-- -- Due to the fact that a warehouse holds (or can hold) a lot of valuable assets, it makes a (potentially) juicy target for enemy attacks. -- There are several interesting situations, which can occurr. --- +-- -- ## Capturing a Warehouses Airbase --- +-- -- If a warehouse has an associated airbase, it can be captured by the enemy. In this case, the warehouse looses its ability so employ all airborne assets and is also cut-off -- from supply by airplanes. Supply of ground troops via helicopters is still possible, because they deliver the troops into the spawn zone. --- --- Technically, the capturing of the airbase is triggered by the DCS [S\_EVENT\_BASE\_CAPTURED](https://wiki.hoggitworld.com/view/DCS_event_base_captured) event. +-- +-- Technically, the capturing of the airbase is triggered by the DCS [S\_EVENT\_BASE\_CAPTURED](https://wiki.hoggitworld.com/view/DCS_event_base_captured) event. -- So the capturing takes place when only enemy ground units are in the airbase zone whilst no ground units of the present airbase owner are in that zone. --- +-- -- The warehouse will also create an event **AirbaseCaptured**, which can be captured by the @{#WAREHOUSE.OnAfterAirbaseCaptured} function. So the warehouse chief can react on -- this attack and for example deploy ground groups to re-capture its airbase. --- +-- -- When an airbase is re-captured the event **AirbaseRecaptured** is triggered and can be captured by the @{#WAREHOUSE.OnAfterAirbaseRecaptured} function. -- This can be used to put the defending assets back into the warehouse stock. --- +-- -- ## Capturing the Warehouse --- +-- -- A warehouse can be captured by the enemy coalition. If enemy ground troops enter the warehouse zone the event **Attacked** is triggered which can be captured by the -- @{#WAREHOUSE.OnAfterAttacked} event. By default the warehouse zone circular zone with a radius of 500 meters located at the center of the physical warehouse. --- The warehouse zone can be set via the @{#WAREHOUSE.SetWarehouseZone}(*zone*) function. The parameter *zone* must also be a cirular zone. --- +-- The warehouse zone can be set via the @{#WAREHOUSE.SetWarehouseZone}(*zone*) function. The parameter *zone* must also be a cirular zone. +-- -- The @{#WAREHOUSE.OnAfterAttacked} function can be used by the mission designer to react to the enemy attack. For example by deploying some or all ground troops -- currently in stock to defend the warehouse. Note that the warehouse also has a self defence option which can be enabled by the @{#WAREHOUSE.SetAutoDefenceOn}() -- function. In this case, the warehouse will automatically spawn all ground troops. If the spawn zone is further away from the warehouse zone, all mobile troops --- are routed to the warehouse zone. --- +-- are routed to the warehouse zone. The self request which is triggered on an automatic defence has the assignment "AutoDefence". So you can use this to +-- give orders to the groups that were spawned using the @{#WAREHOUSE.OnAfterSelfRequest} function. +-- -- If only ground troops of the enemy coalition are present in the warehouse zone, the warehouse and all its assets falls into the hands of the enemy. -- In this case the event **Captured** is triggered which can be captured by the @{#WAREHOUSE.OnAfterCaptured} function. --- +-- -- The warehouse turns to the capturing coalition, i.e. its physical representation, and all assets as well. In paticular, all requests to the warehouse will -- spawn assets beloning to the new owner. --- --- If the enemy troops could be defeated, i.e. no more troops of the opposite coalition are in the warehouse zone, the event **Defeated** is triggered and +-- +-- If the enemy troops could be defeated, i.e. no more troops of the opposite coalition are in the warehouse zone, the event **Defeated** is triggered and -- the @{#WAREHOUSE.OnAfterDefeated} function can be used to adapt to the new situation. For example putting back all spawned defender troops back into -- the warehouse stock. Note that if the automatic defence is enabled, all defenders are automatically put back into the warehouse on the **Defeated** event. --- +-- -- ## Destroying a Warehouse --- +-- -- If an enemy destroy the physical warehouse structure, the warehouse will of course stop all its services. In priciple, all assets contained in the warehouse are -- gone as well. So a warehouse should be properly defended. --- +-- -- Upon destruction of the warehouse, the event **Destroyed** is triggered, which can be captured by the @{#WAREHOUSE.OnAfterDestroyed} function. -- So the mission designer can intervene at this point and for example choose to spawn all or paricular types of assets before the warehouse is gone for good. -- -- === --- +-- -- # Hook in and Take Control --- +-- -- The Finite State Machine implementation allows mission designers to hook into important events and add their own code. -- Most of these events have already been mentioned but here is the list at a glance: --- +-- -- * "NotReadyYet" --> "Start" --> "Running" (Starting the warehouse) -- * "*" --> "Status" --> "*" (status updated in regular intervals) -- * "*" --> "AddAsset" --> "*" (adding a new asset to the warehouse stock) @@ -672,23 +675,23 @@ -- * "Running" --> "Pause" --> "Paused" (warehouse is paused) -- * "Paused" --> "Unpause" --> "Running" (warehouse is unpaused) -- * "*" --> "Stop" --> "Stopped" (warehouse is stopped) --- +-- -- The transitions are of the general form "From State" --> "Event" --> "To State". The "*" star denotes that the transition is possible from *any* state. -- Some transitions, however, are only allowed from certain "From States". For example, no requests can be processed if the warehouse is in "Paused" or "Destroyed" or "Stopped" state. -- -- Mission designers can capture the events with OnAfterEvent functions, e.g. @{#WAREHOUSE.OnAfterDelivered} or @{#WAREHOUSE.OnAfterAirbaseCaptured}. --- +-- -- === --- +-- -- # Persistence of Assets --- +-- -- Assets in stock of a warehouse can be saved to a file on your hard drive and then loaded from that file at a later point. This enables to restart the mission -- and restore the warehouse stock. --- +-- -- ## Prerequisites --- +-- -- **Important** By default, DCS does not allow for writing data to files. Therefore, one first has to comment out the line "sanitizeModule('io')", i.e. --- +-- -- do -- sanitizeModule('os') -- --sanitizeModule('io') @@ -698,62 +701,62 @@ -- end -- -- in the file "MissionScripting.lua", which is located in the subdirectory "Scripts" of your DCS installation root directory. --- +-- -- ### Don't! --- +-- -- Do not use **semi-colons** or **equal signs** in the group names of your assets as these are used as separators in the saved and loaded files texts. -- If you do, it will cause problems and give you a headache! --- +-- -- ## Save Assets --- +-- -- Saving asset data to file is achieved by the @{WAREHOUSE.Save}(*path*, *filename*) function. The parameter *path* specifies the path on the file system where the -- warehouse data is saved. If you do not specify a path, the file is saved your the DCS installation root directory. -- The parameter *filename* is optional and defines the name of the saved file. By default this is automatically created from the warehouse id and name, for example -- "Warehouse-1234_Batumi.txt". --- +-- -- warehouseBatumi:Save("D:\\My Warehouse Data\\") --- +-- -- This will save all asset data to in "D:\\My Warehouse Data\\Warehouse-1234_Batumi.txt". --- +-- -- ### Automatic Save at Mission End --- +-- -- The assets can be saved automatically when the mission is ended via the @{WAREHOUSE.SetSaveOnMissionEnd}(*path*, *filename*) function, i.e. --- +-- -- warehouseBatumi:SetSaveOnMissionEnd("D:\\My Warehouse Data\\") --- +-- -- ## Load Assets --- +-- -- Loading assets data from file is achieved by the @{WAREHOUSE.Load}(*path*, *filename*) function. The parameter *path* specifies the path on the file system where the -- warehouse data is loaded from. If you do not specify a path, the file is loaded from your the DCS installation root directory. -- The parameter *filename* is optional and defines the name of the file to load. By default this is automatically generated from the warehouse id and name, for example -- "Warehouse-1234_Batumi.txt". --- +-- -- Note that the warehouse **must not be started** and in the *Running* state in order to load the assets. In other words, loading should happen after the -- @{#WAREHOUSE.New} command is specified in the code but before the @{#WAREHOUSE.Start} command is given. --- +-- -- Loading the assets is done by --- +-- -- warehouseBatumi:New(STATIC:FindByName("Warehouse Batumi")) -- warehouseBatumi:Load("D:\\My Warehouse Data\\") -- warehouseBatumi:Start() --- +-- -- This sequence loads all assets from file. If a warehouse was captured in the last mission, it also respawns the static warehouse structure with the right coaliton. -- However, it due to DCS limitations it is not possible to set the airbase coalition. This has to be done manually in the mission editor. Or alternatively, one could -- spawn some ground units via a self request and let them capture the airbase. --- +-- -- === -- -- # Examples --- +-- -- This section shows some examples how the WAREHOUSE class is used in practice. This is one of the best ways to explain things, in my opinion. --- +-- -- But first, let me introduce a convenient way to define several warehouses in a table. This is absolutely *not necessary* but quite handy if you have -- multiple WAREHOUSE objects in your mission. --- +-- -- ## Example 0: Setting up a Warehouse Array --- +-- -- If you have multiple warehouses, you can put them in a table. This makes it easier to access them or to loop over them. --- +-- -- -- Define Warehouses. -- local warehouse={} -- -- Blue warehouses @@ -771,118 +774,118 @@ -- warehouse.Sochi = WAREHOUSE:New(STATIC:FindByName("Warehouse Sochi"), "Sochi") --Functional.Warehouse#WAREHOUSE -- -- Remarks: --- +-- -- * I defined the array as local, i.e. local warehouse={}. This is personal preference and sometimes causes trouble with the lua garbage collection. You can also define it as a global array/table! -- * The "--Functional.Warehouse#WAREHOUSE" at the end is only to have the LDT intellisense working correctly. If you don't use LDT (which you should!), it can be omitted. -- -- **NOTE** that all examples below need this bit or code at the beginning - or at least the warehouses which are used. --- +-- -- The example mission is based on the same template mission, which has defined a lot of airborne, ground and naval assets as templates. Only few of those are used here. --- +-- -- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Assets.png) --- +-- -- ## Example 1: Self Request --- +-- -- Ground troops are taken from the Batumi warehouse stock and spawned in its spawn zone. After a short delay, they are added back to the warehouse stock. -- Also a new request is made. Hence, the groups will be spawned, added back to the warehouse, spawned again and so on and so forth... --- +-- -- -- Start warehouse Batumi. -- warehouse.Batumi:Start() --- +-- -- -- Add five groups of infantry as assets. -- warehouse.Batumi:AddAsset(GROUP:FindByName("Infantry Platoon Alpha"), 5) --- +-- -- -- Add self request for three infantry at Batumi. -- warehouse.Batumi:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 3) --- --- +-- +-- -- --- Self request event. Triggered once the assets are spawned in the spawn zone or at the airbase. -- function warehouse.Batumi:OnAfterSelfRequest(From, Event, To, groupset, request) -- local mygroupset=groupset --Core.Set#SET_GROUP --- +-- -- -- Loop over all groups spawned from that request. -- for _,group in pairs(mygroupset:GetSetObjects()) do -- local group=group --Wrapper.Group#GROUP --- +-- -- -- Gree smoke on spawned group. -- group:SmokeGreen() --- +-- -- -- Put asset back to stock after 10 seconds. --- warehouse.Batumi:__AddAsset(10, group) +-- warehouse.Batumi:__AddAsset(10, group) -- end --- +-- -- -- Add new self request after 20 seconds. -- warehouse.Batumi:__AddRequest(20, warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 3) --- +-- -- end -- -- ## Example 2: Self propelled Ground Troops --- +-- -- Warehouse Berlin, which is a FARP near Batumi, requests infantry and troop transports from the warehouse at Batumi. -- The groups are spawned at Batumi and move by themselfs from Batumi to Berlin using the roads. -- Once the troops have arrived at Berlin, the troops are automatically added to the warehouse stock of Berlin. -- While on the road, Batumi has requested back two APCs from Berlin. Since Berlin does not have the assets in stock, -- the request is queued. After the troops have arrived, Berlin is sending back the APCs to Batumi. --- +-- -- -- Start Warehouse at Batumi. -- warehouse.Batumi:Start() --- +-- -- -- Add 20 infantry groups and ten APCs as assets at Batumi. -- warehouse.Batumi:AddAsset("Infantry Platoon Alpha", 20) -- warehouse.Batumi:AddAsset("TPz Fuchs", 10) --- --- -- Start Warehouse Berlin. +-- +-- -- Start Warehouse Berlin. -- warehouse.Berlin:Start() --- +-- -- -- Warehouse Berlin requests 10 infantry groups and 5 APCs from warehouse Batumi. -- warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 10) -- warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_APC, 5) --- +-- -- -- Request from Batumi for 2 APCs. Initially these are not in stock. When they become available, the request is executed. --- warehouse.Berlin:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_APC, 2) +-- warehouse.Berlin:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_APC, 2) -- -- ## Example 3: Self Propelled Airborne Assets --- +-- -- Warehouse Senaki receives a high priority request from Kutaisi for one Yak-52s. At the same time, Kobuleti requests half of -- all available Yak-52s. Request from Kutaisi is first executed and then Kobuleti gets half of the remaining assets. -- Additionally, London requests one third of all available UH-1H Hueys from Senaki. --- Once the units have arrived they are added to the stock of the receiving warehouses and can be used for further assignments. --- +-- Once the units have arrived they are added to the stock of the receiving warehouses and can be used for further assignments. +-- -- -- Start warehouses -- warehouse.Senaki:Start() -- warehouse.Kutaisi:Start() -- warehouse.Kobuleti:Start() -- warehouse.London:Start() --- +-- -- -- Add assets to Senaki warehouse. -- warehouse.Senaki:AddAsset("Yak-52", 10) -- warehouse.Senaki:AddAsset("Huey", 6) --- +-- -- -- Kusaisi requests 3 Yak-52 form Senaki while Kobuleti wants all the rest. -- warehouse.Senaki:AddRequest(warehouse.Kutaisi, WAREHOUSE.Descriptor.GROUPNAME, "Yak-52", 1, nil, nil, 10) -- warehouse.Senaki:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.GROUPNAME, "Yak-52", WAREHOUSE.Quantity.HALF, nil, nil, 70) --- +-- -- -- FARP London wants 1/3 of the six available Hueys. -- warehouse.Senaki:AddRequest(warehouse.London, WAREHOUSE.Descriptor.GROUPNAME, "Huey", WAREHOUSE.Quantity.THIRD) -- -- ## Example 4: Transport of Assets by APCs --- +-- -- Warehouse at FARP Berlin requests five infantry groups from Batumi. These assets shall be transported using two APC groups. --- Infantry and APC are spawned in the spawn zone at Batumi. The APCs have a cargo bay large enough to pick up four of the +-- Infantry and APC are spawned in the spawn zone at Batumi. The APCs have a cargo bay large enough to pick up four of the -- five infantry groups in the first run and will bring them to Berlin. There, they unboard and walk to the warehouse where they will be added to the stock. -- Meanwhile the APCs go back to Batumi and one will pick up the last remaining soldiers. --- Once the APCs have completed their mission, they return to Batumi and are added back to stock. --- +-- Once the APCs have completed their mission, they return to Batumi and are added back to stock. +-- -- -- Start Warehouse at Batumi. -- warehouse.Batumi:Start() --- --- -- Start Warehouse Berlin. +-- +-- -- Start Warehouse Berlin. -- warehouse.Berlin:Start() --- +-- -- -- Add 20 infantry groups and five APCs as assets at Batumi. -- warehouse.Batumi:AddAsset("Infantry Platoon Alpha", 20) -- warehouse.Batumi:AddAsset("TPz Fuchs", 5) --- +-- -- -- Warehouse Berlin requests 5 infantry groups from warehouse Batumi using 2 APCs for transport. -- warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 5, WAREHOUSE.TransportType.APC, 2) -- @@ -898,13 +901,13 @@ -- -- Start Warehouses. -- warehouse.Batumi:Start() -- warehouse.Berlin:Start() --- +-- -- -- Add 20 infantry groups as assets at Batumi. -- warehouse.Batumi:AddAsset("Infantry Platoon Alpha", 20) --- +-- -- -- Add five Hueys for transport. Note that a Huey in DCS is an attack and not a transport helo. So we force this attribute! -- warehouse.Batumi:AddAsset("Huey", 5, WAREHOUSE.Attribute.AIR_TRANSPORTHELO) --- +-- -- -- Warehouse Berlin requests 5 infantry groups from warehouse Batumi using all available helos for transport. -- warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 5, WAREHOUSE.TransportType.HELICOPTER, WAREHOUSE.Quantity.ALL) -- @@ -917,53 +920,53 @@ -- -- Start warehouses. -- warehouse.Batumi:Start() -- warehouse.Kobuleti:Start() --- +-- -- -- Add assets to Batumi warehouse. -- warehouse.Batumi:AddAsset("C-130", 1) -- warehouse.Batumi:AddAsset("TPz Fuchs", 3) --- +-- -- warehouse.Batumi:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_APC, WAREHOUSE.Quantity.ALL, WAREHOUSE.TransportType.AIRPLANE) -- -- ## Example 7: Capturing Airbase and Warehouse --- +-- -- A red BMP has made it through our defence lines and drives towards our unprotected airbase at Senaki. --- Once the BMP captures the airbase (DCS [S\_EVENT\_BASE\_CAPTURED](https://wiki.hoggitworld.com/view/DCS_event_base_captured) is evaluated) +-- Once the BMP captures the airbase (DCS [S\_EVENT\_BASE\_CAPTURED](https://wiki.hoggitworld.com/view/DCS_event_base_captured) is evaluated) -- the warehouse at Senaki lost its air infrastructure and it is not possible any more to spawn airborne units. All requests for airborne units are rejected and cancelled in this case. --- +-- -- The red BMP then drives further to the warehouse. Once it enters the warehouse zone (500 m radius around the warehouse building), the warehouse is -- considered to be under attack. This triggers the event **Attacked**. The @{#WAREHOUSE.OnAfterAttacked} function can be used to react to this situation. -- Here, we only broadcast a distress call and launch a flare. However, it would also be reasonable to spawn all or selected ground troops in order to defend -- the warehouse. Note, that the warehouse has a self defence option which can be activated via the @{#WAREHOUSE.SetAutoDefenceOn}() function. If activated, -- *all* ground assets are automatically spawned and assigned to defend the warehouse. Once/if the attack is defeated, these assets go automatically back -- into the warehouse stock. --- --- If the red coalition manages to capture our warehouse, all assets go into their possession. Now red tries to steal three F/A-18 flights and send them to +-- +-- If the red coalition manages to capture our warehouse, all assets go into their possession. Now red tries to steal three F/A-18 flights and send them to -- Sukhumi. These aircraft will be spawned and begin to taxi. However, ... --- --- A blue Bradley is in the area and will attemt to recapture the warehouse. It might also catch the red F/A-18s before they take off. --- --- -- Start warehouses. +-- +-- A blue Bradley is in the area and will attemt to recapture the warehouse. It might also catch the red F/A-18s before they take off. +-- +-- -- Start warehouses. -- warehouse.Senaki:Start() -- warehouse.Sukhumi:Start() --- +-- -- -- Add some assets. -- warehouse.Senaki:AddAsset("TPz Fuchs", 5) -- warehouse.Senaki:AddAsset("Infantry Platoon Alpha", 10) -- warehouse.Senaki:AddAsset("F/A-18C 2ship", 10) --- +-- -- -- Enable auto defence, i.e. spawn all group troups into the spawn zone. -- --warehouse.Senaki:SetAutoDefenceOn() --- +-- -- -- Activate Red BMP trying to capture the airfield and the warehouse. -- local red1=GROUP:FindByName("Red BMP-80 Senaki"):Activate() --- +-- -- -- The red BMP first drives to the airbase which gets captured and changes from blue to red. --- -- This triggers the "AirbaseCaptured" event where you can hook in and do things. +-- -- This triggers the "AirbaseCaptured" event where you can hook in and do things. -- function warehouse.Senaki:OnAfterAirbaseCaptured(From, Event, To, Coalition) -- -- This request cannot be processed since the warehouse has lost its airbase. In fact it is deleted from the queue. -- warehouse.Senaki:AddRequest(warehouse.Senaki,WAREHOUSE.Descriptor.CATEGORY, Group.Category.AIRPLANE, 1) -- end --- +-- -- -- Now the red BMP also captures the warehouse. This triggers the "Captured" event where you can hook in. -- -- So now the warehouse and the airbase are both red and aircraft can be spawned again. -- function warehouse.Senaki:OnAfterCaptured(From, Event, To, Coalition, Country) @@ -976,63 +979,63 @@ -- elseif Coalition==coalition.side.BLUE then -- warehouse.Senaki.warehouse:SmokeBlue() -- end --- +-- -- -- Activate a blue vehicle to re-capture the warehouse. It will drive to the warehouse zone and kill the red intruder. -- local blue1=GROUP:FindByName("blue1"):Activate() -- end -- -- ## Example 8: Destroying a Warehouse --- +-- -- FARP Berlin requests a Huey from Batumi warehouse. This helo is deployed and will be delivered. -- After 30 seconds into the mission we create and (artificial) big explosion - or a terrorist attack if you like - which completely destroys the -- the warehouse at Batumi. All assets are gone and requests cannot be processed anymore. --- +-- -- -- Start Batumi and Berlin warehouses. -- warehouse.Batumi:Start() -- warehouse.Berlin:Start() --- +-- -- -- Add some assets. -- warehouse.Batumi:AddAsset("Huey", 5, WAREHOUSE.Attribute.AIR_TRANSPORTHELO) -- warehouse.Berlin:AddAsset("Huey", 5, WAREHOUSE.Attribute.AIR_TRANSPORTHELO) --- +-- -- -- Big explosion at the warehose. It has a very nice damage model by the way :) -- local function DestroyWarehouse() -- warehouse.Batumi:GetCoordinate():Explosion(999) -- end -- SCHEDULER:New(nil, DestroyWarehouse, {}, 30) --- +-- -- -- First request is okay since warehouse is still alive. -- warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.AIR_TRANSPORTHELO, 1) --- --- -- These requests should both not be processed any more since the warehouse at Batumi is destroyed. +-- +-- -- These requests should both not be processed any more since the warehouse at Batumi is destroyed. -- warehouse.Batumi:__AddRequest(35, warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.AIR_TRANSPORTHELO, 1) -- warehouse.Berlin:__AddRequest(40, warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.AIR_TRANSPORTHELO, 1) -- -- ## Example 9: Self Propelled Naval Assets --- +-- -- Kobuleti requests all naval assets from Batumi. -- However, before naval assets can be exchanged, both warehouses need a port and at least one shipping lane defined by the user. -- See the @{#WAREHOUSE.SetPortZone}() and @{#WAREHOUSE.AddShippingLane}() functions. -- We do not want to spawn them all at once, because this will probably be a disaster -- in the port zone. Therefore, each ship is spawned with a delay of five minutes. --- +-- -- Batumi has quite a selection of different ships (for testing). --- +-- -- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Naval_Assets.png) --- +-- -- -- Start warehouses. -- warehouse.Batumi:Start() -- warehouse.Kobuleti:Start() --- +-- -- -- Define ports. These are polygon zones created by the waypoints of late activated units. -- warehouse.Batumi:SetPortZone(ZONE_POLYGON:NewFromGroupName("Warehouse Batumi Port Zone", "Warehouse Batumi Port Zone")) -- warehouse.Kobuleti:SetPortZone(ZONE_POLYGON:NewFromGroupName("Warehouse Kobuleti Port Zone", "Warehouse Kobuleti Port Zone")) --- +-- -- -- Shipping lane. Again, the waypoints of late activated units are taken as points defining the shipping lane. -- -- Some units will take lane 1 while others will take lane two. But both lead from Batumi to Kobuleti port. -- warehouse.Batumi:AddShippingLane(warehouse.Kobuleti, GROUP:FindByName("Warehouse Batumi-Kobuleti Shipping Lane 1")) -- warehouse.Batumi:AddShippingLane(warehouse.Kobuleti, GROUP:FindByName("Warehouse Batumi-Kobuleti Shipping Lane 2")) --- +-- -- -- Large selection of available naval units in DCS. -- warehouse.Batumi:AddAsset("Speedboat") -- warehouse.Batumi:AddAsset("Perry") @@ -1055,11 +1058,11 @@ -- warehouse.Batumi:AddAsset("Ivanov") -- warehouse.Batumi:AddAsset("Yantai") -- warehouse.Batumi:AddAsset("Type 052C") --- warehouse.Batumi:AddAsset("Guangzhou") --- +-- warehouse.Batumi:AddAsset("Guangzhou") +-- -- -- Get Number of ships at Batumi. -- local nships=warehouse.Batumi:GetNumberOfAssets(WAREHOUSE.Descriptor.CATEGORY, Group.Category.SHIP) --- +-- -- -- Send one ship every 3 minutes (ships do not evade each other well, so we need a bit space between them). -- for i=1, nships do -- warehouse.Batumi:__AddRequest(180*(i-1)+10, warehouse.Kobuleti, WAREHOUSE.Descriptor.CATEGORY, Group.Category.SHIP, 1) @@ -1067,129 +1070,129 @@ -- -- ## Example 10: Warehouse on Aircraft Carrier -- --- This example shows how to spawn assets from a warehouse located on an aircraft carrier. The warehouse must still be represented by a --- physical static object. However, on a carrier space is limit so we take a smaller static. In priciple one could also take something +-- This example shows how to spawn assets from a warehouse located on an aircraft carrier. The warehouse must still be represented by a +-- physical static object. However, on a carrier space is limit so we take a smaller static. In priciple one could also take something -- like a windsock. --- +-- -- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Carrier.png) --- +-- -- USS Stennis requests F/A-18s from Batumi. At the same time Kobuleti requests F/A-18s from the Stennis which currently does not have any. -- So first, Batumi delivers the fighters to the Stennis. After they arrived they are deployed again and send to Kobuleti. --- +-- -- -- Start warehouses. --- warehouse.Batumi:Start() +-- warehouse.Batumi:Start() -- warehouse.Stennis:Start() -- warehouse.Kobuleti:Start() --- +-- -- -- Add F/A-18 2-ship flight to Batmi. -- warehouse.Batumi:AddAsset("F/A-18C 2ship", 1) --- +-- -- -- USS Stennis requests F/A-18 from Batumi. -- warehouse.Batumi:AddRequest(warehouse.Stennis, WAREHOUSE.Descriptor.GROUPNAME, "F/A-18C 2ship") --- +-- -- -- Kobuleti requests F/A-18 from USS Stennis. -- warehouse.Stennis:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.GROUPNAME, "F/A-18C 2ship") -- -- ## Example 11: Aircraft Carrier - Rescue Helo and Escort --- +-- -- After 10 seconds we make a self request for a rescue helicopter. Note, that the @{#WAREHOUSE.AddRequest} function has a parameter which lets you -- specify an "Assignment". This can be later used to identify the request and take the right actions. --- +-- -- Once the request is processed, the @{#WAREHOUSE.OnAfterSelfRequest} function is called. This is where we hook in and postprocess the spawned assets. -- In particular, we use the @{AI.AI_Formation#AI_FORMATION} class to make some nice escorts for our carrier. --- +-- -- When the resue helo is spawned, we can check that this is the correct asset and make the helo go into formation with the carrier. -- Once the helo runs out of fuel, it will automatically return to the ship and land. For the warehouse, this means that the "cargo", i.e. the helicopter -- has been delivered - assets can be delivered to other warehouses and to the same warehouse - hence a *self* request. -- When that happens, the **Delivered** event is triggered and the @{#WAREHOUSE.OnAfterDelivered} function called. This can now be used to spawn -- a fresh helo. Effectively, there we created an infinite, never ending loop. So a rescue helo will be up at all times. --- +-- -- After 30 and 45 seconds requests for five groups of armed speedboats are made. These will be spawned in the port zone right behind the carrier. -- The first five groups will go port of the carrier an form a left wing formation. The seconds groups will to the analogue on the starboard side. -- **Note** that in order to spawn naval assets a warehouse needs a port (zone). Since the carrier and hence the warehouse is mobile, we define a moving -- zone as @{Core.Zone#ZONE_UNIT} with the carrier as reference unit. The "port" of the Stennis at its stern so all naval assets are spawned behing the carrier. --- +-- -- -- Start warehouse on USS Stennis. -- warehouse.Stennis:Start() --- +-- -- -- Aircraft carrier gets a moving zone right behind it as port. -- warehouse.Stennis:SetPortZone(ZONE_UNIT:New("Warehouse Stennis Port Zone", UNIT:FindByName("USS Stennis"), 100, {rho=250, theta=180, relative_to_unit=true})) --- +-- -- -- Add speedboat assets. -- warehouse.Stennis:AddAsset("Speedboat", 10) -- warehouse.Stennis:AddAsset("CH-53E", 1) --- +-- -- -- Self request of speed boats. -- warehouse.Stennis:__AddRequest(10, warehouse.Stennis, WAREHOUSE.Descriptor.GROUPNAME, "CH-53E", 1, nil, nil, nil, "Rescue Helo") -- warehouse.Stennis:__AddRequest(30, warehouse.Stennis, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.NAVAL_ARMEDSHIP, 5, nil, nil, nil, "Speedboats Left") -- warehouse.Stennis:__AddRequest(45, warehouse.Stennis, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.NAVAL_ARMEDSHIP, 5, nil, nil, nil, "Speedboats Right") --- +-- -- --- Function called after self request --- function warehouse.Stennis:OnAfterSelfRequest(From, Event, To,_groupset, request) +-- function warehouse.Stennis:OnAfterSelfRequest(From, Event, To,_groupset, request) -- local groupset=_groupset --Core.Set#SET_GROUP -- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem --- +-- -- -- USS Stennis is the mother ship. -- local Mother=UNIT:FindByName("USS Stennis") --- +-- -- -- Get assignment of the request. -- local assignment=warehouse.Stennis:GetAssignment(request) --- +-- -- if assignment=="Speedboats Left" then --- +-- -- -- Define AI Formation object. -- -- Note that this has to be a global variable or the garbage collector will remove it for some reason! -- CarrierFormationLeft = AI_FORMATION:New(Mother, groupset, "Left Formation with Carrier", "Escort Carrier.") --- +-- -- -- Formation parameters. --- CarrierFormationLeft:FormationLeftWing(200 ,50, 0, 0, 500, 50) +-- CarrierFormationLeft:FormationLeftWing(200 ,50, 0, 0, 500, 50) -- CarrierFormationLeft:__Start(2) --- +-- -- for _,group in pairs(groupset:GetSetObjects()) do -- local group=group --Wrapper.Group#GROUP --- group:FlareRed() --- end --- +-- group:FlareRed() +-- end +-- -- elseif assignment=="Speedboats Right" then --- +-- -- -- Define AI Formation object. -- -- Note that this has to be a global variable or the garbage collector will remove it for some reason! -- CarrierFormationRight = AI_FORMATION:New(Mother, groupset, "Right Formation with Carrier", "Escort Carrier.") --- +-- -- -- Formation parameters. --- CarrierFormationRight:FormationRightWing(200 ,50, 0, 0, 500, 50) +-- CarrierFormationRight:FormationRightWing(200 ,50, 0, 0, 500, 50) -- CarrierFormationRight:__Start(2) --- +-- -- for _,group in pairs(groupset:GetSetObjects()) do -- local group=group --Wrapper.Group#GROUP --- group:FlareGreen() --- end --- +-- group:FlareGreen() +-- end +-- -- elseif assignment=="Rescue Helo" then --- +-- -- -- Start uncontrolled helo. -- local group=groupset:GetFirst() --Wrapper.Group#GROUP -- group:StartUncontrolled() --- +-- -- -- Define AI Formation object. -- CarrierFormationHelo = AI_FORMATION:New(Mother, groupset, "Helo Formation with Carrier", "Fly Formation.") --- +-- -- -- Formation parameters. -- CarrierFormationHelo:FormationCenterWing(-150, 50, 20, 50, 100, 50) -- CarrierFormationHelo:__Start(2) --- +-- -- end --- +-- -- --- When the helo is out of fuel, it will return to the carrier and should be delivered. -- function warehouse.Stennis:OnAfterDelivered(From,Event,To,request) -- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem --- +-- -- -- So we start another request. -- if request.assignment=="Rescue Helo" then -- warehouse.Stennis:__AddRequest(10, warehouse.Stennis, WAREHOUSE.Descriptor.GROUPNAME, "CH-53E", 1, nil, nil, nil, "Rescue Helo") -- end -- end --- +-- -- end -- -- ## Example 12: Pause a Warehouse @@ -1208,72 +1211,72 @@ -- -- -- Start Warehouse at Batumi. -- warehouse.Batumi:Start() --- --- -- Start Warehouse Berlin. +-- +-- -- Start Warehouse Berlin. -- warehouse.Berlin:Start() --- +-- -- -- Add 20 infantry groups and 5 tank platoons as assets at Batumi. -- warehouse.Batumi:AddAsset("Infantry Platoon Alpha", 20) --- +-- -- -- Pause the warehouse after 10 seconds -- warehouse.Batumi:__Pause(10) --- +-- -- -- Add a request from Berlin after 15 seconds. A request can be added but not be processed while warehouse is paused. -- warehouse.Batumi:__AddRequest(15, warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 1) --- +-- -- -- New asset added after 20 seconds. This is possible even if the warehouse is paused. -- warehouse.Batumi:__AddAsset(20, "Abrams", 5) --- +-- -- -- Unpause warehouse after 30 seconds. Now the request from Berlin can be processed. -- warehouse.Batumi:__Unpause(30) --- +-- -- -- Pause warehouse Berlin -- warehouse.Berlin:__Pause(60) --- +-- -- -- After 90 seconds request from Berlin for tanks. -- warehouse.Batumi:__AddRequest(90, warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_TANK, 1) --- +-- -- -- After 120 seconds unpause Berlin. -- warehouse.Berlin:__Unpause(120) -- -- ## Example 13: Battlefield Air Interdiction --- +-- -- This example show how to couple the WAREHOUSE class with the @{AI.AI_Bai} class. --- Four enemy targets have been located at the famous Kobuleti X. All three available Viggen 2-ship flights are assigned to kill at least one of the BMPs to complete their mission. +-- Four enemy targets have been located at the famous Kobuleti X. All three available Viggen 2-ship flights are assigned to kill at least one of the BMPs to complete their mission. -- -- -- Start Warehouse at Kobuleti. -- warehouse.Kobuleti:Start() --- +-- -- -- Add three 2-ship groups of Viggens. -- warehouse.Kobuleti:AddAsset("Viggen 2ship", 3) --- +-- -- -- Self request for all Viggen assets. -- warehouse.Kobuleti:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.GROUPNAME, "Viggen 2ship", WAREHOUSE.Quantity.ALL, nil, nil, nil, "BAI") --- +-- -- -- Red targets at Kobuleti X (late activated). -- local RedTargets=GROUP:FindByName("Red IVF Alpha") --- +-- -- -- Activate the targets. -- RedTargets:Activate() --- +-- -- -- Do something with the spawned aircraft. -- function warehouse.Kobuleti:OnAfterSelfRequest(From,Event,To,groupset,request) -- local groupset=groupset --Core.Set#SET_GROUP -- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem --- +-- -- if request.assignment=="BAI" then --- +-- -- for _,group in pairs(groupset:GetSetObjects()) do -- local group=group --Wrapper.Group#GROUP --- +-- -- -- Start uncontrolled aircraft. -- group:StartUncontrolled() --- +-- -- local BAI=AI_BAI_ZONE:New(ZONE:New("Patrol Zone Kobuleti"), 500, 1000, 500, 600, ZONE:New("Patrol Zone Kobuleti")) --- +-- -- -- Tell the program to use the object (in this case called BAIPlane) as the group to use in the BAI function -- BAI:SetControllable(group) --- +-- -- -- Function checking if targets are still alive -- local function CheckTargets() -- local nTargets=RedTargets:GetSize() @@ -1285,76 +1288,76 @@ -- else -- MESSAGE:New("BAI Mission: The required red targets are destroyed.", 30):ToAll() -- BAI:__Accomplish(1) -- Now they should fly back to the patrolzone and patrol. --- end +-- end -- end --- +-- -- -- Start scheduler to monitor number of targets. -- local Check, CheckScheduleID = SCHEDULER:New(nil, CheckTargets, {}, 60, 60) --- +-- -- -- When the targets in the zone are destroyed, (see scheduled function), the planes will return home ... -- function BAI:OnAfterAccomplish( Controllable, From, Event, To ) -- MESSAGE:New( "BAI Mission: Sending the Viggens back to base.", 30):ToAll() -- Check:Stop(CheckScheduleID) -- BAI:__RTB(1) -- end --- +-- -- -- Start BAI -- BAI:Start() --- +-- -- -- Engage after 5 minutes. -- BAI:__Engage(300) --- +-- -- -- RTB after 30 min max. -- BAI:__RTB(-30*60) --- +-- -- end -- end --- +-- -- end -- -- ## Example 14: Strategic Bombing --- +-- -- This example shows how to employ stategic bombers in a mission. Three B-52s are lauched at Kobuleti with the assignment to wipe out the enemy warehouse at Sukhumi. -- The bombers will get a flight path and make their approach from the South at an altitude of 5000 m ASL. After their bombing run, they will return to Kobuleti and -- added back to stock. --- +-- -- -- Start warehouses --- warehouse.Kobuleti:Start() +-- warehouse.Kobuleti:Start() -- warehouse.Sukhumi:Start() --- +-- -- -- Add a strategic bomber assets -- warehouse.Kobuleti:AddAsset("B-52H", 3) --- --- -- Request bombers for specific task of bombing Sukhumi warehouse. +-- +-- -- Request bombers for specific task of bombing Sukhumi warehouse. -- warehouse.Kobuleti:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.AIR_BOMBER, WAREHOUSE.Quantity.ALL, nil, nil, nil, "Bomb Sukhumi") --- --- -- Specify assignment after bombers have been spawned. +-- +-- -- Specify assignment after bombers have been spawned. -- function warehouse.Kobuleti:OnAfterSelfRequest(From, Event, To, groupset, request) -- local groupset=groupset --Core.Set#SET_GROUP --- +-- -- -- Get assignment of this request. -- local assignment=warehouse.Kobuleti:GetAssignment(request) --- +-- -- if assignment=="Bomb Sukhumi" then --- +-- -- for _,_group in pairs(groupset:GetSet()) do -- local group=_group --Wrapper.Group#GROUP --- +-- -- -- Start uncontrolled aircraft. -- group:StartUncontrolled() --- +-- -- -- Target coordinate! -- local ToCoord=warehouse.Sukhumi:GetCoordinate():SetAltitude(5000) --- +-- -- -- Home coordinate. -- local HomeCoord=warehouse.Kobuleti:GetCoordinate():SetAltitude(3000) --- +-- -- -- Task bomb Sukhumi warehouse using all bombs (2032) from direction 180 at altitude 5000 m. -- local task=group:TaskBombing(warehouse.Sukhumi:GetCoordinate():GetVec2(), false, "All", nil , 180, 5000, 2032) --- --- -- Define waypoints. +-- +-- -- Define waypoints. -- local WayPoints={} --- +-- -- -- Take off position. -- WayPoints[1]=warehouse.Kobuleti:GetCoordinate():WaypointAirTakeOffParking() -- -- Begin bombing run 20 km south of target. @@ -1363,16 +1366,16 @@ -- WayPoints[3]=HomeCoord:WaypointAirTurningPoint() -- -- Land at homebase. Bombers are added back to stock and can be employed in later assignments. -- WayPoints[4]=warehouse.Kobuleti:GetCoordinate():WaypointAirLanding() --- +-- -- -- Route bombers. -- group:Route(WayPoints) -- end --- +-- -- end -- end -- -- ## Example 15: Defining Off-Road Paths --- +-- -- For self propelled assets it is possible to define custom off-road paths from one warehouse to another via the @{#WAREHOUSE.AddOffRoadPath} function. -- The waypoints of a path are taken from late activated units. In this example, two paths have been defined between the warehouses Kobuleti and FARP London. -- Trucks are spawned at each warehouse and are guided along the paths to the other warehouse. @@ -1381,21 +1384,21 @@ -- -- Start warehouses -- warehouse.Kobuleti:Start() -- warehouse.London:Start() --- +-- -- -- Define a polygon zone as spawn zone at Kobuleti. -- warehouse.Kobuleti:SetSpawnZone(ZONE_POLYGON:New("Warehouse Kobuleti Spawn Zone", GROUP:FindByName("Warehouse Kobuleti Spawn Zone"))) --- +-- -- -- Add assets. -- warehouse.Kobuleti:AddAsset("M978", 20) -- warehouse.London:AddAsset("M818", 20) --- +-- -- -- Off two road paths from Kobuleti to London. The reverse path from London to Kobuleti is added automatically. -- warehouse.Kobuleti:AddOffRoadPath(warehouse.London, GROUP:FindByName("Warehouse Kobuleti-London OffRoad Path 1")) -- warehouse.Kobuleti:AddOffRoadPath(warehouse.London, GROUP:FindByName("Warehouse Kobuleti-London OffRoad Path 2")) --- --- -- London requests all available trucks from Kobuleti. +-- +-- -- London requests all available trucks from Kobuleti. -- warehouse.Kobuleti:AddRequest(warehouse.London, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_TRUCK, WAREHOUSE.Quantity.ALL) --- +-- -- -- Kobuleti requests all available trucks from London. -- warehouse.London:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_TRUCK, WAREHOUSE.Quantity.HALF) -- @@ -1404,62 +1407,62 @@ -- Warehouse at FARP Berlin is located at the front line and sends infantry groups to the battle zone. -- Whenever a group dies, a new group is send from the warehouse to the battle zone. -- Additionally, for each dead group, Berlin requests resupply from Batumi. --- +-- -- -- Start warehouses. -- warehouse.Batumi:Start() -- warehouse.Berlin:Start() --- +-- -- -- Front line warehouse. -- warehouse.Berlin:AddAsset("Infantry Platoon Alpha", 6) --- +-- -- -- Resupply warehouse. -- warehouse.Batumi:AddAsset("Infantry Platoon Alpha", 50) --- +-- -- -- Battle zone near FARP Berlin. This is where the action is! -- local BattleZone=ZONE:New("Virtual Battle Zone") --- +-- -- -- Send infantry groups to the battle zone. Two groups every ~60 seconds. -- for i=1,2 do -- local time=(i-1)*60+10 -- warehouse.Berlin:__AddRequest(time, warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 2, nil, nil, nil, "To Battle Zone") -- end --- +-- -- -- Take care of the spawned units. -- function warehouse.Berlin:OnAfterSelfRequest(From,Event,To,groupset,request) -- local groupset=groupset --Core.Set#SET_GROUP -- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem --- +-- -- -- Get assignment of this request. -- local assignment=warehouse.Berlin:GetAssignment(request) --- +-- -- if assignment=="To Battle Zone" then --- +-- -- for _,group in pairs(groupset:GetSet()) do -- local group=group --Wrapper.Group#GROUP --- +-- -- -- Route group to Battle zone. -- local ToCoord=BattleZone:GetRandomCoordinate() -- group:RouteGroundOnRoad(ToCoord, group:GetSpeedMax()*0.8) --- +-- -- -- After 3-5 minutes we create an explosion to destroy the group. -- SCHEDULER:New(nil, Explosion, {group, 50}, math.random(180, 300)) -- end --- +-- -- end --- +-- -- end --- +-- -- -- An asset has died ==> request resupply for it. -- function warehouse.Berlin:OnAfterAssetDead(From, Event, To, asset, request) -- local asset=asset --Functional.Warehouse#WAREHOUSE.Assetitem -- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem --- +-- -- -- Get assignment. -- local assignment=warehouse.Berlin:GetAssignment(request) --- +-- -- -- Request resupply for dead asset from Batumi. -- warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, asset.attribute, nil, nil, nil, nil, "Resupply") --- +-- -- -- Send asset to Battle zone either now or when they arrive. -- warehouse.Berlin:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, asset.attribute, 1, nil, nil, nil, assignment) -- end @@ -1473,12 +1476,12 @@ -- Once infantry has arrived at Batumi, it will walk by itself to warehouse Pampa. -- The mortars can only be transported once the Mi-8 helos are available again, i.e. when the infantry has been delivered. -- Once the mortars arrive at Batumi, they will be transported by APCs to Pampa. --- +-- -- -- Start warehouses. -- warehouse.Kobuleti:Start() -- warehouse.Batumi:Start() -- warehouse.Pampa:Start() --- +-- -- -- Add assets to Kobuleti warehouse, which is our main hub. -- warehouse.Kobuleti:AddAsset("C-130", 2) -- warehouse.Kobuleti:AddAsset("C-17A", 2, nil, 77000) @@ -1486,32 +1489,32 @@ -- warehouse.Kobuleti:AddAsset("Leopard 2", 10, nil, nil, 62000, 500) -- warehouse.Kobuleti:AddAsset("Mortar Alpha", 10, nil, nil, 210) -- warehouse.Kobuleti:AddAsset("Infantry Platoon Alpha", 20) --- +-- -- -- Transports at Batumi. -- warehouse.Batumi:AddAsset("SPz Marder", 2) -- warehouse.Batumi:AddAsset("TPz Fuchs", 2) --- +-- -- -- Tanks transported by plane from from Kobuleti to Batumi. -- warehouse.Kobuleti:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_TANK, 2, WAREHOUSE.TransportType.AIRPLANE, 2, 10, "Assets for Pampa") -- -- Artillery transported by helicopter from Kobuleti to Batumi. -- warehouse.Kobuleti:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_ARTILLERY, 2, WAREHOUSE.TransportType.HELICOPTER, 2, 30, "Assets for Pampa via APC") -- -- Infantry transported by helicopter from Kobuleti to Batumi. -- warehouse.Kobuleti:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 8, WAREHOUSE.TransportType.HELICOPTER, 2, 20, "Assets for Pampa") --- +-- -- --- Function handling assets delivered from Kobuleti warehouse. -- function warehouse.Kobuleti:OnAfterDelivered(From, Event, To, request) -- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem --- +-- -- -- Get assignment. -- local assignment=warehouse.Kobuleti:GetAssignment(request) --- +-- -- -- Check if these assets were meant for Warehouse Pampa. -- if assignment=="Assets for Pampa via APC" then -- -- Forward everything that arrived at Batumi to Pampa via APC. -- warehouse.Batumi:AddRequest(warehouse.Pampa, WAREHOUSE.Descriptor.ATTRIBUTE, request.cargoattribute, request.ndelivered, WAREHOUSE.TransportType.APC, WAREHOUSE.Quantity.ALL) -- end -- end --- +-- -- -- Forward all mobile ground assets to Pampa once they arrived. -- function warehouse.Batumi:OnAfterNewAsset(From, Event, To, asset, assignment) -- local asset=asset --Functional.Warehouse#WAREHOUSE.Assetitem @@ -1555,6 +1558,8 @@ WAREHOUSE = { autosave = false, autosavepath = nil, autosavefile = nil, + saveparking = false, + isunit = false, } --- Item of the warehouse stock table. @@ -1701,7 +1706,7 @@ WAREHOUSE.TransportType = { --- Warehouse quantity enumerator for selecting number of assets, e.g. all, half etc. of what is in stock rather than an absolute number. -- @type WAREHOUSE.Quantity -- @field #string ALL All "all" assets currently in stock. --- @field #string THREEQUARTERS Three quarters "3/4" of assets in stock. +-- @field #string THREEQUARTERS Three quarters "3/4" of assets in stock. -- @field #string HALF Half "1/2" of assets in stock. -- @field #string THIRD One third "1/3" of assets in stock. -- @field #string QUARTER One quarter "1/4" of assets in stock. @@ -1716,17 +1721,19 @@ WAREHOUSE.Quantity = { --- Warehouse database. Note that this is a global array to have easier exchange between warehouses. -- @type WAREHOUSE.db -- @field #number AssetID Unique ID of each asset. This is a running number, which is increased each time a new asset is added. --- @field #table Assets Table holding registered assets, which are of type @{Functional.Warehouse#WAREHOUSE.Assetitem}. +-- @field #table Assets Table holding registered assets, which are of type @{Functional.Warehouse#WAREHOUSE.Assetitem}.# +-- @field #number WarehouseID Unique ID of the warehouse. Running number. -- @field #table Warehouses Table holding all defined @{#WAREHOUSE} objects by their unique ids. WAREHOUSE.db = { - AssetID = 0, - Assets = {}, - Warehouses = {} + AssetID = 0, + Assets = {}, + WarehouseID = 0, + Warehouses = {} } --- Warehouse class version. -- @field #string version -WAREHOUSE.version="0.6.4" +WAREHOUSE.version="0.6.8" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Warehouse todo list. @@ -1735,12 +1742,12 @@ WAREHOUSE.version="0.6.4" -- TODO: Add check if assets "on the move" are stationary. Can happen if ground units get stuck in buildings. If stationary auto complete transport by adding assets to request warehouse? Time? -- TODO: Optimize findpathonroad. Do it only once (first time) and safe paths between warehouses similar to off-road paths. -- TODO: Spawn assets only virtually, i.e. remove requested assets from stock but do NOT spawn them ==> Interface to A2A dispatcher! Maybe do a negative sign on asset number? --- TODO: Test capturing a neutral warehouse. -- TODO: Make more examples: ARTY, CAP, ... -- TODO: Check also general requests like all ground. Is this a problem for self propelled if immobile units are among the assets? Check if transport. -- TODO: Handle the case when units of a group die during the transfer. -- TODO: Added habours as interface for transport to from warehouses? Could make a rudimentary shipping dispatcher. --- TODO: Add save/load capability of warehouse <==> percistance after mission restart. Difficult in lua! +-- DONE: Test capturing a neutral warehouse. +-- DONE: Add save/load capability of warehouse <==> percistance after mission restart. Difficult in lua! -- DONE: Get cargo bay and weight from CARGO_GROUP and GROUP. No necessary any more! -- DONE: Add possibility to set weight and cargo bay manually in AddAsset function as optional parameters. -- DONE: Check overlapping aircraft sometimes. @@ -1759,7 +1766,7 @@ WAREHOUSE.version="0.6.4" -- DONE: Warehouse re-capturing not working?! -- DONE: Naval assets dont go back into stock once arrived. -- DONE: Take cargo weight into consideration, when selecting transport assets. --- DONE: Add ports for spawning naval assets. +-- DONE: Add ports for spawning naval assets. -- DONE: Add shipping lanes between warehouses. -- DONE: Handle cases with immobile units <== should be handled by dispatcher classes. -- DONE: Handle cases for aircraft carriers and other ships. Place warehouse on carrier possible? On others probably not - exclude them? @@ -1787,23 +1794,31 @@ WAREHOUSE.version="0.6.4" --- The WAREHOUSE constructor. Creates a new WAREHOUSE object from a static object. Parameters like the coalition and country are taken from the static object structure. -- @param #WAREHOUSE self --- @param Wrapper.Static#STATIC warehouse The physical structure of the warehouse. --- @param #string alias (Optional) Alias of the warehouse, i.e. the name it will be called when sending messages etc. Default is the name of the static +-- @param Wrapper.Static#STATIC warehouse The physical structure representing the warehouse. +-- @param #string alias (Optional) Alias of the warehouse, i.e. the name it will be called when sending messages etc. Default is the name of the static -- @return #WAREHOUSE self function WAREHOUSE:New(warehouse, alias) BASE:T({warehouse=warehouse}) - + -- Check if just a string was given and convert to static. if type(warehouse)=="string" then - warehouse=STATIC:FindByName(warehouse, true) + local warehousename=warehouse + warehouse=UNIT:FindByName(warehousename) + if warehouse==nil then + env.info(string.format("No warehouse unit with name %s found trying static.", tostring(warehousename))) + warehouse=STATIC:FindByName(warehousename, true) + self.isunit=false + else + self.isunit=true + end end - + -- Nil check. if warehouse==nil then BASE:E("ERROR: Warehouse does not exist!") return nil end - + -- Set alias. self.alias=alias or warehouse:GetName() @@ -1818,25 +1833,33 @@ function WAREHOUSE:New(warehouse, alias) -- Set some variables. self.warehouse=warehouse - self.uid=tonumber(warehouse:GetID()) + + -- Increase global warehouse counter. + WAREHOUSE.db.WarehouseID=WAREHOUSE.db.WarehouseID+1 + + -- Set unique ID for this warehouse. + self.uid=WAREHOUSE.db.WarehouseID + + -- As Kalbuth found out, this would fail when using SPAWNSTATIC https://forums.eagle.ru/showthread.php?p=3703488#post3703488 + --self.uid=tonumber(warehouse:GetID()) -- Closest of the same coalition but within a certain range. local _airbase=self:GetCoordinate():GetClosestAirbase(nil, self:GetCoalition()) if _airbase and _airbase:GetCoordinate():Get2DDistance(self:GetCoordinate()) < 3000 then self:SetAirbase(_airbase) end - + -- Define warehouse and default spawn zone. self.zone=ZONE_RADIUS:New(string.format("Warehouse zone %s", self.warehouse:GetName()), warehouse:GetVec2(), 500) self.spawnzone=ZONE_RADIUS:New(string.format("Warehouse %s spawn zone", self.warehouse:GetName()), warehouse:GetVec2(), 250) - + -- Add warehouse to database. WAREHOUSE.db.Warehouses[self.uid]=self - + ----------------------- --- FSM Transitions --- ----------------------- - + -- Start State. self:SetStartState("NotReadyYet") @@ -1845,7 +1868,7 @@ function WAREHOUSE:New(warehouse, alias) self:AddTransition("NotReadyYet", "Load", "Loaded") -- Load the warehouse state from scatch. self:AddTransition("Stopped", "Load", "Loaded") -- Load the warehouse state stopped state. self:AddTransition("NotReadyYet", "Start", "Running") -- Start the warehouse from scratch. - self:AddTransition("Loaded", "Start", "Running") -- Start the warehouse when loaded from disk. + self:AddTransition("Loaded", "Start", "Running") -- Start the warehouse when loaded from disk. self:AddTransition("*", "Status", "*") -- Status update. self:AddTransition("*", "AddAsset", "*") -- Add asset to warehouse stock. self:AddTransition("*", "NewAsset", "*") -- New asset was added to warehouse stock. @@ -1857,25 +1880,25 @@ function WAREHOUSE:New(warehouse, alias) self:AddTransition("*", "Delivered", "*") -- All cargo groups of a request have been delivered to the requesting warehouse. self:AddTransition("Running", "SelfRequest", "*") -- Request to warehouse itself. Requested assets are only spawned but not delivered anywhere. self:AddTransition("Attacked", "SelfRequest", "*") -- Request to warehouse itself. Also possible when warehouse is under attack! - self:AddTransition("Running", "Pause", "Paused") -- Pause the processing of new requests. Still possible to add assets and requests. - self:AddTransition("Paused", "Unpause", "Running") -- Unpause the warehouse. Queued requests are processed again. + self:AddTransition("Running", "Pause", "Paused") -- Pause the processing of new requests. Still possible to add assets and requests. + self:AddTransition("Paused", "Unpause", "Running") -- Unpause the warehouse. Queued requests are processed again. self:AddTransition("*", "Stop", "Stopped") -- Stop the warehouse. self:AddTransition("Stopped", "Restart", "Running") -- Restart the warehouse when it was stopped before. self:AddTransition("Loaded", "Restart", "Running") -- Restart the warehouse when assets were loaded from file before. - self:AddTransition("*", "Save", "*") -- TODO Save the warehouse state to disk. + self:AddTransition("*", "Save", "*") -- Save the warehouse state to disk. self:AddTransition("*", "Attacked", "Attacked") -- Warehouse is under attack by enemy coalition. self:AddTransition("Attacked", "Defeated", "Running") -- Attack by other coalition was defeated! - self:AddTransition("*", "ChangeCountry", "*") -- Change country (and coalition) of the warehouse. Warehouse is respawned! + self:AddTransition("*", "ChangeCountry", "*") -- Change country (and coalition) of the warehouse. Warehouse is respawned! self:AddTransition("Attacked", "Captured", "Running") -- Warehouse was captured by another coalition. It must have been attacked first. self:AddTransition("*", "AirbaseCaptured", "*") -- Airbase was captured by other coalition. self:AddTransition("*", "AirbaseRecaptured", "*") -- Airbase was re-captured from other coalition. self:AddTransition("*", "AssetDead", "*") -- An asset group died. self:AddTransition("*", "Destroyed", "Destroyed") -- Warehouse was destroyed. All assets in stock are gone and warehouse is stopped. - + ------------------------ --- Pseudo Functions --- ------------------------ - + --- Triggers the FSM event "Start". Starts the warehouse. Initializes parameters and starts event handlers. -- @function [parent=#WAREHOUSE] Start -- @param #WAREHOUSE self @@ -2013,7 +2036,7 @@ function WAREHOUSE:New(warehouse, alias) -- @function [parent=#WAREHOUSE] Request -- @param #WAREHOUSE self -- @param #WAREHOUSE.Queueitem Request Information table of the request. - + --- Triggers the FSM event "Request" after a delay. Executes a request from the queue if possible. -- @function [parent=#WAREHOUSE] __Request -- @param #WAREHOUSE self @@ -2024,8 +2047,8 @@ function WAREHOUSE:New(warehouse, alias) --- Triggers the FSM event "Arrived" when a group has arrived at the destination warehouse. -- This function should always be called from the sending and not the receiving warehouse. -- If the group is a cargo asset, it is added to the receiving warehouse. If the group is a transporter it - -- is added to the sending warehouse since carriers are supposed to return to their home warehouse once - -- all cargo was delivered. + -- is added to the sending warehouse since carriers are supposed to return to their home warehouse once + -- all cargo was delivered. -- @function [parent=#WAREHOUSE] Arrived -- @param #WAREHOUSE self -- @param Wrapper.Group#GROUP group Group that has arrived. @@ -2033,7 +2056,7 @@ function WAREHOUSE:New(warehouse, alias) --- Triggers the FSM event "Arrived" after a delay when a group has arrived at the destination. -- This function should always be called from the sending and not the receiving warehouse. -- If the group is a cargo asset, it is added to the receiving warehouse. If the group is a transporter it - -- is added to the sending warehouse since carriers are supposed to return to their home warehouse once + -- is added to the sending warehouse since carriers are supposed to return to their home warehouse once -- @function [parent=#WAREHOUSE] __Arrived -- @param #WAREHOUSE self -- @param #number delay Delay in seconds. @@ -2088,24 +2111,24 @@ function WAREHOUSE:New(warehouse, alias) --- On after "SelfRequest" event. Request was initiated from the warehouse to itself. Groups are simply spawned at the warehouse or the associated airbase. -- All requested assets are passed as a @{Core.Set#SET_GROUP} and can be used for further tasks or in other MOOSE classes. -- Note that airborne assets are spawned in uncontrolled state so they do not simply "fly away" after spawning. - -- + -- -- @usage -- --- Self request event. Triggered once the assets are spawned in the spawn zone or at the airbase. -- function mywarehouse:OnAfterSelfRequest(From, Event, To, groupset, request) -- local groupset=groupset --Core.Set#SET_GROUP - -- + -- -- -- Loop over all groups spawned from that request. -- for _,group in pairs(groupset:GetSetObjects()) do -- local group=group --Wrapper.Group#GROUP - -- + -- -- -- Gree smoke on spawned group. -- group:SmokeGreen() - -- + -- -- -- Activate uncontrolled airborne group if necessary. -- group:StartUncontrolled() -- end - -- end - -- + -- end + -- -- @function [parent=#WAREHOUSE] OnAfterSelfRequest -- @param #WAREHOUSE self -- @param #string From From state. @@ -2159,7 +2182,7 @@ function WAREHOUSE:New(warehouse, alias) -- @function [parent=#WAREHOUSE] ChangeCountry -- @param #WAREHOUSE self -- @param DCS#country.id Country New country id of the warehouse. - + --- Triggers the FSM event "ChangeCountry" after a delay so the warehouse is respawned with the new country. -- @function [parent=#WAREHOUSE] __ChangeCountry -- @param #WAREHOUSE self @@ -2180,7 +2203,7 @@ function WAREHOUSE:New(warehouse, alias) -- @param #WAREHOUSE self -- @param DCS#coalition.side Coalition Coalition side which captured the warehouse. -- @param DCS#country.id Country Country id which has captured the warehouse. - + --- Triggers the FSM event "Captured" with a delay when a warehouse has been captured by another coalition. -- @function [parent=#WAREHOUSE] __Captured -- @param #WAREHOUSE self @@ -2196,13 +2219,13 @@ function WAREHOUSE:New(warehouse, alias) -- @param #string To To state. -- @param DCS#coalition.side Coalition Coalition side which captured the warehouse, i.e. a number of @{DCS#coalition.side} enumerator. -- @param DCS#country.id Country Country id which has captured the warehouse, i.e. a number @{DCS#country.id} enumerator. - -- + -- --- Triggers the FSM event "AirbaseCaptured" when the airbase of the warehouse has been captured by another coalition. -- @function [parent=#WAREHOUSE] AirbaseCaptured -- @param #WAREHOUSE self -- @param DCS#coalition.side Coalition Coalition side which captured the airbase, i.e. a number of @{DCS#coalition.side} enumerator. - + --- Triggers the FSM event "AirbaseCaptured" with a delay when the airbase of the warehouse has been captured by another coalition. -- @function [parent=#WAREHOUSE] __AirbaseCaptured -- @param #WAREHOUSE self @@ -2222,7 +2245,7 @@ function WAREHOUSE:New(warehouse, alias) -- @param #WAREHOUSE self -- @function [parent=#WAREHOUSE] AirbaseRecaptured -- @param DCS#coalition.side Coalition Coalition which re-captured the airbase, i.e. the same as the current warehouse owner coalition. - + --- Triggers the FSM event "AirbaseRecaptured" with a delay when the airbase of the warehouse has been re-captured from the other coalition. -- @function [parent=#WAREHOUSE] __AirbaseRecaptured -- @param #WAREHOUSE self @@ -2264,7 +2287,7 @@ function WAREHOUSE:New(warehouse, alias) --- Triggers the FSM event "Destroyed" when the warehouse was destroyed. Services are stopped. -- @function [parent=#WAREHOUSE] Destroyed -- @param #WAREHOUSE self - + --- Triggers the FSM event "Destroyed" with a delay when the warehouse was destroyed. Services are stopped. -- @function [parent=#WAREHOUSE] __Destroyed -- @param #WAREHOUSE self @@ -2283,7 +2306,7 @@ function WAREHOUSE:New(warehouse, alias) -- @param #WAREHOUSE self -- @param #string path Path where the file is saved. Default is the DCS installation root directory. -- @param #string filename (Optional) File name. Default is WAREHOUSE-_.txt. - + --- Triggers the FSM event "Save" with a delay when the warehouse assets are saved to a file. -- @function [parent=#WAREHOUSE] __Save -- @param #WAREHOUSE self @@ -2306,7 +2329,7 @@ function WAREHOUSE:New(warehouse, alias) -- @param #WAREHOUSE self -- @param #string path Path where the file is located. Default is the DCS installation root directory. -- @param #string filename (Optional) File name. Default is WAREHOUSE-_.txt. - + --- Triggers the FSM event "Load" with a delay when the warehouse assets are loaded from disk. -- @function [parent=#WAREHOUSE] __Load -- @param #WAREHOUSE self @@ -2363,6 +2386,24 @@ function WAREHOUSE:SetReportOff() return self end +--- Enable safe parking option, i.e. parking spots at an airbase will be considered as occupied when a client aircraft is parked there (even if the client slot is not taken by a player yet). +-- Note that also incoming aircraft can reserve/occupie parking spaces. +-- @param #WAREHOUSE self +-- @return #WAREHOUSE self +function WAREHOUSE:SetSafeParkingOn() + self.safeparking=true + return self +end + +--- Disable safe parking option. Note that is the default setting. +-- @param #WAREHOUSE self +-- @return #WAREHOUSE self +function WAREHOUSE:SetSafeParkingOff() + self.safeparking=false + return self +end + + --- Set interval of status updates. Note that normally only one request can be processed per time interval. -- @param #WAREHOUSE self -- @param #number timeinterval Time interval in seconds. @@ -2393,7 +2434,7 @@ function WAREHOUSE:SetWarehouseZone(zone) return self end ---- Set auto defence on. When the warehouse is under attack, all ground assets are spawned automatically and will defend the warehouse zone. +--- Set auto defence on. When the warehouse is under attack, all ground assets are spawned automatically and will defend the warehouse zone. -- @param #WAREHOUSE self -- @return #WAREHOUSE self function WAREHOUSE:SetAutoDefenceOn() @@ -2401,7 +2442,7 @@ function WAREHOUSE:SetAutoDefenceOn() return self end ---- Set auto defence off. This is the default. +--- Set auto defence off. This is the default. -- @param #WAREHOUSE self -- @return #WAREHOUSE self function WAREHOUSE:SetAutoDefenceOff() @@ -2409,7 +2450,7 @@ function WAREHOUSE:SetAutoDefenceOff() return self end ---- Set auto defence off. This is the default. +--- Enable auto save of warehouse assets at mission end event. -- @param #WAREHOUSE self -- @param #string path Path where to save the asset data file. -- @param #string filename File name. Default is generated automatically from warehouse id. @@ -2441,7 +2482,7 @@ end --- Set the connection of the warehouse to the road. -- Ground assets spawned in the warehouse spawn zone will first go to this point and from there travel on road to the requesting warehouse. -- Note that by default the road connection is set to the closest point on road from the center of the spawn zone if it is withing 3000 meters. --- Also note, that if the parameter "coordinate" is passed as nil, any road connection is disabled and ground assets cannot travel of be transportet on the ground. +-- Also note, that if the parameter "coordinate" is passed as nil, any road connection is disabled and ground assets cannot travel of be transportet on the ground. -- @param #WAREHOUSE self -- @param Core.Point#COORDINATE coordinate The road connection. Technically, the closest point on road from this coordinate is determined by DCS API function. So this point must not be exactly on the road. -- @return #WAREHOUSE self @@ -2469,7 +2510,7 @@ function WAREHOUSE:SetRailConnection(coordinate) end --- Set the port zone for this warehouse. --- The port zone is the zone, where all naval assets of the warehouse are spawned. +-- The port zone is the zone, where all naval assets of the warehouse are spawned. -- @param #WAREHOUSE self -- @param Core.Zone#ZONE zone The zone defining the naval port of the warehouse. -- @return #WAREHOUSE self @@ -2499,10 +2540,10 @@ function WAREHOUSE:AddShippingLane(remotewarehouse, group, oneway) -- Initial and final coordinates are random points within the port zones. local startcoord=self.portzone:GetRandomCoordinate() local finalcoord=remotewarehouse.portzone:GetRandomCoordinate() - + -- Create new lane from waypoints of the template group. local lane=self:_NewLane(group, startcoord, finalcoord) - + -- Debug info. Marks along shipping lane. if self.Debug then for i=1,#lane do @@ -2511,29 +2552,29 @@ function WAREHOUSE:AddShippingLane(remotewarehouse, group, oneway) coord:MarkToCoalition(text, self:GetCoalition()) end end - + -- Name of the remote warehouse. local remotename=remotewarehouse.warehouse:GetName() - + -- Create new table if no shipping lane exists yet. if self.shippinglanes[remotename]==nil then self.shippinglanes[remotename]={} - end - + end + -- Add shipping lane. table.insert(self.shippinglanes[remotename], lane) - + -- Add shipping lane in the opposite direction. if not oneway then remotewarehouse:AddShippingLane(self, group, true) end - + return self end --- Add an off-road path from this warehouse to another and back. --- The start and end points are automatically set to one random point in the respective spawn zones of the two warehouses. +-- The start and end points are automatically set to one random point in the respective spawn zones of the two warehouses. -- By default, the reverse path is also added as path from the remote warehouse to this warehouse. -- @param #WAREHOUSE self -- @param #WAREHOUSE remotewarehouse The remote warehouse to which the path leads. @@ -2545,15 +2586,15 @@ function WAREHOUSE:AddOffRoadPath(remotewarehouse, group, oneway) -- Initial and final points are random points within the spawn zone. local startcoord=self.spawnzone:GetRandomCoordinate() local finalcoord=remotewarehouse.spawnzone:GetRandomCoordinate() - + -- Create new path from template group waypoints. local path=self:_NewLane(group, startcoord, finalcoord) - + if path==nil then self:E(self.wid.."ERROR: Offroad path could not be added. Group present in ME?") return end - + -- Debug info. Marks along path. if path and self.Debug then for i=1,#path do @@ -2562,23 +2603,23 @@ function WAREHOUSE:AddOffRoadPath(remotewarehouse, group, oneway) coord:MarkToCoalition(text, self:GetCoalition()) end end - + -- Name of the remote warehouse. local remotename=remotewarehouse.warehouse:GetName() - + -- Create new table if no shipping lane exists yet. if self.offroadpaths[remotename]==nil then self.offroadpaths[remotename]={} - end - + end + -- Add off road path. table.insert(self.offroadpaths[remotename], path) - - -- Add off road path in the opposite direction (if not forbidden). + + -- Add off road path in the opposite direction (if not forbidden). if not oneway then remotewarehouse:AddOffRoadPath(self, group, true) end - + return self end @@ -2596,19 +2637,19 @@ function WAREHOUSE:_NewLane(group, startcoord, finalcoord) -- Get route from template. local lanepoints=group:GetTemplateRoutePoints() - + -- First and last waypoints local laneF=lanepoints[1] local laneL=lanepoints[#lanepoints] - + -- Get corresponding coordinates. local coordF=COORDINATE:New(laneF.x, 0, laneF.y) local coordL=COORDINATE:New(laneL.x, 0, laneL.y) - + -- Figure out which point is closer to the port of this warehouse. local distF=startcoord:Get2DDistance(coordF) local distL=startcoord:Get2DDistance(coordL) - + -- Add the lane. Need to take care of the wrong "direction". lane={} if distF0 then - + -- Check if coalition is right. local samecoalition=anycoalition or Coalition==warehouse:GetCoalition() - + -- Check that warehouse is in service. if samecoalition and not (warehouse:IsNotReadyYet() or warehouse:IsStopped() or warehouse:IsDestroyed()) then - + -- Get number of assets. Whole stock is returned if no descriptor/value is given. local nassets=warehouse:GetNumberOfAssets(Descriptor, DescriptorValue) - + --env.info(string.format(" FF warehouse %s nassets = %d for %s=%s", warehouse.alias, nassets, tostring(Descriptor), tostring(DescriptorValue))) - + -- Assume we have enough. local enough=true -- If specifc assets need to be present... if Descriptor and DescriptorValue then -- Check that enough assets (default 1) are available. enough = nassets>=MinAssets - end - + end + -- Check distance. if enough and (distmin==nil or dist Need to do a lot of checks. - + -- All transports are dead but there is still cargo left ==> Put cargo back into stock. for _,_group in pairs(request.transportgroupset:GetSetObjects()) do local group=_group --Wrapper.Group#GROUP - + -- Check if group is alive. if group and group:IsAlive() then - + -- Check if group is in the spawn zone? local category=group:GetCategory() - + -- Get current speed. local speed=group:GetVelocityKMH() local notmoving=speed<1 - + -- Closest airbase. local airbase=group:GetCoordinate():GetClosestAirbase():GetName() local athomebase=self.airbase and self.airbase:GetName()==airbase - + -- On ground local onground=not group:InAir() - + -- In spawn zone. local inspawnzone=group:IsPartlyOrCompletelyInZone(self.spawnzone) - - -- Check conditions for being back home. + + -- Check conditions for being back home. local ishome=false if category==Group.Category.GROUND or category==Group.Category.HELICOPTER then -- Units go back to the spawn zone, helicopters land and they should not move any more. @@ -3321,70 +3364,70 @@ function WAREHOUSE:_JobDone() -- Planes need to be on ground at their home airbase and should not move any more. ishome=athomebase and onground and notmoving end - + -- Debug text. local text=string.format("Group %s: speed=%d km/h, onground=%s , airbase=%s, spawnzone=%s ==> ishome=%s", group:GetName(), speed, tostring(onground), airbase, tostring(inspawnzone), tostring(ishome)) self:T(self.wid..text) - + if ishome then -- Info message. local text=string.format("Warehouse %s: Transport group arrived back home and no cargo left for request id=%d.\nSending transport group %s back to stock.", self.alias, request.uid, group:GetName()) - self:_InfoMessage(text) - + self:_InfoMessage(text) + -- Debug smoke. if self.Debug then group:SmokeRed() end - + -- Group arrived. self:Arrived(group) end - end + end end - + end - + else - + if ntransport==0 and request.ntransport>0 then ----------------------------------- -- Still cargo but no transports -- ----------------------------------- - + local ncargoalive=0 - + -- All transports are dead but there is still cargo left ==> Put cargo back into stock. for _,_group in pairs(request.cargogroupset:GetSetObjects()) do --local group=group --Wrapper.Group#GROUP - + -- These groups have been respawned as cargo, i.e. their name changed! local groupname=_group:GetName() local group=GROUP:FindByName(groupname.."#CARGO") - + -- Check if group is alive. if group and group:IsAlive() then - + -- Check if group is in spawn zone? if group:IsPartlyOrCompletelyInZone(self.spawnzone) then - -- Debug smoke. + -- Debug smoke. if self.Debug then group:SmokeBlue() - end + end -- Add asset group back to stock. self:AddAsset(group) ncargoalive=ncargoalive+1 end end - + end -- Info message. - self:_InfoMessage(string.format("Warehouse %s: All transports of request id=%s dead! Putting remaining %s cargo assets back into warehouse!", self.alias, request.uid, ncargoalive)) + self:_InfoMessage(string.format("Warehouse %s: All transports of request id=%s dead! Putting remaining %s cargo assets back into warehouse!", self.alias, request.uid, ncargoalive)) end end - + end -- loop over requests -- Remove pending requests if done. @@ -3401,15 +3444,15 @@ function WAREHOUSE:_CheckAssetStatus() local function _CheckGroup(_request, _group) local request=_request --#WAREHOUSE.Pendingitem local group=_group --Wrapper.Group#GROUP - + if group and group:IsAlive() then - + -- Category of group. local category=group:GetCategory() - + for _,_unit in pairs(group:GetUnits()) do local unit=_unit --Wrapper.Unit#UNIT - + if unit and unit:IsAlive() then local unitid=unit:GetID() local life9=unit:GetLife() @@ -3417,16 +3460,16 @@ function WAREHOUSE:_CheckAssetStatus() local life=life9/life0*100 local speed=unit:GetVelocityMPS() local onground=unit:InAir() - + local problem=false if life<10 then - self:T(string.format("Unit %s is heavily damaged!", unit:GetName())) + self:T(string.format("Unit %s is heavily damaged!", unit:GetName())) end if speed<1 and unit:GetSpeedMax()>1 and onground then self:T(string.format("Unit %s is not moving!", unit:GetName())) problem=true end - + if problem then if request.assetproblem[unitid] then local deltaT=timer.getAbsTime()-request.assetproblem[unitid] @@ -3439,33 +3482,33 @@ function WAREHOUSE:_CheckAssetStatus() end end end - + end end end - + for _,request in pairs(self.pending) do local request=request --#WAREHOUSE.Pendingitem - + -- Cargo groups. if request.cargogroupset then for _,_group in pairs(request.cargogroupset:GetSet()) do local group=_group --Wrapper.Group#GROUP - + _CheckGroup(request, group) - + end end - + -- Transport groups. if request.transportgroupset then for _,group in pairs(request.transportgroupset:GetSet()) do - - _CheckGroup(request, group) + + _CheckGroup(request, group) end end - + end end @@ -3491,32 +3534,32 @@ function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribu -- Set default. local n=ngroups or 1 - + -- Handle case where just a string is passed. if type(group)=="string" then group=GROUP:FindByName(group) end - + if liveries and type(liveries)=="string" then liveries={liveries} end - + if group then - + -- Try to get UIDs from group name. Is this group a known or a new asset? local wid,aid,rid=self:_GetIDsFromGroup(group) - + if wid and aid and rid then --------------------------- -- This is a KNOWN asset -- --------------------------- - + -- Get the original warehouse this group belonged to. local warehouse=self:FindWarehouseInDB(wid) if warehouse then local request=warehouse:_GetRequestOfGroup(group, warehouse.pending) if request then - + -- Increase number of cargo delivered and transports home. local istransport=warehouse:_GroupIsTransport(group,request) if istransport==true then @@ -3530,66 +3573,67 @@ function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribu else self:T(warehouse.wid..string.format("WARNING: Group %s is neither cargo nor transport!", group:GetName())) end - - end - - -- If no assignment was given we take the assignment of the request if there is any. - if assignment==nil and request.assignment~=nil then - assignment=request.assignment + + -- If no assignment was given we take the assignment of the request if there is any. + if assignment==nil and request.assignment~=nil then + assignment=request.assignment + end + end end -- Get the asset from the global DB. local asset=self:FindAssetInDB(group) - -- Set livery. + -- Set livery. if liveries then asset.livery=liveries[math.random(#liveries)] end - + -- Set skill. asset.skill=skill - + -- Note the group is only added once, i.e. the ngroups parameter is ignored here. -- This is because usually these request comes from an asset that has been transfered from another warehouse and hence should only be added once. - if asset~=nil then + if asset~=nil then self:_DebugMessage(string.format("Warehouse %s: Adding KNOWN asset uid=%d with attribute=%s to stock.", self.alias, asset.uid, asset.attribute), 5) table.insert(self.stock, asset) self:NewAsset(asset, assignment or "") else self:_ErrorMessage(string.format("ERROR: Known asset could not be found in global warehouse db!"), 0) - end - + end + else ------------------------- -- This is a NEW asset -- ------------------------- - + -- Debug info. self:_DebugMessage(string.format("Warehouse %s: Adding %d NEW assets of group %s to stock.", self.alias, n, tostring(group:GetName())), 5) - + -- This is a group that is not in the db yet. Add it n times. local assets=self:_RegisterAsset(group, n, forceattribute, forcecargobay, forceweight, loadradius, liveries, skill) - + -- Add created assets to stock of this warehouse. for _,asset in pairs(assets) do table.insert(self.stock, asset) self:NewAsset(asset, assignment or "") - end - - end - + end + + end + -- Destroy group if it is alive. if group:IsAlive()==true then self:_DebugMessage(string.format("Destroying group %s.", group:GetName()), 5) -- Setting parameter to false, i.e. creating NO dead or remove unit event, seems to not confuse the dispatcher logic. group:Destroy(false) end - + else self:E(self.wid.."ERROR: Unknown group added as asset!") + self:E({unknowngroup=group}) end - + -- Update status. --self:__Status(-1) end @@ -3610,7 +3654,7 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, -- Set default. local n=ngroups or 1 - + -- Get the size of an object. local function _GetObjectSize(DCSdesc) if DCSdesc.box then @@ -3620,18 +3664,18 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, return math.max(x,z), x , y, z end return 0,0,0,0 - end - + end + -- Get name of template group. local templategroupname=group:GetName() - + local Descriptors=group:GetUnit(1):GetDesc() local Category=group:GetCategory() local TypeName=group:GetTypeName() local SpeedMax=group:GetSpeedMax() local RangeMin=group:GetRange() local smax,sx,sy,sz=_GetObjectSize(Descriptors) - + -- Get weight and cargo bay size in kg. local weight=0 local cargobay={} @@ -3640,31 +3684,31 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, for _i,_unit in pairs(group:GetUnits()) do local unit=_unit --Wrapper.Unit#UNIT local Desc=unit:GetDesc() - + -- Weight. We sum up all units in the group. local unitweight=forceweight or Desc.massEmpty if unitweight then weight=weight+unitweight end - + local cargomax=0 local massfuel=Desc.fuelMassMax or 0 local massempty=Desc.massEmpty or 0 local massmax=Desc.massMax or 0 - + -- Calcuate cargo bay limit value. cargomax=massmax-massfuel-massempty self:T3(self.wid..string.format("Unit name=%s: mass empty=%.1f kg, fuel=%.1f kg, max=%.1f kg ==> cargo=%.1f kg", unit:GetName(), unitweight, massfuel, massmax, cargomax)) - + -- Cargo bay size. local bay=forcecargobay or unit:GetCargoBayFreeWeight() - + -- Add bay size to table. table.insert(cargobay, bay) - + -- Sum up total bay size. cargobaytot=cargobaytot+bay - + -- Get max bay size. if bay>cargobaymax then cargobaymax=bay @@ -3680,20 +3724,20 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, -- Add this n times to the table. for i=1,n do local asset={} --#WAREHOUSE.Assetitem - + -- Increase asset unique id counter. WAREHOUSE.db.AssetID=WAREHOUSE.db.AssetID+1 - + -- Set parameters. asset.uid=WAREHOUSE.db.AssetID asset.templatename=templategroupname asset.template=UTILS.DeepCopy(_DATABASE.Templates.Groups[templategroupname].Template) asset.category=Category asset.unittype=TypeName - asset.nunits=#asset.template.units + asset.nunits=#asset.template.units asset.range=RangeMin asset.speedmax=SpeedMax - asset.size=smax + asset.size=smax asset.weight=weight asset.DCSdesc=Descriptors asset.attribute=attribute @@ -3705,14 +3749,14 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, asset.livery=liveries[math.random(#liveries)] end asset.skill=skill - + if i==1 then self:_AssetItemInfo(asset) end - + -- Add asset to global db. WAREHOUSE.db.Assets[asset.uid]=asset - + -- Add asset to the table that is retured. table.insert(assets,asset) end @@ -3773,12 +3817,12 @@ end -- @param #string Assignment A keyword or text that later be used to identify this request and postprocess the assets. -- @return #boolean If true, request is okay at first glance. function WAREHOUSE:onbeforeAddRequest(From, Event, To, warehouse, AssetDescriptor, AssetDescriptorValue, nAsset, TransportType, nTransport, Assignment, Prio) - + -- Request is okay. local okay=true - + if AssetDescriptor==WAREHOUSE.Descriptor.ATTRIBUTE then - + -- Check if a valid attibute was given. local gotit=false for _,attribute in pairs(WAREHOUSE.Attribute) do @@ -3790,7 +3834,7 @@ function WAREHOUSE:onbeforeAddRequest(From, Event, To, warehouse, AssetDescripto self:_ErrorMessage("ERROR: Invalid request. Asset attribute is unknown!", 5) okay=false end - + elseif AssetDescriptor==WAREHOUSE.Descriptor.CATEGORY then -- Check if a valid category was given. @@ -3804,21 +3848,21 @@ function WAREHOUSE:onbeforeAddRequest(From, Event, To, warehouse, AssetDescripto self:_ErrorMessage("ERROR: Invalid request. Asset category is unknown!", 5) okay=false end - + elseif AssetDescriptor==WAREHOUSE.Descriptor.GROUPNAME then - + if type(AssetDescriptorValue)~="string" then self:_ErrorMessage("ERROR: Invalid request. Asset template name must be passed as a string!", 5) - okay=false + okay=false end - + elseif AssetDescriptor==WAREHOUSE.Descriptor.UNITTYPE then if type(AssetDescriptorValue)~="string" then self:_ErrorMessage("ERROR: Invalid request. Asset unit type must be passed as a string!", 5) - okay=false + okay=false end - + else self:_ErrorMessage("ERROR: Invalid request. Asset descriptor is not ATTRIBUTE, CATEGORY, GROUPNAME or UNITTYPE!", 5) okay=false @@ -3827,7 +3871,7 @@ function WAREHOUSE:onbeforeAddRequest(From, Event, To, warehouse, AssetDescripto -- Warehouse is stopped? if self:IsStopped() then self:_ErrorMessage("ERROR: Invalid request. Warehouse is stopped!", 0) - okay=false + okay=false end -- Warehouse is destroyed? @@ -3835,7 +3879,7 @@ function WAREHOUSE:onbeforeAddRequest(From, Event, To, warehouse, AssetDescripto self:_ErrorMessage("ERROR: Invalid request. Warehouse is destroyed!", 0) okay=false end - + return okay end @@ -3849,7 +3893,7 @@ end -- @param AssetDescriptorValue Value of the asset descriptor. Type depends on descriptor, i.e. could be a string, etc. -- @param #number nAsset Number of groups requested that match the asset specification. -- @param #WAREHOUSE.TransportType TransportType Type of transport. --- @param #number nTransport Number of transport units requested. +-- @param #number nTransport Number of transport units requested. -- @param #number Prio Priority of the request. Number ranging from 1=high to 100=low. -- @param #string Assignment A keyword or text that later be used to identify this request and postprocess the assets. function WAREHOUSE:onafterAddRequest(From, Event, To, warehouse, AssetDescriptor, AssetDescriptorValue, nAsset, TransportType, nTransport, Prio, Assignment) @@ -3870,8 +3914,8 @@ function WAREHOUSE:onafterAddRequest(From, Event, To, warehouse, AssetDescriptor local toself=false if self.warehouse:GetName()==warehouse.warehouse:GetName() then toself=true - end - + end + -- Increase id. self.queueid=self.queueid+1 @@ -3887,17 +3931,17 @@ function WAREHOUSE:onafterAddRequest(From, Event, To, warehouse, AssetDescriptor ntransport=nTransport, assignment=tostring(Assignment), airbase=warehouse:GetAirbase(), - category=warehouse:GetAirbaseCategory(), + category=warehouse:GetAirbaseCategory(), ndelivered=0, ntransporthome=0, assets={}, toself=toself, } --#WAREHOUSE.Queueitem - + -- Add request to queue. table.insert(self.queue, request) - - local text=string.format("Warehouse %s: New request from warehouse %s.\nDescriptor %s=%s, #assets=%s; Transport=%s, #transports =%s.", + + local text=string.format("Warehouse %s: New request from warehouse %s.\nDescriptor %s=%s, #assets=%s; Transport=%s, #transports =%s.", self.alias, warehouse.alias, request.assetdesc, tostring(request.assetdescval), tostring(request.nasset), request.transporttype, tostring(request.ntransport)) self:_DebugMessage(text, 5) @@ -3922,29 +3966,29 @@ function WAREHOUSE:onbeforeRequest(From, Event, To, Request) -- Shortcut to cargoassets. local _assets=Request.cargoassets - + if Request.nasset==0 then local text=string.format("Warehouse %s: Request denied! Zero assets were requested.", self.alias) self:_InfoMessage(text, 10) return false end - + -- Check if destination is in range for all requested assets. for _,_asset in pairs(_assets) do local asset=_asset --#WAREHOUSE.Assetitem - -- Check if destination is in range. - if asset.range1 then @@ -4458,35 +4502,35 @@ function WAREHOUSE:onafterUnloaded(From, Event, To, group) self:Arrived(group) elseif group:IsShip() then -- Not sure if naval units will be allowed as cargo even though it might be possible. Best put them into warehouse immediately. - self:Arrived(group) + self:Arrived(group) end - + else self:E(self.wid..string.format("ERROR unloaded Cargo group is not alive!")) - end + end end --- On after "Arrived" event. Triggered when a group has arrived at its destination warehouse. -- The routine should be called by the warehouse sending this asset and not by the receiving warehouse. -- It is checked if this asset is cargo (or self propelled) or transport. If it is cargo it is put into the stock of receiving warehouse. --- If it is a transporter it is put back into the sending warehouse since transports are supposed to return their home warehouse. +-- If it is a transporter it is put back into the sending warehouse since transports are supposed to return their home warehouse. -- @param #WAREHOUSE self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -- @param Wrapper.Group#GROUP group The group that was delivered. function WAREHOUSE:onafterArrived(From, Event, To, group) - + -- Debug message and smoke. if self.Debug then group:SmokeOrange() end - + -- Get pending request this group belongs to. local request=self:_GetRequestOfGroup(group, self.pending) if request then - + -- Get the right warehouse to put the asset into -- Transports go back to the warehouse which called this function while cargo goes into the receiving warehouse. local warehouse=request.warehouse @@ -4499,15 +4543,15 @@ function WAREHOUSE:onafterArrived(From, Event, To, group) self:E(self.wid..string.format("ERROR: Group %s is neither cargo nor transport", group:GetName())) return end - + -- Debug message. self:_DebugMessage(string.format("Group %s arrived at warehouse %s!", tostring(group:GetName()), warehouse.alias), 5) - + -- Route mobile ground group to the warehouse. Group has 60 seconds to get there or it is despawned and added as asset to the new warehouse regardless. if group:IsGround() and group:GetSpeedMax()>1 then group:RouteGroundTo(warehouse:GetCoordinate(), group:GetSpeedMax()*0.3, "Off Road") end - + -- Increase number of cargo delivered and transports home. local istransport=warehouse:_GroupIsTransport(group,request) if istransport==true then @@ -4520,12 +4564,12 @@ function WAREHOUSE:onafterArrived(From, Event, To, group) self:T2(warehouse.wid..string.format("Cargo %d of %s delivered.", request.ndelivered, tostring(request.nasset))) else self:E(warehouse.wid..string.format("ERROR: Group %s is neither cargo nor transport!", group:GetName())) - end - + end + -- Move asset from pending queue into new warehouse. warehouse:__AddAsset(60, group) end - + end --- On after "Delivered" event. Triggered when all asset groups have reached their destination. Corresponding request is deleted from the pending queue. @@ -4544,10 +4588,10 @@ function WAREHOUSE:onafterDelivered(From, Event, To, request) if self.Debug then self:_Fireworks(request.warehouse:GetCoordinate()) end - + -- Set delivered status for this request uid. self.delivered[request.uid]=true - + end @@ -4564,7 +4608,7 @@ function WAREHOUSE:onafterSelfRequest(From, Event, To, groupset, request) -- Debug info. self:_DebugMessage(string.format("Assets spawned at warehouse %s after self request!", self.alias)) - + -- Debug info. for _,_group in pairs(groupset:GetSetObjects()) do local group=_group --Wrapper.Group#GROUP @@ -4572,7 +4616,7 @@ function WAREHOUSE:onafterSelfRequest(From, Event, To, groupset, request) group:FlareGreen() end end - + -- Add a "defender request" to be able to despawn all assets once defeated. if self:IsAttacked() then @@ -4584,13 +4628,13 @@ function WAREHOUSE:onafterSelfRequest(From, Event, To, groupset, request) if group:IsGround() and speedmax>1 and group:IsNotInZone(self.zone) then group:RouteGroundTo(self.zone:GetRandomCoordinate(), 0.8*speedmax, "Off Road") end - end - end - + end + end + -- Add request to defenders. table.insert(self.defending, request) end - + end --- On after "Attacked" event. Warehouse is under attack by an another coalition. @@ -4605,29 +4649,29 @@ function WAREHOUSE:onafterAttacked(From, Event, To, Coalition, Country) -- Warning. local text=string.format("Warehouse %s: We are under attack!", self.alias) self:_InfoMessage(text) - + -- Debug smoke. if self.Debug then self:GetCoordinate():SmokeOrange() - end - + end + -- Spawn all ground units in the spawnzone? if self.autodefence then local nground=self:GetNumberOfAssets(WAREHOUSE.Descriptor.CATEGORY, Group.Category.GROUND) local text=string.format("Warehouse auto defence activated.\n") - + if nground>0 then text=text..string.format("Deploying all %d ground assets.", nground) - + -- Add self request. - self:AddRequest(self, WAREHOUSE.Descriptor.CATEGORY, Group.Category.GROUND, WAREHOUSE.Quantity.ALL, nil, nil , 0) + self:AddRequest(self, WAREHOUSE.Descriptor.CATEGORY, Group.Category.GROUND, WAREHOUSE.Quantity.ALL, nil, nil , 0, "AutoDefence") else - text=text..string.format("No ground assets currently available.") + text=text..string.format("No ground assets currently available.") end self:_InfoMessage(text) else local text=string.format("Warehouse auto defence inactive.") - self:I(self.wid..text) + self:I(self.wid..text) end end @@ -4645,28 +4689,28 @@ function WAREHOUSE:onafterDefeated(From, Event, To) -- Debug smoke. if self.Debug then self:GetCoordinate():SmokeGreen() - end + end -- Auto defence: put assets back into stock. if self.autodefence then for _,request in pairs(self.defending) do - - -- Route defenders back to warehoue (for visual reasons only) and put them back into stock. + + -- Route defenders back to warehoue (for visual reasons only) and put them back into stock. for _,_group in pairs(request.cargogroupset:GetSetObjects()) do local group=_group --Wrapper.Group#GROUP - + -- Get max speed of group and route it back slowly to the warehouse. local speed=group:GetSpeedMax() if group:IsGround() and speed>1 then group:RouteGroundTo(self:GetCoordinate(), speed*0.3) - end - + end + -- Add asset group back to stock after 60 seconds. self:__AddAsset(60, group) end - + end - + self.defending=nil self.defending={} end @@ -4686,8 +4730,8 @@ function WAREHOUSE:onbeforeChangeCountry(From, Event, To, Country) -- Message. local text=string.format("Warehouse %s: request to change country %d-->%d", self.alias, currentCountry, Country) self:_DebugMessage(text, 10) - - -- Check if current or requested coalition or country match. + + -- Check if current or requested coalition or country match. if currentCountry~=Country then return true end @@ -4709,17 +4753,17 @@ function WAREHOUSE:onafterChangeCountry(From, Event, To, Country) -- Respawn warehouse with new coalition/country. self.warehouse:ReSpawn(Country) - + local CoalitionNew=self:GetCoalition() - + -- Delete all waiting requests because they are not valid any more. self.queue=nil self.queue={} - + -- Airbase could have been captured before and already belongs to the new coalition. local airbase=AIRBASE:FindByName(self.airbasename) local airbasecoaltion=airbase:GetCoalition() - + if CoalitionNew==airbasecoaltion then -- Airbase already owned by the coalition that captured the warehouse. Airbase can be used by this warehouse. self.airbase=airbase @@ -4727,7 +4771,7 @@ function WAREHOUSE:onafterChangeCountry(From, Event, To, Country) -- Airbase is owned by other coalition. So this warehouse does not have an airbase unil it is captured. self.airbase=nil end - + -- Debug smoke. if self.Debug then if CoalitionNew==coalition.side.RED then @@ -4736,7 +4780,7 @@ function WAREHOUSE:onafterChangeCountry(From, Event, To, Country) self:GetCoordinate():SmokeBlue() end end - + end --- On after "Captured" event. Warehouse has been captured by another coalition. @@ -4752,9 +4796,8 @@ function WAREHOUSE:onafterCaptured(From, Event, To, Coalition, Country) local text=string.format("Warehouse %s: We were captured by enemy coalition (side=%d)!", self.alias, Coalition) self:_InfoMessage(text) - -- Change coalition and country of warehouse static. - self:ChangeCoaliton(Coalition, Country) - + -- Warehouse respawned. + self:ChangeCountry(Country) end @@ -4778,7 +4821,7 @@ function WAREHOUSE:onafterAirbaseCaptured(From, Event, To, Coalition) self.airbase:GetCoordinate():SmokeBlue() end end - + -- Set airbase to nil and category to no airbase. self.airbase=nil end @@ -4795,9 +4838,9 @@ function WAREHOUSE:onafterAirbaseRecaptured(From, Event, To, Coalition) local text=string.format("Warehouse %s: We recaptured our airbase %s from the enemy (coalition=%d)!", self.alias, self.airbasename, Coalition) self:_InfoMessage(text) - -- Set airbase and category. + -- Set airbase and category. self.airbase=AIRBASE:FindByName(self.airbasename) - + -- Debug smoke. if self.Debug then if Coalition==coalition.side.RED then @@ -4806,7 +4849,7 @@ function WAREHOUSE:onafterAirbaseRecaptured(From, Event, To, Coalition) self.airbase:GetCoordinate():SmokeBlue() end end - + end @@ -4834,7 +4877,7 @@ function WAREHOUSE:onafterDestroyed(From, Event, To) -- Message. local text=string.format("Warehouse %s was destroyed! Assets lost %d.", self.alias, #self.stock) self:_InfoMessage(text) - + -- Remove all table entries from waiting queue and stock. for k,_ in pairs(self.queue) do self.queue[k]=nil @@ -4866,32 +4909,32 @@ function WAREHOUSE:onafterSave(From, Event, To, path, filename) f:write(data) f:close() end - + -- Set file name. filename=filename or string.format("WAREHOUSE-%d_%s.txt", self.uid, self.alias) - + -- Set path. if path~=nil then filename=path.."\\"..filename end - + -- Info local text=string.format("Saving warehouse assets to file %s", filename) MESSAGE:New(text,30):ToAllIf(self.Debug or self.Report) self:I(self.wid..text) - + local warehouseassets="" warehouseassets=warehouseassets..string.format("coalition=%d\n", self:GetCoalition()) warehouseassets=warehouseassets..string.format("country=%d\n", self:GetCountry()) - + -- Loop over all assets in stock. for _,_asset in pairs(self.stock) do local asset=_asset -- #WAREHOUSE.Assetitem - + -- Loop over asset parameters. local assetstring="" for key,value in pairs(asset) do - + -- Only save keys which are needed to restore the asset. if key=="templatename" or key=="attribute" or key=="cargobay" or key=="weight" or key=="loadradius" or key=="livery" or key=="skill" or key=="assignment" then local name @@ -4904,13 +4947,13 @@ function WAREHOUSE:onafterSave(From, Event, To, path, filename) end self:I(string.format("Loaded asset: %s", assetstring)) end - + -- Add asset string. warehouseassets=warehouseassets..assetstring.."\n" end -- Save file. - _savefile(filename, warehouseassets) + _savefile(filename, warehouseassets) end @@ -4927,25 +4970,25 @@ function WAREHOUSE:onbeforeLoad(From, Event, To, path, filename) local function _fileexists(name) local f=io.open(name,"r") - if f~=nil then + if f~=nil then io.close(f) - return true - else + return true + else return false end end -- Set file name. filename=filename or string.format("WAREHOUSE-%d_%s.txt", self.uid, self.alias) - + -- Set path. if path~=nil then filename=path.."\\"..filename end - + -- Check if file exists. local exists=_fileexists(filename) - + if exists then return true else @@ -4974,40 +5017,40 @@ function WAREHOUSE:onafterLoad(From, Event, To, path, filename) -- Set file name. filename=filename or string.format("WAREHOUSE-%d_%s.txt", self.uid, self.alias) - + -- Set path. if path~=nil then filename=path.."\\"..filename end - + -- Info local text=string.format("Loading warehouse assets from file %s", filename) MESSAGE:New(text,30):ToAllIf(self.Debug or self.Report) - self:I(self.wid..text) + self:I(self.wid..text) -- Load asset data from file. local data=_loadfile(filename) -- Split by line break. local assetdata=UTILS.Split(data,"\n") - + -- Coalition and coutrny. local Coalition local Country - + -- Loop over asset lines. local assets={} for _,asset in pairs(assetdata) do - + -- Parameters are separated by semi-colons local descriptors=UTILS.Split(asset,";") - + local asset={} local isasset=false for _,descriptor in pairs(descriptors) do - + local keyval=UTILS.Split(descriptor,"=") - + if #keyval==2 then if keyval[1]=="coalition" then @@ -5017,20 +5060,20 @@ function WAREHOUSE:onafterLoad(From, Event, To, path, filename) -- Get country id. Country=tonumber(keyval[2]) else - + -- This is an asset. isasset=true - + local key=keyval[1] local val=keyval[2] - - --env.info(string.format("FF asset key=%s val=%s", key, val)) - + + --env.info(string.format("FF asset key=%s val=%s", key, val)) + -- Livery or skill could be "nil". if val=="nil" then val=nil - end - + end + -- Convert string to number where necessary. if key=="cargobay" or key=="weight" or key=="loadradius" then asset[key]=tonumber(val) @@ -5038,25 +5081,25 @@ function WAREHOUSE:onafterLoad(From, Event, To, path, filename) asset[key]=val end end - + end end - + -- Add to table. if isasset then table.insert(assets, asset) end end - + -- Respawn warehouse with prev coalition if necessary. if Country~=self:GetCountry() then self:T(self.wid..string.format("Changing warehouse country %d-->%d on loading assets.", self:GetCountry(), Country)) self:ChangeCountry(Country) end - + for _,_asset in pairs(assets) do local asset=_asset --#WAREHOUSE.Assetitem - + local group=GROUP:FindByName(asset.templatename) if group then self:AddAsset(group, 1, asset.attribute, asset.cargobay, asset.weight, asset.loadradius, asset.skill, asset.livery, asset.assignment) @@ -5078,67 +5121,70 @@ end function WAREHOUSE:_SpawnAssetRequest(Request) self:F2({requestUID=Request.uid}) - -- Shortcut to cargo assets. + -- Shortcut to cargo assets. local _assetstock=Request.cargoassets -- General type and category. local _cargotype=Request.cargoattribute --#WAREHOUSE.Attribute local _cargocategory=Request.cargocategory --DCS#Group.Category - + -- Now we try to find all parking spots for all cargo groups in advance. Due to the for loop, the parking spots do not get updated while spawning. local Parking={} if _cargocategory==Group.Category.AIRPLANE or _cargocategory==Group.Category.HELICOPTER then Parking=self:_FindParkingForAssets(self.airbase,_assetstock) or {} end - + -- Spawn aircraft in uncontrolled state. local UnControlled=true - + -- Create an empty group set. local _groupset=SET_GROUP:New() -- Table for all spawned assets. local _assets={} - + -- Loop over cargo requests. for i=1,#_assetstock do -- Get stock item. local _assetitem=_assetstock[i] --#WAREHOUSE.Assetitem - + -- Alias of the group. local _alias=self:_Alias(_assetitem, Request) -- Spawn an asset group. - local _group=nil --Wrapper.Group#GROUP + local _group=nil --Wrapper.Group#GROUP if _assetitem.category==Group.Category.GROUND then - - -- Spawn ground troops. + + -- Spawn ground troops. _group=self:_SpawnAssetGroundNaval(_alias,_assetitem, Request, self.spawnzone) - + elseif _assetitem.category==Group.Category.AIRPLANE or _assetitem.category==Group.Category.HELICOPTER then - + -- Spawn air units. if Parking[_assetitem.uid] then _group=self:_SpawnAssetAircraft(_alias,_assetitem, Request, Parking[_assetitem.uid], UnControlled) else _group=self:_SpawnAssetAircraft(_alias,_assetitem, Request, nil, UnControlled) end - + elseif _assetitem.category==Group.Category.TRAIN then - + -- Spawn train. if self.rail then --TODO: Rail should only get one asset because they would spawn on top! + + -- Spawn naval assets. + _group=self:_SpawnAssetGroundNaval(_alias,_assetitem, Request, self.spawnzone) end - - self:E(self.wid.."ERROR: Spawning of TRAIN assets not possible yet!") - + + --self:E(self.wid.."ERROR: Spawning of TRAIN assets not possible yet!") + elseif _assetitem.category==Group.Category.SHIP then - + -- Spawn naval assets. _group=self:_SpawnAssetGroundNaval(_alias,_assetitem, Request, self.portzone) - + else self:E(self.wid.."ERROR: Unknown asset category!") end @@ -5146,11 +5192,11 @@ function WAREHOUSE:_SpawnAssetRequest(Request) -- Add group to group set and asset list. if _group then _groupset:AddGroup(_group) - table.insert(_assets, _assetitem) + table.insert(_assets, _assetitem) else self:E(self.wid.."ERROR: Cargo asset could not be spawned!") end - + end -- Delete spawned items from warehouse stock. @@ -5159,12 +5205,12 @@ function WAREHOUSE:_SpawnAssetRequest(Request) Request.assets[asset.uid]=asset self:_DeleteStockItem(asset) end - + -- Overwrite the assets with the actually spawned ones. Request.cargoassets=_assets return _groupset -end +end --- Spawn a ground or naval asset in the corresponding spawn zone of the warehouse. @@ -5177,23 +5223,28 @@ end -- @return Wrapper.Group#GROUP The spawned group or nil if the group could not be spawned. function WAREHOUSE:_SpawnAssetGroundNaval(alias, asset, request, spawnzone, aioff) - if asset and (asset.category==Group.Category.GROUND or asset.category==Group.Category.SHIP) then - + if asset and (asset.category==Group.Category.GROUND or asset.category==Group.Category.SHIP or asset.category==Group.Category.TRAIN) then + -- Prepare spawn template. - local template=self:_SpawnAssetPrepareTemplate(asset, alias) - + local template=self:_SpawnAssetPrepareTemplate(asset, alias) + -- Initial spawn point. - template.route.points[1]={} - + template.route.points[1]={} + -- Get a random coordinate in the spawn zone. local coord=spawnzone:GetRandomCoordinate() + + -- For trains, we use the rail connection point. + if asset.category==Group.Category.TRAIN then + coord=self.rail + end -- Translate the position of the units. for i=1,#template.units do - + -- Unit template. local unit = template.units[i] - + -- Translate position. local SX = unit.x or 0 local SY = unit.y or 0 @@ -5201,40 +5252,40 @@ function WAREHOUSE:_SpawnAssetGroundNaval(alias, asset, request, spawnzone, aiof local BY = asset.template.route.points[1].y local TX = coord.x + (SX-BX) local TY = coord.z + (SY-BY) - + template.units[i].x = TX template.units[i].y = TY - + if asset.livery then unit.livery_id = asset.livery end if asset.skill then unit.skill= asset.skill end - + end - + template.route.points[1].x = coord.x template.route.points[1].y = coord.z - + template.x = coord.x template.y = coord.z template.alt = coord.y - + -- Spawn group. local group=_DATABASE:Spawn(template) --Wrapper.Group#GROUP - + -- Activate group. Should only be necessary for late activated groups. --group:Activate() - + -- Switch AI off if desired. This works only for ground and naval groups. if aioff then group:SetAIOff() end - + return group end - + return nil end @@ -5250,55 +5301,55 @@ end function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrolled, hotstart) if asset and asset.category==Group.Category.AIRPLANE or asset.category==Group.Category.HELICOPTER then - + -- Prepare the spawn template. local template=self:_SpawnAssetPrepareTemplate(asset, alias) - + -- Set route points. if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then - + -- Get flight path if the group goes to another warehouse by itself. template.route.points=self:_GetFlightplan(asset, self.airbase, request.warehouse.airbase) - + else - + -- Cold start (default). local _type=COORDINATE.WaypointType.TakeOffParking local _action=COORDINATE.WaypointAction.FromParkingArea - + -- Hot start. if hotstart then _type=COORDINATE.WaypointType.TakeOffParkingHot _action=COORDINATE.WaypointAction.FromParkingAreaHot end - + -- First route point is the warehouse airbase. template.route.points[1]=self.airbase:GetCoordinate():WaypointAir("BARO",_type,_action, 0, true, self.airbase, nil, "Spawnpoint") - + end - + -- Get airbase ID and category. local AirbaseID = self.airbase:GetID() local AirbaseCategory = self:GetAirbaseCategory() - + -- Check enough parking spots. if AirbaseCategory==Airbase.Category.HELIPAD or AirbaseCategory==Airbase.Category.SHIP then - + --TODO Figure out what's necessary in this case. - + else - + if #parking<#template.units then local text=string.format("ERROR: Not enough parking! Free parking = %d < %d aircraft to be spawned.", #parking, #template.units) self:_DebugMessage(text) return nil end - + end - + -- Position the units. for i=1,#template.units do - + -- Unit template. local unit = template.units[i] @@ -5306,67 +5357,67 @@ function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrol -- Helipads we take the position of the airbase location, since the exact location of the spawn point does not make sense. local coord=self.airbase:GetCoordinate() - + unit.x=coord.x unit.y=coord.z unit.alt=coord.y - + unit.parking_id = nil unit.parking = nil - + else - + local coord=parking[i].Coordinate --Core.Point#COORDINATE local terminal=parking[i].TerminalID --#number - + if self.Debug then coord:MarkToAll(string.format("Spawnplace unit %s terminal %d.", unit.name, terminal)) end - + unit.x=coord.x unit.y=coord.z unit.alt=coord.y - + unit.parking_id = nil unit.parking = terminal - + end - + if asset.livery then unit.livery_id = asset.livery end if asset.skill then unit.skill= asset.skill end - + end - + -- And template position. template.x = template.units[1].x template.y = template.units[1].y - + -- DCS bug workaround. Spawning helos in uncontrolled state on carriers causes a big spash! -- See https://forums.eagle.ru/showthread.php?t=219550 -- Should be solved in latest OB update 2.5.3.21708 --if AirbaseCategory == Airbase.Category.SHIP and asset.category==Group.Category.HELICOPTER then -- uncontrolled=false --end - + -- Uncontrolled spawning. template.uncontrolled=uncontrolled - + -- Debug info. self:T2({airtemplate=template}) - + -- Spawn group. local group=_DATABASE:Spawn(template) --Wrapper.Group#GROUP - + -- Activate group - should only be necessary for late activated groups. --group:Activate() - + return group end - + return nil end @@ -5380,14 +5431,14 @@ function WAREHOUSE:_SpawnAssetPrepareTemplate(asset, alias) -- Create an own copy of the template! local template=UTILS.DeepCopy(asset.template) - + -- Set unique name. template.name=alias - - -- Set current(!) coalition and country. + + -- Set current(!) coalition and country. template.CoalitionID=self:GetCoalition() template.CountryID=self:GetCountry() - + -- Nillify the group ID. template.groupId=nil @@ -5395,7 +5446,7 @@ function WAREHOUSE:_SpawnAssetPrepareTemplate(asset, alias) if asset.category==Group.Category.GROUND then --template.visible=false end - + -- No late activation. template.lateActivation=false @@ -5406,16 +5457,16 @@ function WAREHOUSE:_SpawnAssetPrepareTemplate(asset, alias) -- Handle units. for i=1,#template.units do - + -- Unit template. local unit = template.units[i] - + -- Nillify the unit ID. unit.unitId=nil - + -- Set unit name: -01, -02, ... unit.name=string.format("%s-%02d", template.name , i) - + end return template @@ -5437,50 +5488,50 @@ function WAREHOUSE:_RouteGround(group, request) -- Set speed to 70% of max possible. local _speed=group:GetSpeedMax()*0.7 - + -- Route waypoints. local Waypoints={} - - -- Check if an off road path has been defined. + + -- Check if an off road path has been defined. local hasoffroad=self:HasConnectionOffRoad(request.warehouse, self.Debug) - + -- Check if any off road paths have be defined. They have priority! if hasoffroad then -- Get off road path to remote warehouse. If more have been defined, pick one randomly. local remotename=request.warehouse.warehouse:GetName() local path=self.offroadpaths[remotename][math.random(#self.offroadpaths[remotename])] - + -- Loop over user defined shipping lanes. for i=1,#path do - + -- Shortcut and coordinate intellisense. local coord=path[i] --Core.Point#COORDINATE - + -- Get waypoint for coordinate. local Waypoint=coord:WaypointGround(_speed, "Off Road") - + -- Add waypoint to route. - table.insert(Waypoints, Waypoint) - end - + table.insert(Waypoints, Waypoint) + end + else - + -- Waypoints for road-to-road connection. Waypoints = group:TaskGroundOnRoad(request.warehouse.road, _speed, "Off Road", false, self.road) - + -- First waypoint = current position of the group. local FromWP=group:GetCoordinate():WaypointGround(_speed, "Off Road") table.insert(Waypoints, 1, FromWP) - + -- Final coordinate. local ToWP=request.warehouse.spawnzone:GetRandomCoordinate():WaypointGround(_speed, "Off Road") table.insert(Waypoints, #Waypoints+1, ToWP) - + end - + -- Task function triggering the arrived event at the last waypoint. - local TaskFunction = self:_SimpleTaskFunction("warehouse:_Arrived", group) + local TaskFunction = self:_SimpleTaskFunction("warehouse:_Arrived", group) -- Put task function on last waypoint. local Waypoint = Waypoints[#Waypoints] @@ -5488,7 +5539,7 @@ function WAREHOUSE:_RouteGround(group, request) -- Route group to destination. group:Route(Waypoints, 1) - + -- Set ROE and alaram state. group:OptionROEReturnFire() group:OptionAlarmStateGreen() @@ -5506,47 +5557,47 @@ function WAREHOUSE:_RouteNaval(group, request) -- Set speed to 80% of max possible. local _speed=group:GetSpeedMax()*0.8 - + -- Get shipping lane to remote warehouse. If more have been defined, pick one randomly. local remotename=request.warehouse.warehouse:GetName() local lane=self.shippinglanes[remotename][math.random(#self.shippinglanes[remotename])] - + if lane then - + -- Route waypoints. local Waypoints={} - + -- Loop over user defined shipping lanes. for i=1,#lane do - + -- Shortcut and coordinate intellisense. local coord=lane[i] --Core.Point#COORDINATE - + -- Get waypoint for coordinate. local Waypoint=coord:WaypointGround(_speed) - + -- Add waypoint to route. - table.insert(Waypoints, Waypoint) + table.insert(Waypoints, Waypoint) end - + -- Task function triggering the arrived event at the last waypoint. local TaskFunction = self:_SimpleTaskFunction("warehouse:_Arrived", group) - + -- Put task function on last waypoint. local Waypoint = Waypoints[#Waypoints] group:SetTaskWaypoint(Waypoint, TaskFunction) - + -- Route group to destination. - group:Route(Waypoints, 1) - + group:Route(Waypoints, 1) + -- Set ROE (Naval units dont have and alaram state.) group:OptionROEReturnFire() - + else -- This should not happen! Existance of shipping lane was checked before executing this request. self:E(self.wid..string.format("ERROR: No shipping lane defined for Naval asset!")) end - + end end @@ -5558,21 +5609,21 @@ end function WAREHOUSE:_RouteAir(aircraft) if aircraft and aircraft:IsAlive()~=nil then - + -- Debug info. self:T2(self.wid..string.format("RouteAir aircraft group %s alive=%s", aircraft:GetName(), tostring(aircraft:IsAlive()))) - + -- Give start command to activate uncontrolled aircraft within the next 60 seconds. local starttime=math.random(60) aircraft:StartUncontrolled(starttime) - + -- Debug info. self:T2(self.wid..string.format("RouteAir aircraft group %s alive=%s (after start command)", aircraft:GetName(), tostring(aircraft:IsAlive()))) - + -- Set ROE and alaram state. aircraft:OptionROEReturnFire() aircraft:OptionROTPassiveDefense() - + else self:E(string.format("ERROR: aircraft %s cannot be routed since it does not exist or is not alive %s!", tostring(aircraft:GetName()), tostring(aircraft:IsAlive()))) end @@ -5609,12 +5660,12 @@ end -- @param Wrapper.Group#GROUP group The group that arrived. function WAREHOUSE:_Arrived(group) self:_DebugMessage(string.format("Group %s arrived!", tostring(group:GetName()))) - + if group then --Trigger "Arrived event. self:__Arrived(1, group) end - + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -5626,7 +5677,7 @@ end -- @param Core.Event#EVENTDATA EventData Event data. function WAREHOUSE:_OnEventBirth(EventData) self:T3(self.wid..string.format("Warehouse %s (id=%s) captured event birth!", self.alias, self.uid)) - + if EventData and EventData.IniGroup then local group=EventData.IniGroup -- Note: Remember, group:IsAlive might(?) not return true here. @@ -5653,7 +5704,7 @@ function WAREHOUSE:_OnEventEngineStartup(EventData) if wid==self.uid then self:T(self.wid..string.format("Warehouse %s captured event engine startup of its asset unit %s.", self.alias, EventData.IniUnitName)) end - end + end end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -5663,14 +5714,14 @@ end -- @param Core.Event#EVENTDATA EventData Event data. function WAREHOUSE:_OnEventTakeOff(EventData) self:T3(self.wid..string.format("Warehouse %s captured event takeoff!",self.alias)) - + if EventData and EventData.IniGroup then local group=EventData.IniGroup local wid,aid,rid=self:_GetIDsFromGroup(group) if wid==self.uid then self:T(self.wid..string.format("Warehouse %s captured event takeoff of its asset unit %s.", self.alias, EventData.IniUnitName)) end - end + end end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -5680,19 +5731,19 @@ end -- @param Core.Event#EVENTDATA EventData Event data. function WAREHOUSE:_OnEventLanding(EventData) self:T3(self.wid..string.format("Warehouse %s captured event landing!", self.alias)) - + if EventData and EventData.IniGroup then local group=EventData.IniGroup - + -- Try to get UIDs from group name. local wid,aid,rid=self:_GetIDsFromGroup(group) - + -- Check that this group belongs to this warehouse. if wid~=nil and wid==self.uid then - + -- Debug info. self:T(self.wid..string.format("Warehouse %s captured event landing of its asset unit %s.", self.alias, EventData.IniUnitName)) - + end end end @@ -5704,14 +5755,14 @@ end -- @param Core.Event#EVENTDATA EventData Event data. function WAREHOUSE:_OnEventEngineShutdown(EventData) self:T3(self.wid..string.format("Warehouse %s captured event engine shutdown!", self.alias)) - + if EventData and EventData.IniGroup then local group=EventData.IniGroup local wid,aid,rid=self:_GetIDsFromGroup(group) if wid==self.uid then self:T(self.wid..string.format("Warehouse %s captured event engine shutdown of its asset unit %s.", self.alias, EventData.IniUnitName)) end - end + end end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -5722,49 +5773,49 @@ end function WAREHOUSE:_OnEventArrived(EventData) if EventData and EventData.IniUnit then - + -- Unit that arrived. local unit=EventData.IniUnit - + -- Check if unit is alive and on the ground. Engine shutdown can also be triggered in other situations! if unit and unit:IsAlive()==true and unit:InAir()==false then - + -- Get group. local group=EventData.IniGroup - - -- Get unique IDs from group name. + + -- Get unique IDs from group name. local wid,aid,rid=self:_GetIDsFromGroup(group) - + -- If all IDs are good we can assume it is a warehouse asset. if wid~=nil and aid~=nil and rid~=nil then - + -- Check that warehouse ID is right. if self.uid==wid then - + local request=self:_GetRequestOfGroup(group, self.pending) local istransport=self:_GroupIsTransport(group,request) - + -- Check if engine shutdown happend at right airbase because the event is also triggered in other situations. local rightairbase=group:GetCoordinate():GetClosestAirbase():GetName()==request.warehouse:GetAirbase():GetName() - + -- Check that group is cargo and not transport. - if istransport==false and rightairbase then - + if istransport==false and rightairbase then + -- Debug info. local text=string.format("Air asset group %s from warehouse %s arrived at its destination.", group:GetName(), self.alias) self:_InfoMessage(text) - + -- Trigger arrived event for this group. Note that each unit of a group will trigger this event. So the onafterArrived function needs to take care of that. -- Actually, we only take the first unit of the group that arrives. If it does, we assume the whole group arrived, which might not be the case, since -- some units might still be taxiing or whatever. Therefore, we add 10 seconds for each additional unit of the group until the first arrived event is triggered. local nunits=#group:GetUnits() local dt=10*(nunits-1)+1 -- one unit = 1 sec, two units = 11 sec, three units = 21 sec before we call the group arrived. self:__Arrived(dt, group) - + end - + end - + else self:T3(string.format("Group that arrived did not belong to a warehouse. Warehouse ID=%s, Asset ID=%s, Request ID=%s.", tostring(wid), tostring(aid), tostring(rid))) end @@ -5780,52 +5831,52 @@ end -- @param Core.Event#EVENTDATA EventData Event data. function WAREHOUSE:_OnEventCrashOrDead(EventData) self:T3(self.wid..string.format("Warehouse %s captured event dead or crash!", self.alias)) - + if EventData then - + -- Check if warehouse was destroyed. We compare the name of the destroyed unit. - if EventData.IniUnitName then + if EventData.IniUnitName then local warehousename=self.warehouse:GetName() if EventData.IniUnitName==warehousename then self:_DebugMessage(string.format("Warehouse %s alias %s was destroyed!", warehousename, self.alias)) - + -- Trigger Destroyed event. self:Destroyed() end end - + --self:I(self.wid..string.format("Warehouse %s captured event dead or crash or unit %s.", self.alias, tostring(EventData.IniUnitName))) - - -- Check if an asset unit was destroyed. + + -- Check if an asset unit was destroyed. if EventData.IniGroup then - - -- Group initiating the event. + + -- Group initiating the event. local group=EventData.IniGroup - + -- Get warehouse, asset and request IDs from the group name. local wid,aid,rid=self:_GetIDsFromGroup(group) - + -- Check that we have the right warehouse. if wid==self.uid then - + -- Debug message. self:T(self.wid..string.format("Warehouse %s captured event dead or crash of its asset unit %s.", self.alias, EventData.IniUnitName)) - + -- Loop over all pending requests and get the one belonging to this unit. for _,request in pairs(self.pending) do local request=request --#WAREHOUSE.Pendingitem - + -- This is the right request. if request.uid==rid then - + -- Update cargo and transport group sets of this request. We need to know if this job is finished. self:_UnitDead(EventData.IniUnit, request) - - end + + end end end end - end + end end --- A unit of a group just died. Update group sets in request. @@ -5837,10 +5888,10 @@ function WAREHOUSE:_UnitDead(deadunit, request) -- Flare unit deadunit:FlareRed() - + -- Group the dead unit belongs to. local group=deadunit:GetGroup() - + -- Check if this was the last unit of the group ==> whole group dead. local groupdead=true local nunits=0 @@ -5849,17 +5900,17 @@ function WAREHOUSE:_UnitDead(deadunit, request) -- Get current size of group and substract the unit that just died because it is not counted yet! nunits=group:GetSize()-1 nunits0=group:GetInitialSize() - + if nunits > 0 then groupdead=false - end + end end - - + + -- Here I need to get rid of the #CARGO at the end to obtain the original name again! local unitname=self:_GetNameWithOut(deadunit) local groupname=self:_GetNameWithOut(group) - + -- Debug message. local text=string.format("Unit %s died! #units=%d/%d ==> Group dead=%s (IsAlive=%s).", unitname, nunits, nunits0, tostring(groupdead), tostring(group:IsAlive())) self:T2(self.wid..text) @@ -5868,7 +5919,7 @@ function WAREHOUSE:_UnitDead(deadunit, request) if nunits<0 then self:E(self.wid.."ERROR: Number of units negative! This should not happen.") end - + -- Group is dead! if groupdead then self:T(self.wid..string.format("Group %s (transport=%s) is dead!", groupname, tostring(self:_GroupIsTransport(group,request)))) @@ -5879,57 +5930,57 @@ function WAREHOUSE:_UnitDead(deadunit, request) local asset=self:FindAssetInDB(group) self:AssetDead(asset, request) end - - + + -- Not sure what this does actually and if it would be better to set it to true. local NoTriggerEvent=true - + if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then - + --- -- Easy case: Group can simply be removed from the cargogroupset. --- - + -- Remove dead group from carg group set. if groupdead==true then request.cargogroupset:Remove(groupname, NoTriggerEvent) self:T(self.wid..string.format("Removed selfpropelled cargo %s: ncargo=%d.", groupname, request.cargogroupset:Count())) end - + else - + --- - -- Complicated case: Dead unit could be: + -- Complicated case: Dead unit could be: -- 1.) A Cargo unit (e.g. waiting to be picked up). -- 2.) A Transport unit which itself holds cargo groups. - --- - + --- + -- Check if this a cargo or transport group. local istransport=self:_GroupIsTransport(group,request) - + if istransport==true then - + -- Get the carrier unit table holding the cargo groups inside this carrier. local cargogroupnames=request.carriercargo[unitname] - + if cargogroupnames then - + -- Loop over all groups inside the destroyed carrier ==> all dead. for _,cargoname in pairs(cargogroupnames) do request.cargogroupset:Remove(cargoname, NoTriggerEvent) self:T(self.wid..string.format("Removed transported cargo %s inside dead carrier %s: ncargo=%d", cargoname, unitname, request.cargogroupset:Count())) end - + end - + -- Whole carrier group is dead. Remove it from the carrier group set. if groupdead then request.transportgroupset:Remove(groupname, NoTriggerEvent) self:T(self.wid..string.format("Removed transport %s: ntransport=%d", groupname, request.transportgroupset:Count())) - end - + end + elseif istransport==false then - + -- This must have been an alive cargo group that was killed outside the carrier, e.g. waiting to be transported or waiting to be put back. -- Remove dead group from cargo group set. if groupdead==true then @@ -5938,12 +5989,12 @@ function WAREHOUSE:_UnitDead(deadunit, request) -- This as well? --request.transportcargoset:RemoveCargosByName(RemoveCargoNames) end - - else + + else self:E(self.wid..string.format("ERROR: Group %s is neither cargo nor transport!", group:GetName())) end end - + end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -5954,29 +6005,29 @@ end -- @param Core.Event#EVENTDATA EventData Event data. function WAREHOUSE:_OnEventBaseCaptured(EventData) self:T3(self.wid..string.format("Warehouse %s captured event base captured!",self.alias)) - + -- This warehouse does not have an airbase and never had one. So it could not have been captured. if self.airbasename==nil then return end - + if EventData and EventData.Place then - + -- Place is the airbase that was captured. local airbase=EventData.Place --Wrapper.Airbase#AIRBASE - + -- Check that this airbase belongs or did belong to this warehouse. if EventData.PlaceName==self.airbasename then - + -- New coalition of airbase after it was captured. local NewCoalitionAirbase=airbase:GetCoalition() - + -- Debug info self:T(self.wid..string.format("Airbase of warehouse %s (coalition ID=%d) was captured! New owner coalition ID=%d.",self.alias, self:GetCoalition(), NewCoalitionAirbase)) - + -- So what can happen? -- Warehouse is blue, airbase is blue and belongs to warehouse and red captures it ==> self.airbase=nil - -- Warehouse is blue, airbase is blue self.airbase is nil and blue (re-)captures it ==> self.airbase=Event.Place + -- Warehouse is blue, airbase is blue self.airbase is nil and blue (re-)captures it ==> self.airbase=Event.Place if self.airbase==nil then -- New coalition is the same as of the warehouse ==> warehouse previously lost this airbase and now it was re-captured. if NewCoalitionAirbase == self:GetCoalition() then @@ -5988,7 +6039,7 @@ function WAREHOUSE:_OnEventBaseCaptured(EventData) self:AirbaseCaptured(NewCoalitionAirbase) end end - + end end end @@ -5999,7 +6050,7 @@ end -- @param Core.Event#EVENTDATA EventData Event data. function WAREHOUSE:_OnEventMissionEnd(EventData) self:T3(self.wid..string.format("Warehouse %s captured event mission end!",self.alias)) - + if self.autosave then self:Save(self.autosavepath, self.autosavefile) end @@ -6016,35 +6067,35 @@ function WAREHOUSE:_CheckConquered() -- Get coordinate and radius to check. local coord=self.zone:GetCoordinate() local radius=self.zone:GetRadius() - + -- Scan units in zone. local gotunits,_,_,units,_,_=coord:ScanObjects(radius, true, false, false) - + local Nblue=0 local Nred=0 local Nneutral=0 - + local CountryBlue=nil local CountryRed=nil local CountryNeutral=nil - + if gotunits then -- Loop over all units. for _,_unit in pairs(units) do local unit=_unit --Wrapper.Unit#UNIT - + local distance=coord:Get2DDistance(unit:GetCoordinate()) - + -- Filter only alive groud units. Also check distance again, because the scan routine might give some larger distances. if unit:IsGround() and unit:IsAlive() and distance <= radius then - + -- Get coalition and country. local _coalition=unit:GetCoalition() local _country=unit:GetCountry() - + -- Debug info. self:T2(self.wid..string.format("Unit %s in warehouse zone of radius=%d m. Coalition=%d, country=%d. Distance = %d m.",unit:GetName(), radius,_coalition,_country, distance)) - + -- Add up units for each side. if _coalition==coalition.side.BLUE then Nblue=Nblue+1 @@ -6056,15 +6107,15 @@ function WAREHOUSE:_CheckConquered() Nneutral=Nneutral+1 CountryNeutral=_country end - - end + + end end end - + -- Debug info. self:T(self.wid..string.format("Ground troops in warehouse zone: blue=%d, red=%d, neutral=%d", Nblue, Nred, Nneutral)) - - + + -- Figure out the new coalition if any. -- Condition is that only units of one coalition are within the zone. local newcoalition=self:GetCoalition() @@ -6088,7 +6139,7 @@ function WAREHOUSE:_CheckConquered() self:Captured(newcoalition, newcountry) return end - + -- Before a warehouse can be captured, it has to be attacked. -- That is, even if only enemy units are present it is not immediately captured in order to spawn all ground assets for defence. if self:GetCoalition()==coalition.side.BLUE then @@ -6099,7 +6150,7 @@ function WAREHOUSE:_CheckConquered() -- Blue warehouse was under attack by blue but no more blue units in zone. if self:IsAttacked() and Nred==0 then self:Defeated() - end + end elseif self:GetCoalition()==coalition.side.RED then -- Red Warehouse is running and we have blue units in the zone. if self:IsRunning() and Nblue>0 then @@ -6117,7 +6168,7 @@ function WAREHOUSE:_CheckConquered() self:Attacked(coalition.side.BLUE, CountryBlue) end end - + end --- Checks if the associated airbase still belongs to the warehouse. @@ -6125,26 +6176,26 @@ end function WAREHOUSE:_CheckAirbaseOwner() -- The airbasename is set at start and not deleted if the airbase was captured. if self.airbasename then - + local airbase=AIRBASE:FindByName(self.airbasename) local airbasecurrentcoalition=airbase:GetCoalition() - + if self.airbase then - + -- Warehouse has lost its airbase. if self:GetCoalition()~=airbasecurrentcoalition then self.airbase=nil end - + else - + -- Warehouse has re-captured the airbase. if self:GetCoalition()==airbasecurrentcoalition then self.airbase=airbase - end - + end + end - + end end @@ -6158,47 +6209,47 @@ function WAREHOUSE:_CheckRequestConsistancy(queue) -- Requests to delete. local invalid={} - + for _,_request in pairs(queue) do local request=_request --#WAREHOUSE.Queueitem - + -- Debug info. self:T2(self.wid..string.format("Checking request id=%d.", request.uid)) - + -- Let's assume everything is fine. local valid=true - + -- Check if at least one asset was requested. if request.nasset==0 then self:E(self.wid..string.format("ERROR: INVALID request. Request for zero assets not possible. Can happen when, e.g. \"all\" ground assets are requests but none in stock.")) valid=false end - + -- Request from enemy coalition? if self:GetCoalition()~=request.warehouse:GetCoalition() then self:E(self.wid..string.format("ERROR: INVALID request. Requesting warehouse is of wrong coaltion! Own coalition %s != %s of requesting warehouse.", self:GetCoalitionName(), request.warehouse:GetCoalitionName())) valid=false end - + -- Is receiving warehouse stopped? if request.warehouse:IsStopped() then self:E(self.wid..string.format("ERROR: INVALID request. Requesting warehouse is stopped!")) - valid=false + valid=false end -- Is receiving warehouse destroyed? if request.warehouse:IsDestroyed() then self:E(self.wid..string.format("ERROR: INVALID request. Requesting warehouse is destroyed!")) - valid=false + valid=false end - + -- Add request as unvalid and delete it later. if valid==false then self:E(self.wid..string.format("Got invalid request id=%d.", request.uid)) - table.insert(invalid, request) + table.insert(invalid, request) else self:T3(self.wid..string.format("Got valid request id=%d.", request.uid)) - end + end end -- Delete invalid requests. @@ -6206,7 +6257,7 @@ function WAREHOUSE:_CheckRequestConsistancy(queue) self:E(self.wid..string.format("Deleting INVALID request id=%d.",_request.uid)) self:_DeleteQueueItem(_request, self.queue) end - + end --- Check if a request is valid in general. If not, it will be removed from the queue. @@ -6219,12 +6270,12 @@ function WAREHOUSE:_CheckRequestValid(request) -- Check if number of requested assets is in stock. local _assets,_nassets,_enough=self:_FilterStock(self.stock, request.assetdesc, request.assetdescval, request.nasset) - + -- No assets in stock? Checks cannot be performed. if #_assets==0 then return true end - + -- Convert relative to absolute number if necessary. local nasset=request.nasset if type(request.nasset)=="string" then @@ -6234,10 +6285,10 @@ function WAREHOUSE:_CheckRequestValid(request) -- Debug check, request.nasset might be a string Quantity enumerator. local text=string.format("Request valid? Number of assets: requested=%s=%d, selected=%d, total=%d, enough=%s.", tostring(request.nasset), nasset,#_assets,_nassets, tostring(_enough)) self:T(text) - + -- First asset. Is representative for all filtered items in stock. local asset=_assets[1] --#WAREHOUSE.Assetitem - + -- Asset is air, ground etc. local asset_plane = asset.category==Group.Category.AIRPLANE local asset_helo = asset.category==Group.Category.HELICOPTER @@ -6250,158 +6301,159 @@ function WAREHOUSE:_CheckRequestValid(request) -- Assume everything is okay. local valid=true - + -- Category of the requesting warehouse airbase. local requestcategory=request.warehouse:GetAirbaseCategory() - + if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then ------------------------------------------- -- Case where the units go my themselves -- ------------------------------------------- if asset_air then - + if asset_plane then - + -- No airplane to or from FARPS. if requestcategory==Airbase.Category.HELIPAD or self:GetAirbaseCategory()==Airbase.Category.HELIPAD then self:E("ERROR: Incorrect request. Asset airplane requested but warehouse or requestor is HELIPAD/FARP!") valid=false end - + -- Category SHIP is not general enough! Fighters can go to carriers. Which fighters, is there an attibute? -- Also for carriers, attibute? - + elseif asset_helo then - + -- Helos need a FARP or AIRBASE or SHIP for spawning. Also at the the receiving warehouse. So even if they could go there they "cannot" be spawned again. -- Unless I allow spawning of helos in the the spawn zone. But one should place at least a FARP there. if self:GetAirbaseCategory()==-1 or requestcategory==-1 then self:E("ERROR: Incorrect request. Helos need a AIRBASE/HELIPAD/SHIP as home/destination base!") - valid=false + valid=false end - + end - + -- All aircraft need an airbase of any type at depature and destination. if self.airbase==nil or request.airbase==nil then - + self:E("ERROR: Incorrect request. Either warehouse or requesting warehouse does not have any kind of airbase!") valid=false - + else - + -- Check if enough parking spots are available. This checks the spots available in general, i.e. not the free spots. -- TODO: For FARPS/ships, is it possible to send more assets than parking spots? E.g. a FARPS has only four (or even one). -- TODO: maybe only check if spots > 0 for the necessary terminal type? At least for FARPS. - + -- Get necessary terminal type. - local termtype=self:_GetTerminal(asset.attribute) - + local termtype_dep=self:_GetTerminal(asset.attribute, self:GetAirbaseCategory()) + local termtype_des=self:_GetTerminal(asset.attribute, request.warehouse:GetAirbaseCategory()) + -- Get number of parking spots. - local np_departure=self.airbase:GetParkingSpotsNumber(termtype) - local np_destination=request.airbase:GetParkingSpotsNumber(termtype) - + local np_departure=self.airbase:GetParkingSpotsNumber(termtype_dep) + local np_destination=request.airbase:GetParkingSpotsNumber(termtype_des) + -- Debug info. - self:T(string.format("Asset attribute = %s, terminal type = %d, spots at departure = %d, destination = %d", asset.attribute, termtype, np_departure, np_destination)) - + self:T(string.format("Asset attribute = %s, DEPARTURE: terminal type = %d, spots = %d, DESTINATION: terminal type = %d, spots = %d", asset.attribute, termtype_dep, np_departure, termtype_des, np_destination)) + -- Not enough parking at sending warehouse. --if (np_departure < request.nasset) and not (self.category==Airbase.Category.SHIP or self.category==Airbase.Category.HELIPAD) then if np_departure < nasset then - self:E(string.format("ERROR: Incorrect request. Not enough parking spots of terminal type %d at warehouse. Available spots %d < %d necessary.", termtype, np_departure, nasset)) - valid=false + self:E(string.format("ERROR: Incorrect request. Not enough parking spots of terminal type %d at warehouse. Available spots %d < %d necessary.", termtype_dep, np_departure, nasset)) + valid=false end -- No parking at requesting warehouse. if np_destination == 0 then - self:E(string.format("ERROR: Incorrect request. No parking spots of terminal type %d at requesting warehouse. Available spots = %d!", termtype, np_destination)) - valid=false - end - + self:E(string.format("ERROR: Incorrect request. No parking spots of terminal type %d at requesting warehouse. Available spots = %d!", termtype_des, np_destination)) + valid=false + end + end - + elseif asset_ground then - + -- Check that both spawn zones are not in water. local inwater=self.spawnzone:GetCoordinate():IsSurfaceTypeWater() or request.warehouse.spawnzone:GetCoordinate():IsSurfaceTypeWater() - + if inwater then self:E("ERROR: Incorrect request. Ground asset requested but at least one spawn zone is in water!") valid=false end - + -- No ground assets directly to or from ships. -- TODO: May needs refinement if warehouse is on land and requestor is ship in harbour?! --if (requestcategory==Airbase.Category.SHIP or self:GetAirbaseCategory()==Airbase.Category.SHIP) then -- self:E("ERROR: Incorrect request. Ground asset requested but warehouse or requestor is SHIP!") -- valid=false --end - + if asset_train then - + -- Check if there is a valid path on rail. local hasrail=self:HasConnectionRail(request.warehouse) if not hasrail then self:E("ERROR: Incorrect request. No valid path on rail for train assets!") valid=false end - + else - + if self.warehouse:GetName()~=request.warehouse.warehouse:GetName() then - + -- Check if there is a valid path on road. local hasroad=self:HasConnectionRoad(request.warehouse) - + -- Check if there is a valid off road path. local hasoffroad=self:HasConnectionOffRoad(request.warehouse) - + if not (hasroad or hasoffroad) then self:E("ERROR: Incorrect request. No valid path on or off road for ground assets!") valid=false end - + end - + end - + elseif asset_naval then - + -- Check shipping lane. local shippinglane=self:HasConnectionNaval(request.warehouse) - + if not shippinglane then self:E("ERROR: Incorrect request. No shipping lane has been defined between warehouses!") valid=false - end - + end + end - - else + + else ------------------------------- -- Assests need a transport --- ------------------------------- if request.transporttype==WAREHOUSE.TransportType.AIRPLANE then - + -- Airplanes only to AND from airdromes. if self:GetAirbaseCategory()~=Airbase.Category.AIRDROME or requestcategory~=Airbase.Category.AIRDROME then self:E("ERROR: Incorrect request. Warehouse or requestor does not have an airdrome. No transport by plane possible!") valid=false end - + --TODO: Not sure if there are any transport planes that can land on a carrier? - + elseif request.transporttype==WAREHOUSE.TransportType.APC then - + -- Transport by ground units. - + -- No transport to or from ships if self:GetAirbaseCategory()==Airbase.Category.SHIP or requestcategory==Airbase.Category.SHIP then self:E("ERROR: Incorrect request. Warehouse or requestor is SHIP. No transport by APC possible!") valid=false end - + -- Check if there is a valid path on road. local hasroad=self:HasConnectionRoad(request.warehouse) if not hasroad then @@ -6410,37 +6462,37 @@ function WAREHOUSE:_CheckRequestValid(request) end elseif request.transporttype==WAREHOUSE.TransportType.HELICOPTER then - + -- Transport by helicopters ==> need airbase for spawning but not for delivering to the spawn zone of the receiver. if self:GetAirbaseCategory()==-1 then self:E("ERROR: Incorrect request. Warehouse has no airbase. Transport by helicopter not possible!") valid=false end - + elseif request.transporttype==WAREHOUSE.TransportType.SHIP then - + -- Transport by ship. self:E("ERROR: Incorrect request. Transport by SHIP not implemented yet!") valid=false - + elseif request.transporttype==WAREHOUSE.TransportType.TRAIN then - + -- Transport by train. self:E("ERROR: Incorrect request. Transport by TRAIN not implemented yet!") valid=false - + else -- No match. self:E("ERROR: Incorrect request. Transport type unknown!") valid=false end - + -- Airborne assets: check parking situation. if request.transporttype==WAREHOUSE.TransportType.AIRPLANE or request.transporttype==WAREHOUSE.TransportType.HELICOPTER then - + -- Check if number of requested assets is in stock. local _assets,_nassets,_enough=self:_FilterStock(self.stock, WAREHOUSE.Descriptor.ATTRIBUTE, request.transporttype, request.ntransport) - + -- Convert relative to absolute number if necessary. local nasset=request.ntransport if type(request.ntransport)=="string" then @@ -6452,49 +6504,50 @@ function WAREHOUSE:_CheckRequestValid(request) self:T(text) -- Get necessary terminal type for helos or transport aircraft. - local termtype=self:_GetTerminal(request.transporttype) - + local termtype=self:_GetTerminal(request.transporttype, self:GetAirbaseCategory()) + -- Get number of parking spots. local np_departure=self.airbase:GetParkingSpotsNumber(termtype) - + -- Debug info. self:T(self.wid..string.format("Transport attribute = %s, terminal type = %d, spots at departure = %d.", request.transporttype, termtype, np_departure)) - + -- Not enough parking at sending warehouse. --if (np_departure < request.nasset) and not (self.category==Airbase.Category.SHIP or self.category==Airbase.Category.HELIPAD) then if np_departure < nasset then self:E(self.wid..string.format("ERROR: Incorrect request. Not enough parking spots of terminal type %d at warehouse. Available spots %d < %d necessary.", termtype, np_departure, nasset)) valid=false end - + -- Planes also need parking at the receiving warehouse. if request.transporttype==WAREHOUSE.TransportType.AIRPLANE then - + -- Total number of parking spots for transport planes at destination. + termtype=self:_GetTerminal(request.transporttype, request.warehouse:GetAirbaseCategory()) local np_destination=request.airbase:GetParkingSpotsNumber(termtype) -- Debug info. self:T(self.wid..string.format("Transport attribute = %s: total # of spots (type=%d) at destination = %d.", asset.attribute, termtype, np_destination)) - + -- No parking at requesting warehouse. if np_destination == 0 then self:E(string.format("ERROR: Incorrect request. No parking spots of terminal type %d at requesting warehouse for transports. Available spots = %d!", termtype, np_destination)) - valid=false + valid=false end end - + end - + end - + -- Add request as unvalid and delete it later. if valid==false then self:E(self.wid..string.format("ERROR: Got invalid request id=%d.", request.uid)) else self:T3(self.wid..string.format("Request id=%d valid :)", request.uid)) end - + return valid end @@ -6510,20 +6563,20 @@ function WAREHOUSE:_CheckRequestNow(request) if (request.warehouse:IsRunning()==false) and not (request.toself and self:IsAttacked()) then local text=string.format("Warehouse %s: Request denied! Receiving warehouse %s is not running. Current state %s.", self.alias, request.warehouse.alias, request.warehouse:GetState()) self:_InfoMessage(text, 5) - + return false end - + -- If no transport is requested, assets need to be mobile unless it is a self request. local onlymobile=false if type(request.transport)=="number" and request.ntransport==0 and not request.toself then onlymobile=true end - + -- Check if number of requested assets is in stock. local _assets,_nassets,_enough=self:_FilterStock(self.stock, request.assetdesc, request.assetdescval, request.nasset, onlymobile) - - + + -- Check if enough assets are in stock. if not _enough then local text=string.format("Warehouse %s: Request ID=%d denied! Not enough (cargo) assets currently available.", self.alias, request.uid) @@ -6532,107 +6585,107 @@ function WAREHOUSE:_CheckRequestNow(request) self:T(self.wid..text) return false end - + local _transports local _assetattribute local _assetcategory - + -- Check if at least one (cargo) asset is available. if _nassets>0 then -- Get the attibute of the requested asset. _assetattribute=_assets[1].attribute - _assetcategory=_assets[1].category - - -- Check available parking for air asset units. + _assetcategory=_assets[1].category + + -- Check available parking for air asset units. if self.airbase and (_assetcategory==Group.Category.AIRPLANE or _assetcategory==Group.Category.HELICOPTER) then - + local Parking=self:_FindParkingForAssets(self.airbase,_assets) - + --if Parking==nil and not (self.category==Airbase.Category.HELIPAD) then if Parking==nil then local text=string.format("Warehouse %s: Request denied! Not enough free parking spots for all requested assets at the moment.", self.alias) self:_InfoMessage(text, 5) - + return false end - + end - + -- Add this here or gettransport fails request.cargoassets=_assets - - end - + + end + -- Check that a transport units. if request.transporttype ~= WAREHOUSE.TransportType.SELFPROPELLED then -- Get best transports for this asset pack. _transports=self:_GetTransportsForAssets(request) - + -- Check if at least one transport asset is available. if #_transports>0 then - + -- Get the attibute of the transport units. local _transportattribute=_transports[1].attribute local _transportcategory=_transports[1].category - + -- Check available parking for transport units. if self.airbase and (_transportcategory==Group.Category.AIRPLANE or _transportcategory==Group.Category.HELICOPTER) then local Parking=self:_FindParkingForAssets(self.airbase,_transports) if Parking==nil then local text=string.format("Warehouse %s: Request denied! Not enough free parking spots for all transports at the moment.", self.alias) self:_InfoMessage(text, 5) - + return false end end - + else -- Not enough or the right transport carriers. local text=string.format("Warehouse %s: Request denied! Not enough transport carriers available at the moment.", self.alias) self:_InfoMessage(text, 5) - - return false - end + + return false + end else - + -- Self propelled case. Nothing to do for now. - + -- Ground asset checks. if _assetcategory==Group.Category.GROUND then - + -- Distance between warehouse and spawn zone. local dist=self.warehouse:GetCoordinate():Get2DDistance(self.spawnzone:GetCoordinate()) - + -- Check min dist to spawn zone. if dist>self.spawnzonemaxdist then -- Not close enough to spawn zone. local text=string.format("Warehouse %s: Request denied! Not close enough to spawn zone. Distance = %d m. We need to be at least within %d m range to spawn.", self.alias, dist, self.spawnzonemaxdist) - self:_InfoMessage(text, 5) + self:_InfoMessage(text, 5) return false end - + end - + end -- Set chosen cargo assets. request.cargoassets=_assets request.cargoattribute=_assets[1].attribute - request.cargocategory=_assets[1].category + request.cargocategory=_assets[1].category request.nasset=#_assets -- Debug info: - local text=string.format("Selected cargo assets, attibute=%s, category=%d:\n", request.cargoattribute, request.cargocategory) + local text=string.format("Selected cargo assets, attibute=%s, category=%d:\n", request.cargoattribute, request.cargocategory) for _i,_asset in pairs(_assets) do local asset=_asset --#WAREHOUSE.Assetitem text=text..string.format("%d) name=%s, type=%s, category=%d, #units=%d",_i, asset.templatename, asset.unittype, asset.category, asset.nunits) end - self:T(self.wid..text) + self:T(self.wid..text) if request.transporttype ~= WAREHOUSE.TransportType.SELFPROPELLED then @@ -6641,21 +6694,21 @@ function WAREHOUSE:_CheckRequestNow(request) request.transportattribute=_transports[1].attribute request.transportcategory=_transports[1].category request.ntransport=#_transports - + -- Debug info: - local text=string.format("Selected transport assets, attibute=%s, category=%d:\n", request.transportattribute, request.transportcategory) + local text=string.format("Selected transport assets, attibute=%s, category=%d:\n", request.transportattribute, request.transportcategory) for _i,_asset in pairs(_transports) do local asset=_asset --#WAREHOUSE.Assetitem text=text..string.format("%d) name=%s, type=%s, category=%d, #units=%d\n",_i, asset.templatename, asset.unittype, asset.category, asset.nunits) end self:T(self.wid..text) - + end - + return true end ----Get (optimized) transport carriers for the given assets to be transported. +---Get (optimized) transport carriers for the given assets to be transported. -- @param #WAREHOUSE self -- @param #WAREHOUSE.Pendingitem Chosen request. function WAREHOUSE:_GetTransportsForAssets(request) @@ -6668,29 +6721,29 @@ function WAREHOUSE:_GetTransportsForAssets(request) local cargoset=request.transportcargoset -- TODO: Get weight and cargo bay from CARGO_GROUP - --local cargogroup=CARGO_GROUP:New(CargoGroup,Type,Name,LoadRadius,NearRadius) + --local cargogroup=CARGO_GROUP:New(CargoGroup,Type,Name,LoadRadius,NearRadius) --cargogroup:GetWeight() - + -- Sort transport carriers w.r.t. cargo bay size. local function sort_transports(a,b) return a.cargobaymax>b.cargobaymax end - + -- Sort cargo assets w.r.t. weight in assending order. local function sort_cargoassets(a,b) return a.weight>b.weight end - + -- Sort tables. table.sort(transports, sort_transports) table.sort(cargoassets, sort_cargoassets) - + -- Total cargo bay size of all groups. self:T2(self.wid.."Transport capability:") local totalbay=0 for i=1,#transports do local transport=transports[i] --#WAREHOUSE.Assetitem - for j=1,transport.nunits do + for j=1,transport.nunits do totalbay=totalbay+transport.cargobay[j] self:T2(self.wid..string.format("Cargo bay = %d (unit=%d)", transport.cargobay[j], j)) end @@ -6704,91 +6757,91 @@ function WAREHOUSE:_GetTransportsForAssets(request) local asset=cargoassets[i] --#WAREHOUSE.Assetitem totalcargoweight=totalcargoweight+asset.weight self:T2(self.wid..string.format("weight = %d", asset.weight)) - end + end self:T2(self.wid..string.format("Total weight = %d", totalcargoweight)) - + -- Transports used. local used_transports={} - + -- Loop over all transport groups, largest cargobaymax to smallest. for i=1,#transports do - + -- Shortcut for carrier and cargo bay local transport=transports[i] - -- Cargo put into carrier. + -- Cargo put into carrier. local putintocarrier={} - + -- Cargo assigned to this transport group? local used=false - + -- Loop over all units for k=1,transport.nunits do - + -- Get cargo bay of this carrier. local cargobay=transport.cargobay[k] - + -- Loop over cargo assets. for j,asset in pairs(cargoassets) do local asset=asset --#WAREHOUSE.Assetitem - + -- How many times does the cargo fit into the carrier? local delta=cargobay-asset.weight --env.info(string.format("k=%d, j=%d delta=%d cargobay=%d weight=%d", k, j, delta, cargobay, asset.weight)) - + --self:E(self.wid..string.format("%s unit %d loads cargo uid=%d: bayempty=%02d, bayloaded = %02d - weight=%02d", transport.templatename, k, asset.uid, transport.cargobay[k], cargobay, asset.weight)) - + -- Cargo fits into carrier if delta>=0 then -- Reduce remaining cargobay. cargobay=cargobay-asset.weight self:T3(self.wid..string.format("%s unit %d loads cargo uid=%d: bayempty=%02d, bayloaded = %02d - weight=%02d", transport.templatename, k, asset.uid, transport.cargobay[k], cargobay, asset.weight)) - + -- Remember this cargo and remove it so it does not get loaded into other carriers. table.insert(putintocarrier, j) - + -- This transport group is used. used=true - else + else self:T2(self.wid..string.format("Carrier unit %s too small for cargo asset %s ==> cannot be used! Cargo bay - asset weight = %d kg", transport.templatename, asset.templatename, delta)) end - - end -- loop over assets + + end -- loop over assets end -- loop over units - + -- Remove cargo assets from list. Needs to be done back-to-front in order not to confuse the loop. for j=#putintocarrier,1, -1 do - + local nput=putintocarrier[j] local cargo=cargoassets[nput] - + -- Need to check if multiple units in a group and the group has already been removed! -- TODO: This might need to be improved but is working okay so far. if cargo then -- Remove this group because it was used. - self:T2(self.wid..string.format("Cargo id=%d assigned for carrier id=%d", cargo.uid, transport.uid)) + self:T2(self.wid..string.format("Cargo id=%d assigned for carrier id=%d", cargo.uid, transport.uid)) table.remove(cargoassets, nput) end end - + -- Cargo was assined for this carrier. if used then table.insert(used_transports, transport) end - + -- Convert relative quantity (all, half) to absolute number if necessary. local ntrans=self:_QuantityRel2Abs(request.ntransport, #transports) - + -- Max number of transport groups reached? if #used_transports >= ntrans then request.ntransport=#used_transports break end end - + -- Debug info. local text=string.format("Used Transports for request %d to warehouse %s:\n", request.uid, request.warehouse.alias) - local totalcargobay=0 + local totalcargobay=0 for _i,_transport in pairs(used_transports) do local transport=_transport --#WAREHOUSE.Assetitem text=text..string.format("%d) %s: cargobay tot = %d kg, cargobay max = %d kg, nunits=%d\n", _i, transport.unittype, transport.cargobaytot, transport.cargobaymax, transport.nunits) @@ -6800,7 +6853,7 @@ function WAREHOUSE:_GetTransportsForAssets(request) text=text..string.format("Total cargo bay capacity = %.1f kg\n", totalcargobay) text=text..string.format("Total cargo weight = %.1f kg\n", totalcargoweight) text=text..string.format("Minimum number of runs = %.1f", totalcargoweight/totalcargobay) - self:_DebugMessage(text) + self:_DebugMessage(text) return used_transports end @@ -6823,7 +6876,7 @@ function WAREHOUSE:_QuantityRel2Abs(relative, ntot) elseif relative==WAREHOUSE.Quantity.HALF then nabs=UTILS.Round(ntot/2) elseif relative==WAREHOUSE.Quantity.THIRD then - nabs=UTILS.Round(ntot/3) + nabs=UTILS.Round(ntot/3) elseif relative==WAREHOUSE.Quantity.QUARTER then nabs=UTILS.Round(ntot/4) else @@ -6832,7 +6885,7 @@ function WAREHOUSE:_QuantityRel2Abs(relative, ntot) else nabs=relative end - + self:T2(self.wid..string.format("Relative %s: tot=%d, abs=%.2f", tostring(relative), ntot, nabs)) return nabs @@ -6848,24 +6901,24 @@ function WAREHOUSE:_CheckQueue() -- Search for a request we can execute. local request=nil --#WAREHOUSE.Queueitem - + local invalid={} local gotit=false for _,_qitem in ipairs(self.queue) do local qitem=_qitem --#WAREHOUSE.Queueitem - + -- Check if request is valid in general. local valid=self:_CheckRequestValid(qitem) - + -- Check if request is possible now. local okay=false - if valid then + if valid then okay=self:_CheckRequestNow(qitem) else -- Remember invalid request and delete later in order not to confuse the loop. table.insert(invalid, qitem) end - + -- Get the first valid request that can be executed now. if okay and valid and not gotit then request=qitem @@ -6873,7 +6926,7 @@ function WAREHOUSE:_CheckQueue() break end end - + -- Delete invalid requests. for _,_request in pairs(invalid) do self:T(self.wid..string.format("Deleting invalid request id=%d.",_request.uid)) @@ -6898,27 +6951,31 @@ function WAREHOUSE:_SimpleTaskFunction(Function, group) -- Task script. local DCSScript = {} --DCSScript[#DCSScript+1] = string.format('env.info(\"WAREHOUSE: Simple task function called!\") ') - DCSScript[#DCSScript+1] = string.format('local mygroup = GROUP:FindByName(\"%s\") ', groupname) -- The group that executes the task function. Very handy with the "...". - DCSScript[#DCSScript+1] = string.format("local mystatic = STATIC:FindByName(\"%s\") ", warehouse) -- The static that holds the warehouse self object. - DCSScript[#DCSScript+1] = string.format('local warehouse = mystatic:GetState(mystatic, \"WAREHOUSE\") ') -- Get the warehouse self object from the static. - DCSScript[#DCSScript+1] = string.format('%s(mygroup)', Function) -- Call the function, e.g. myfunction.(warehouse,mygroup) + DCSScript[#DCSScript+1] = string.format('local mygroup = GROUP:FindByName(\"%s\") ', groupname) -- The group that executes the task function. Very handy with the "...". + if self.isunit then + DCSScript[#DCSScript+1] = string.format("local mywarehouse = UNIT:FindByName(\"%s\") ", warehouse) -- The unit that holds the warehouse self object. + else + DCSScript[#DCSScript+1] = string.format("local mywarehouse = STATIC:FindByName(\"%s\") ", warehouse) -- The static that holds the warehouse self object. + end + DCSScript[#DCSScript+1] = string.format('local warehouse = mywarehouse:GetState(mywarehouse, \"WAREHOUSE\") ') -- Get the warehouse self object from the static. + DCSScript[#DCSScript+1] = string.format('%s(mygroup)', Function) -- Call the function, e.g. myfunction.(warehouse,mygroup) -- Create task. local DCSTask = CONTROLLABLE.TaskWrappedAction(self, CONTROLLABLE.CommandDoScript(self, table.concat(DCSScript))) - + return DCSTask end --- Get the proper terminal type based on generalized attribute of the group. --@param #WAREHOUSE self --@param #WAREHOUSE.Attribute _attribute Generlized attibute of unit. +--@param #number _category Airbase category. --@return Wrapper.Airbase#AIRBASE.TerminalType Terminal type for this group. -function WAREHOUSE:_GetTerminal(_attribute) +function WAREHOUSE:_GetTerminal(_attribute, _category) -- Default terminal is "large". local _terminal=AIRBASE.TerminalType.OpenBig - - + if _attribute==WAREHOUSE.Attribute.AIR_FIGHTER then -- Fighter ==> small. _terminal=AIRBASE.TerminalType.FighterAircraft @@ -6928,8 +6985,17 @@ function WAREHOUSE:_GetTerminal(_attribute) elseif _attribute==WAREHOUSE.Attribute.AIR_TRANSPORTHELO or _attribute==WAREHOUSE.Attribute.AIR_ATTACKHELO then -- Helicopter. _terminal=AIRBASE.TerminalType.HelicopterUsable + else + --_terminal=AIRBASE.TerminalType.OpenMedOrBig end - + + -- For ships, we allow medium spots for all fixed wing aircraft. There are smaller tankers and AWACS aircraft that can use a carrier. + if _category==Airbase.Category.SHIP then + if not (_attribute==WAREHOUSE.Attribute.AIR_TRANSPORTHELO or _attribute==WAREHOUSE.Attribute.AIR_ATTACKHELO) then + _terminal=AIRBASE.TerminalType.OpenMedOrBig + end + end + return _terminal end @@ -6955,27 +7021,27 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) local safedist=(l1/2+l2/2)*1.05 -- 5% safety margine added to safe distance! local safe = (dist > safedist) self:T3(string.format("l1=%.1f l2=%.1f s=%.1f d=%.1f ==> safe=%s", l1,l2,safedist,dist,tostring(safe))) - return safe + return safe end - + -- Get parking spot data table. This contains all free and "non-free" spots. local parkingdata=airbase:GetParkingSpotsTable() - + -- List of obstacles. local obstacles={} - + -- Loop over all parking spots and get the currently present obstacles. -- How long does this take on very large airbases, i.e. those with hundereds of parking spots? Seems to be okay! for _,parkingspot in pairs(parkingdata) do - + -- Coordinate of the parking spot. local _spot=parkingspot.Coordinate -- Core.Point#COORDINATE local _termid=parkingspot.TerminalID - + -- Scan a radius of 100 meters around the spot. local _,_,_,_units,_statics,_sceneries=_spot:ScanObjects(scanradius, scanunits, scanstatics, scanscenery) - -- Check all units. + -- Check all units. for _,_unit in pairs(_units) do local unit=_unit --Wrapper.Unit#UNIT local _coord=unit:GetCoordinate() @@ -6983,7 +7049,7 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) local _name=unit:GetName() table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="unit"}) end - + -- Check all statics. for _,static in pairs(_statics) do local _vec3=static:getPoint() @@ -6992,7 +7058,7 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) local _size=self:_GetObjectSize(static) table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="static"}) end - + -- Check all scenery. for _,scenery in pairs(_sceneries) do local _vec3=scenery:getPoint() @@ -7001,63 +7067,56 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) local _size=self:_GetObjectSize(scenery) table.insert(obstacles,{coord=_coord, size=_size, name=_name, type="scenery"}) end - - --[[ - -- TODO Clients? Unoccupied client aircraft are also important! Are they already included in scanned units maybe? - local clients=_DATABASE.CLIENTS - for _,_client in pairs(clients) do - local client=_client --Wrapper.Client#CLIENT - env.info(string.format("FF Client name %s", client:GetName())) - local unit=UNIT:FindByName(client:GetName()) - --local unit=client:GetClientGroupUnit() - local _coord=unit:GetCoordinate() - local _name=unit:GetName() - local _size=self:_GetObjectSize(client:GetClientGroupDCSUnit()) - table.insert(obstacles,{coord=_coord, size=_size, name=_name, type="client"}) - end - ]] + end - + -- Parking data for all assets. local parking={} -- Loop over all assets that need a parking psot. for _,asset in pairs(assets) do local _asset=asset --#WAREHOUSE.Assetitem - + -- Get terminal type of this asset - local terminaltype=self:_GetTerminal(asset.attribute) - + local terminaltype=self:_GetTerminal(asset.attribute, self:GetAirbaseCategory()) + -- Asset specific parking. parking[_asset.uid]={} - + -- Loop over all units - each one needs a spot. for i=1,_asset.nunits do - + -- Loop over all parking spots. local gotit=false - for _,_parkingspot in pairs(parkingdata) do + for _,_parkingspot in pairs(parkingdata) do local parkingspot=_parkingspot --Wrapper.Airbase#AIRBASE.ParkingSpot - + -- Check correct terminal type for asset. We don't want helos in shelters etc. - if AIRBASE._CheckTerminalType(parkingspot.TerminalType, terminaltype) then - + if AIRBASE._CheckTerminalType(parkingspot.TerminalType, terminaltype) then + -- Coordinate of the parking spot. local _spot=parkingspot.Coordinate -- Core.Point#COORDINATE local _termid=parkingspot.TerminalID local _toac=parkingspot.TOAC - + --env.info(string.format("FF asset=%s (id=%d): needs terminal type=%d, id=%d, #obstacles=%d", _asset.templatename, _asset.uid, terminaltype, _termid, #obstacles)) - - -- Loop over all obstacles. + local free=true local problem=nil + + -- Safe parking using TO_AC from DCS result. + if self.safeparking and _toac then + free=false + self:T("Parking spot %d is occupied by other aircraft taking off or landing.", _termid) + end + + -- Loop over all obstacles. for _,obstacle in pairs(obstacles) do - + -- Check if aircraft overlaps with any obstacle. local dist=_spot:Get2DDistance(obstacle.coord) local safe=_overlap(_asset.size, obstacle.size, dist) - + -- Spot is blocked. if not safe then --env.info(string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is NOT SAFE", _asset.templatename, _asset.uid, _termid, dist)) @@ -7068,25 +7127,25 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) else --env.info(string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is SAFE", _asset.templatename, _asset.uid, _termid, dist)) end - + end - + -- Check if spot is free if free then - + -- Add parkingspot for this asset unit. table.insert(parking[_asset.uid], parkingspot) - + self:T(self.wid..string.format("Parking spot #%d is free for asset id=%d!", _termid, _asset.uid)) - + -- Add the unit as obstacle so that this spot will not be available for the next unit. table.insert(obstacles, {coord=_spot, size=_asset.size, name=_asset.templatename, type="asset"}) - + gotit=true break - + else - + -- Debug output for occupied spots. self:T(self.wid..string.format("Parking spot #%d is occupied or not big enough!", _termid)) if self.Debug then @@ -7094,20 +7153,20 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) local text=string.format("Obstacle blocking spot #%d is %s type %s with size=%.1f m and distance=%.1f m.", _termid, problem.name, problem.type, problem.size, problem.dist) coord:MarkToAll(string.format(text)) end - + end - + end -- check terminal type end -- loop over parking spots - + -- No parking spot for at least one asset :( if not gotit then self:T(self.wid..string.format("WARNING: No free parking spot for asset id=%d",_asset.uid)) return nil - end + end end -- loop over asset units end -- loop over asset groups - + return parking end @@ -7121,7 +7180,7 @@ function WAREHOUSE:_GetRequestOfGroup(group, queue) -- Get warehouse, asset and request ID from group name. local wid,aid,rid=self:_GetIDsFromGroup(group) - + -- Find the request. for _,_request in pairs(queue) do local request=_request --#WAREHOUSE.Queueitem @@ -7129,7 +7188,7 @@ function WAREHOUSE:_GetRequestOfGroup(group, queue) return request end end - + end --- Is the group a used as transporter for a given request? @@ -7142,31 +7201,31 @@ function WAREHOUSE:_GroupIsTransport(group, request) -- Name of the group under question. local groupname=self:_GetNameWithOut(group) - if request.transportgroupset then + if request.transportgroupset then local transporters=request.transportgroupset:GetSetObjects() - + for _,transport in pairs(transporters) do if transport:GetName()==groupname then return true end end end - + if request.cargogroupset then local cargos=request.cargogroupset:GetSetObjects() - + for _,cargo in pairs(cargos) do if self:_GetNameWithOut(cargo)==groupname then return false end end - end - + end + return nil end ---- Creates a unique name for spawned assets. From the group name the original warehouse, global asset and the request can be derived. +--- Creates a unique name for spawned assets. From the group name the original warehouse, global asset and the request can be derived. -- @param #WAREHOUSE self -- @param #WAREHOUSE.Assetitem _assetitem Asset for which the name is created. -- @param #WAREHOUSE.Queueitem _queueitem (Optional) Request specific name. @@ -7227,16 +7286,16 @@ function WAREHOUSE:_GetIDsFromGroup(group) ---@param #string text The text to analyse. local function analyse(text) - + -- Get rid of #0001 tail from spawn. local unspawned=UTILS.Split(text, "#")[1] - - -- Split keywords. + + -- Split keywords. local keywords=UTILS.Split(unspawned, "_") local _wid=nil -- warehouse UID local _aid=nil -- asset UID local _rid=nil -- request UID - + -- Loop over keys. for _,keys in pairs(keywords) do local str=UTILS.Split(keys, "-") @@ -7248,26 +7307,26 @@ function WAREHOUSE:_GetIDsFromGroup(group) _aid=tonumber(val) elseif key:find("RID") then _rid=tonumber(val) - end + end end - + return _wid,_aid,_rid end - + if group then - + -- Group name local name=group:GetName() - + -- Get ids local wid,aid,rid=analyse(name) - + -- Debug info - self:T3(self.wid..string.format("Group Name = %s", tostring(name))) + self:T3(self.wid..string.format("Group Name = %s", tostring(name))) self:T3(self.wid..string.format("Warehouse ID = %s", tostring(wid))) self:T3(self.wid..string.format("Asset ID = %s", tostring(aid))) self:T3(self.wid..string.format("Request ID = %s", tostring(rid))) - + return wid,aid,rid else self:E("WARNING: Group not found in GetIDsFromGroup() function!") @@ -7307,34 +7366,34 @@ function WAREHOUSE:_FilterStock(stock, descriptor, attribute, nmax, mobile) end end end - + -- Treat case where ntot=0, i.e. no assets at all. if ntot==0 then return filtered, ntot, false end - + -- Convert relative to absolute number if necessary. nmax=self:_QuantityRel2Abs(nmax,ntot) -- Loop over stock items. for _i,_asset in ipairs(stock) do local asset=_asset --#WAREHOUSE.Assetitem - + -- Check if asset has the right attribute. if asset[descriptor]==attribute then - + -- Check if asset has to be mobile. if (mobile and asset.speedmax>0) or (not mobile) then - + -- Add asset to filtered table. table.insert(filtered, asset) - + -- Break loop if nmax was reached. if nmax~=nil and #filtered>=nmax then return filtered, ntot, true end - - end + + end end end @@ -7367,24 +7426,24 @@ function WAREHOUSE:_GetAttribute(group) local attribute=WAREHOUSE.Attribute.OTHER_UNKNOWN --#WAREHOUSE.Attribute if group then - + ----------- --- Air --- - ----------- + ----------- -- Planes local transportplane=group:HasAttribute("Transports") and group:HasAttribute("Planes") local awacs=group:HasAttribute("AWACS") - local fighter=group:HasAttribute("Fighters") or group:HasAttribute("Interceptors") or group:HasAttribute("Multirole fighters") or (group:HasAttribute("Bombers") and not group:HasAttribute("Strategic bombers")) + local fighter=group:HasAttribute("Fighters") or group:HasAttribute("Interceptors") or group:HasAttribute("Multirole fighters") or (group:HasAttribute("Bombers") and not group:HasAttribute("Strategic bombers")) local bomber=group:HasAttribute("Strategic bombers") - local tanker=group:HasAttribute("Tankers") - local uav=group:HasAttribute("UAVs") + local tanker=group:HasAttribute("Tankers") + local uav=group:HasAttribute("UAVs") -- Helicopters local transporthelo=group:HasAttribute("Transport helicopters") local attackhelicopter=group:HasAttribute("Attack helicopters") -------------- --- Ground --- - -------------- + -------------- -- Ground local apc=group:HasAttribute("Infantry carriers") local truck=group:HasAttribute("Trucks") and group:GetCategory()==Group.Category.GROUND @@ -7399,13 +7458,13 @@ function WAREHOUSE:_GetAttribute(group) ------------- --- Naval --- - ------------- + ------------- -- Ships local aircraftcarrier=group:HasAttribute("Aircraft Carriers") local warship=group:HasAttribute("Heavy armed ships") local armedship=group:HasAttribute("Armed ships") local unarmedship=group:HasAttribute("Unarmed ships") - + -- Define attribute. Order is important. if transportplane then @@ -7439,7 +7498,7 @@ function WAREHOUSE:_GetAttribute(group) elseif sam then attribute=WAREHOUSE.Attribute.GROUND_SAM elseif truck then - attribute=WAREHOUSE.Attribute.GROUND_TRUCK + attribute=WAREHOUSE.Attribute.GROUND_TRUCK elseif train then attribute=WAREHOUSE.Attribute.GROUND_TRAIN elseif aircraftcarrier then @@ -7447,7 +7506,7 @@ function WAREHOUSE:_GetAttribute(group) elseif warship then attribute=WAREHOUSE.Attribute.NAVAL_WARSHIP elseif armedship then - attribute=WAREHOUSE.Attribute.NAVAL_ARMEDSHIP + attribute=WAREHOUSE.Attribute.NAVAL_ARMEDSHIP elseif unarmedship then attribute=WAREHOUSE.Attribute.NAVAL_UNARMEDSHIP else @@ -7459,7 +7518,7 @@ function WAREHOUSE:_GetAttribute(group) attribute=WAREHOUSE.Attribute.AIR_OTHER else attribute=WAREHOUSE.Attribute.OTHER_UNKNOWN - end + end end end @@ -7482,7 +7541,7 @@ function WAREHOUSE:_GetObjectSize(DCSobject) return math.max(x,z), x , y, z end return 0,0,0,0 -end +end --- Returns the number of assets for each generalized attribute. -- @param #WAREHOUSE self @@ -7526,7 +7585,7 @@ end -- @param #table queue The queue from which the item should be deleted. function WAREHOUSE:_DeleteQueueItem(qitem, queue) self:F({qitem=qitem, queue=queue}) - + for i=1,#queue do local _item=queue[i] --#WAREHOUSE.Queueitem if _item.uid==qitem.uid then @@ -7561,14 +7620,14 @@ function WAREHOUSE:_PrintQueue(queue, name) -- Init string. local text=string.format("%s at %s: %s",name, self.alias, total) - + for i,qitem in ipairs(queue) do local qitem=qitem --#WAREHOUSE.Pendingitem - + local uid=qitem.uid local prio=qitem.prio local clock="N/A" - if qitem.timestamp then + if qitem.timestamp then clock=tostring(UTILS.SecondsToClock(qitem.timestamp)) end local assignment=tostring(qitem.assignment) @@ -7578,19 +7637,19 @@ function WAREHOUSE:_PrintQueue(queue, name) local assetdesc=qitem.assetdesc local assetdescval=qitem.assetdescval local nasset=tostring(qitem.nasset) - local ndelivered=tostring(qitem.ndelivered) + local ndelivered=tostring(qitem.ndelivered) local ncargogroupset="N/A" if qitem.cargogroupset then - ncargogroupset=tostring(qitem.cargogroupset:Count()) - end + ncargogroupset=tostring(qitem.cargogroupset:Count()) + end local transporttype="N/A" if qitem.transporttype then transporttype=qitem.transporttype - end + end local ntransport="N/A" if qitem.ntransport then - ntransport=tostring(qitem.ntransport) - end + ntransport=tostring(qitem.ntransport) + end local ntransportalive="N/A" if qitem.transportgroupset then ntransportalive=tostring(qitem.transportgroupset:Count()) @@ -7598,21 +7657,21 @@ function WAREHOUSE:_PrintQueue(queue, name) local ntransporthome="N/A" if qitem.ntransporthome then ntransporthome=tostring(qitem.ntransporthome) - end - - -- Output text: + end + + -- Output text: text=text..string.format( "\n%d) UID=%d, Prio=%d, Clock=%s, Assignment=%s | Requestor=%s [Airbase=%s, category=%d] | Assets(%s)=%s: #requested=%s / #alive=%s / #delivered=%s | Transport=%s: #requested=%s / #alive=%s / #home=%s", i, uid, prio, clock, assignment, requestor, airbasename, requestorAirbaseCat, assetdesc, assetdescval, nasset, ncargogroupset, ndelivered, transporttype, ntransport, ntransportalive, ntransporthome) - + end - + self:I(self.wid..text) end --- Display status of warehouse. -- @param #WAREHOUSE self -function WAREHOUSE:_DisplayStatus() +function WAREHOUSE:_DisplayStatus() local text=string.format("\n------------------------------------------------------\n") text=text..string.format("Warehouse %s status: %s\n", self.alias, self:GetState()) text=text..string.format("------------------------------------------------------\n") @@ -7634,8 +7693,8 @@ function WAREHOUSE:_GetStockAssetsText(messagetoall) -- Get assets in stock. local _data=self:GetStockInfo(self.stock) - - -- Text. + + -- Text. local text="Stock:\n" local total=0 for _attribute,_count in pairs(_data) do @@ -7648,10 +7707,10 @@ function WAREHOUSE:_GetStockAssetsText(messagetoall) text=text..string.format("===================\n") text=text..string.format("Total = %d\n", total) text=text..string.format("------------------------------------------------------\n") - + -- Send message? MESSAGE:New(text, 10):ToAllIf(messagetoall) - + return text end @@ -7665,11 +7724,11 @@ function WAREHOUSE:_UpdateWarehouseMarkText() if self.markerid~=nil then trigger.action.removeMark(self.markerid) end - + -- Get assets in stock. local _data=self:GetStockInfo(self.stock) - -- Text. + -- Text. local text=string.format("Warehouse state: %s\nTotal assets in stock %d:\n", self:GetState(), #self.stock) for _attribute,_count in pairs(_data) do @@ -7678,7 +7737,7 @@ function WAREHOUSE:_UpdateWarehouseMarkText() text=text..string.format("%s=%d, ", attribute,_count) end end - + -- Create/update marker at warehouse in F10 map. self.markerid=self:GetCoordinate():MarkToCoalition(text, self:GetCoalition(), true) end @@ -7701,7 +7760,7 @@ function WAREHOUSE:_DisplayStockItems(stock) local speed=mystock.speedmax local uid=mystock.uid local unittype=mystock.unittype - local weight=mystock.weight + local weight=mystock.weight local attribute=mystock.attribute text=text..string.format("\n%02d) uid=%d, name=%s, unittype=%s, category=%d, attribute=%s, nunits=%d, speed=%.1f km/h, range=%.1f km, size=%.1f m, weight=%.1f kg, cargobax max=%.1f kg tot=%.1f kg", _i, uid, name, unittype, category, attribute, nunits, speed, range/1000, size, weight, cargobaymax, cargobaytot) @@ -7777,23 +7836,23 @@ function WAREHOUSE:_GetMaxHeight(D, alphaC, alphaD, Hdep, Hdest, Deltahhold) local Hhold=Hdest+Deltahhold local hdest=Hdest-Hdep local hhold=hdest+Deltahhold - + local Dp=math.sqrt(D^2 + hhold^2) - + local alphaS=math.atan(hdest/D) -- slope angle local alphaH=math.atan(hhold/D) -- angle to holding point (could be necative!) - + local alphaCp=alphaC-alphaH -- climb angle with slope local alphaDp=alphaD+alphaH -- descent angle with slope - + -- ASA triangle. local gammap=math.pi-alphaCp-alphaDp local sCp=Dp*math.sin(alphaDp)/math.sin(gammap) local sDp=Dp*math.sin(alphaCp)/math.sin(gammap) - + -- Max height from departure. local hmax=sCp*math.sin(alphaC) - + -- Debug info. if self.Debug then env.info(string.format("Hdep = %.3f km", Hdep/1000)) @@ -7817,14 +7876,14 @@ function WAREHOUSE:_GetMaxHeight(D, alphaC, alphaD, Hdep, Hdest, Deltahhold) env.info() env.info(string.format("hmax = %.3f km", hmax/1000)) env.info() - + -- Descent height local hdescent=hmax-hhold - + local dClimb = hmax/math.tan(alphaC) local dDescent = (hmax-hhold)/math.tan(alphaD) local dCruise = D-dClimb-dDescent - + env.info(string.format("hmax = %.3f km", hmax/1000)) env.info(string.format("hdescent = %.3f km", hdescent/1000)) env.info(string.format("Dclimb = %.3f km", dClimb/1000)) @@ -7832,84 +7891,84 @@ function WAREHOUSE:_GetMaxHeight(D, alphaC, alphaD, Hdep, Hdest, Deltahhold) env.info(string.format("Ddescent = %.3f km", dDescent/1000)) env.info() end - + return hmax end ---- Make a flight plan from a departure to a destination airport. +--- Make a flight plan from a departure to a destination airport. -- @param #WAREHOUSE self --- @param #WAREHOUSE.Assetitem asset +-- @param #WAREHOUSE.Assetitem asset -- @param Wrapper.Airbase#AIRBASE departure Departure airbase. -- @param Wrapper.Airbase#AIRBASE destination Destination airbase. -- @return #table Table of flightplan waypoints. --- @return #table Table of flightplan coordinates. +-- @return #table Table of flightplan coordinates. function WAREHOUSE:_GetFlightplan(asset, departure, destination) - + -- Parameters in SI units (m/s, m). local Vmax=asset.speedmax/3.6 local Range=asset.range local category=asset.category local ceiling=asset.DCSdesc.Hmax local Vymax=asset.DCSdesc.VyMax - + -- Max cruise speed 90% of max speed. local VxCruiseMax=0.90*Vmax -- Min cruise speed 70% of max cruise or 600 km/h whichever is lower. local VxCruiseMin = math.min(VxCruiseMax*0.70, 166) - + -- Cruise speed (randomized). Expectation value at midpoint between min and max. local VxCruise = UTILS.RandomGaussian((VxCruiseMax-VxCruiseMin)/2+VxCruiseMin, (VxCruiseMax-VxCruiseMax)/4, VxCruiseMin, VxCruiseMax) - + -- Climb speed 90% ov Vmax but max 720 km/h. local VxClimb = math.min(Vmax*0.90, 200) - + -- Descent speed 60% of Vmax but max 500 km/h. local VxDescent = math.min(Vmax*0.60, 140) - + -- Holding speed is 90% of descent speed. local VxHolding = VxDescent*0.9 - + -- Final leg is 90% of holding speed. local VxFinal = VxHolding*0.9 - + -- Reasonably civil climb speed Vy=1500 ft/min = 7.6 m/s but max aircraft specific climb rate. local VyClimb=math.min(7.6, Vymax) - + -- Climb angle in rad. --local AlphaClimb=math.asin(VyClimb/VxClimb) local AlphaClimb=math.rad(4) - + -- Descent angle in rad. Moderate 4 degrees. local AlphaDescent=math.rad(4) - + -- Expected cruise level (peak of Gaussian distribution) local FLcruise_expect=150*RAT.unit.FL2m - if category==Group.Category.HELICOPTER then + if category==Group.Category.HELICOPTER then FLcruise_expect=1000 -- 1000 m ASL end - + ------------------------- --- DEPARTURE AIRPORT --- ------------------------- - + -- Coordinates of departure point. local Pdeparture=departure:GetCoordinate() - + -- Height ASL of departure point. local H_departure=Pdeparture.y - - --------------------------- + + --------------------------- --- DESTINATION AIRPORT --- --------------------------- - + -- Position of destination airport. local Pdestination=destination:GetCoordinate() - + -- Height ASL of destination airport/zone. local H_destination=Pdestination.y - + ----------------------------- --- DESCENT/HOLDING POINT --- ----------------------------- @@ -7917,26 +7976,26 @@ function WAREHOUSE:_GetFlightplan(asset, departure, destination) -- Get a random point between 5 and 10 km away from the destination. local Rhmin=5000 local Rhmax=10000 - + -- For helos we set a distance between 500 to 1000 m. - if category==Group.Category.HELICOPTER then + if category==Group.Category.HELICOPTER then Rhmin=500 Rhmax=1000 end - + -- Coordinates of the holding point. y is the land height at that point. local Pholding=Pdestination:GetRandomCoordinateInRadius(Rhmax, Rhmin) -- Distance from holding point to final destination (not used). local d_holding=Pholding:Get2DDistance(Pdestination) - + -- AGL height of holding point. local H_holding=Pholding.y - + --------------- --- GENERAL --- --------------- - + -- We go directly to the holding point not the destination airport. From there, planes are guided by DCS to final approach. local heading=Pdeparture:HeadingTo(Pholding) local d_total=Pdeparture:Get2DDistance(Pholding) @@ -7944,46 +8003,46 @@ function WAREHOUSE:_GetFlightplan(asset, departure, destination) ------------------------------ --- Holding Point Altitude --- ------------------------------ - + -- Holding point altitude. For planes between 1600 and 2400 m AGL. For helos 160 to 240 m AGL. local h_holding=1200 if category==Group.Category.HELICOPTER then h_holding=150 end h_holding=UTILS.Randomize(h_holding, 0.2) - + -- Max holding altitude. local DeltaholdingMax=self:_GetMaxHeight(d_total, AlphaClimb, AlphaDescent, H_departure, H_holding, 0) - + if h_holding>DeltaholdingMax then h_holding=math.abs(DeltaholdingMax) end - + -- This is the height ASL of the holding point we want to fly to. local Hh_holding=H_holding+h_holding - + --------------------------- --- Max Flight Altitude --- - --------------------------- - + --------------------------- + -- Get max flight altitude relative to H_departure. local h_max=self:_GetMaxHeight(d_total, AlphaClimb, AlphaDescent, H_departure, H_holding, h_holding) -- Max flight level ASL aircraft can reach for given angles and distance. local FLmax = h_max+H_departure - - --CRUISE + + --CRUISE -- Min cruise alt is just above holding point at destination or departure height, whatever is larger. local FLmin=math.max(H_departure, Hh_holding) - + -- Ensure that FLmax not above its service ceiling. FLmax=math.min(FLmax, ceiling) - + -- If the route is very short we set FLmin a bit lower than FLmax. if FLmin>FLmax then FLmin=FLmax end - + -- Expected cruise altitude - peak of gaussian distribution. if FLcruise_expectFLmax then FLcruise_expect=FLmax end - + -- Set cruise altitude. Selected from Gaussian distribution but limited to FLmin and FLmax. local FLcruise=UTILS.RandomGaussian(FLcruise_expect, math.abs(FLmax-FLmin)/4, FLmin, FLmax) -- Climb and descent heights. local h_climb = FLcruise - H_departure local h_descent = FLcruise - Hh_holding - + -- Get distances. local d_climb = h_climb/math.tan(AlphaClimb) local d_descent = h_descent/math.tan(AlphaDescent) local d_cruise = d_total-d_climb-d_descent - + -- Debug. local text=string.format("Flight plan:\n") text=text..string.format("Vx max = %.2f km/h\n", Vmax*3.6) @@ -8031,7 +8090,7 @@ function WAREHOUSE:_GetFlightplan(asset, departure, destination) text=text..string.format("Ceiling = %.3f km\n", ceiling/1000) text=text..string.format("Max range = %.3f km\n", Range/1000) self:T(self.wid..text) - + -- Ensure that cruise distance is positve. Can be slightly negative in special cases. And we don't want to turn back. if d_cruise<0 then d_cruise=100 @@ -8044,32 +8103,32 @@ function WAREHOUSE:_GetFlightplan(asset, departure, destination) -- Waypoints and coordinates local wp={} local c={} - + --- Departure/Take-off c[#c+1]=Pdeparture wp[#wp+1]=Pdeparture:WaypointAir("RADIO", COORDINATE.WaypointType.TakeOffParking, COORDINATE.WaypointAction.FromParkingArea, VxClimb, true, departure, nil, "Departure") - + --- Begin of Cruise local Pcruise=Pdeparture:Translate(d_climb, heading) Pcruise.y=FLcruise c[#c+1]=Pcruise wp[#wp+1]=Pcruise:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxCruise, true, nil, nil, "Cruise") - --- Descent + --- Descent local Pdescent=Pcruise:Translate(d_cruise, heading) Pdescent.y=FLcruise c[#c+1]=Pdescent wp[#wp+1]=Pdescent:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxDescent, true, nil, nil, "Descent") - + --- Holding point Pholding.y=H_holding+h_holding c[#c+1]=Pholding - wp[#wp+1]=Pholding:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxHolding, true, nil, nil, "Holding") + wp[#wp+1]=Pholding:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxHolding, true, nil, nil, "Holding") - --- Final destination. + --- Final destination. c[#c+1]=Pdestination wp[#wp+1]=Pdestination:WaypointAir("RADIO", COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, VxFinal, true, destination, nil, "Final Destination") - + -- Mark points at waypoints for debugging. if self.Debug then @@ -8080,9 +8139,9 @@ function WAREHOUSE:_GetFlightplan(asset, departure, destination) dist=coord:Get2DDistance(c[i-1]) end coord:MarkToAll(string.format("Waypoint %i, distance = %.2f km",i, dist/1000)) - end + end end - + return wp,c end @@ -8095,37 +8154,37 @@ end --- Departure/Take-off c[#c+1]=Pdeparture wp[#wp+1]=Pdeparture:WaypointAir("RADIO", COORDINATE.WaypointType.TakeOffParking, COORDINATE.WaypointAction.FromParkingArea, VxClimb, true, departure, nil, "Departure") - - --- Climb + + --- Climb local Pclimb=Pdeparture:Translate(d_climb/2, heading) Pclimb.y=H_departure+(FLcruise-H_departure)/2 c[#c+1]=Pclimb wp[#wp+1]=Pclimb:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxClimb, true, nil, nil, "Climb") - + --- Begin of Cruise local Pcruise1=Pclimb:Translate(d_climb/2, heading) Pcruise1.y=FLcruise c[#c+1]=Pcruise1 wp[#wp+1]=Pcruise1:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxCruise, true, nil, nil, "Begin of Cruise") - --- End of Cruise + --- End of Cruise local Pcruise2=Pcruise1:Translate(d_cruise, heading) Pcruise2.y=FLcruise c[#c+1]=Pcruise2 wp[#wp+1]=Pcruise2:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxCruise, true, nil, nil, "End of Cruise") - --- Descent + --- Descent local Pdescent=Pcruise2:Translate(d_descent/2, heading) Pdescent.y=FLcruise-(FLcruise-(h_holding+H_holding))/2 c[#c+1]=Pdescent wp[#wp+1]=Pcruise2:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxDescent, true, nil, nil, "Descent") - - --- Holding point - Pholding.y=H_holding+h_holding - c[#c+1]=Pholding - wp[#wp+1]=Pholding:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxHolding, true, nil, nil, "Holding") - --- Final destination. + --- Holding point + Pholding.y=H_holding+h_holding + c[#c+1]=Pholding + wp[#wp+1]=Pholding:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxHolding, true, nil, nil, "Holding") + + --- Final destination. c[#c+1]=Pdestination wp[#wp+1]=Pdestination:WaypointAir("RADIO", COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, VxFinal, true, destination, nil, "Final Destination") ]] diff --git a/Moose Development/Moose/Moose.lua b/Moose Development/Moose/Globals.lua similarity index 100% rename from Moose Development/Moose/Moose.lua rename to Moose Development/Moose/Globals.lua diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua new file mode 100644 index 000000000..d0614bbfe --- /dev/null +++ b/Moose Development/Moose/Modules.lua @@ -0,0 +1,115 @@ +__Moose.Include( 'Scripts/Moose/Utilities/Routines.lua' ) +__Moose.Include( 'Scripts/Moose/Utilities/Utils.lua' ) + +__Moose.Include( 'Scripts/Moose/Core/Base.lua' ) +__Moose.Include( 'Scripts/Moose/Core/UserFlag.lua' ) +__Moose.Include( 'Scripts/Moose/Core/UserSound.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Report.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Scheduler.lua' ) +__Moose.Include( 'Scripts/Moose/Core/ScheduleDispatcher.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Event.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Settings.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Menu.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Zone.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Database.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Set.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Point.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Velocity.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Message.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Fsm.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Radio.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Spawn.lua' ) +__Moose.Include( 'Scripts/Moose/Core/SpawnStatic.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Goal.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Spot.lua' ) + +__Moose.Include( 'Scripts/Moose/Wrapper/Object.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/Identifiable.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/Positionable.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/Controllable.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/Group.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/Unit.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/Client.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/Static.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/Airbase.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/Scenery.lua' ) + +__Moose.Include( 'Scripts/Moose/Cargo/Cargo.lua' ) +__Moose.Include( 'Scripts/Moose/Cargo/CargoUnit.lua' ) +__Moose.Include( 'Scripts/Moose/Cargo/CargoSlingload.lua' ) +__Moose.Include( 'Scripts/Moose/Cargo/CargoCrate.lua' ) +__Moose.Include( 'Scripts/Moose/Cargo/CargoGroup.lua' ) + +__Moose.Include( 'Scripts/Moose/Functional/Scoring.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/CleanUp.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Movement.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Sead.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Escort.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/MissileTrainer.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/ATC_Ground.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Detection.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Designate.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/RAT.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Range.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/ZoneGoal.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/ZoneGoalCoalition.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/ZoneCaptureCoalition.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Artillery.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Suppression.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/PseudoATC.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Warehouse.lua' ) + +__Moose.Include( 'Scripts/Moose/Ops/Airboss.lua' ) +__Moose.Include( 'Scripts/Moose/Ops/RecoveryTanker.lua' ) +__Moose.Include( 'Scripts/Moose/Ops/RescueHelo.lua' ) + +__Moose.Include( 'Scripts/Moose/AI/AI_Balancer.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Air.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2A.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2A_Patrol.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2A_Cap.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2A_Gci.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2A_Dispatcher.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2G.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2G_Engage.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2G_BAI.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2G_CAS.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2G_SEAD.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2G_Patrol.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2G_Dispatcher.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Patrol.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Cap.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Cas.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Bai.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Formation.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Cargo.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Cargo_APC.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Cargo_Helicopter.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Cargo_Airplane.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Cargo_Dispatcher.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Cargo_Dispatcher_APC.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Cargo_Dispatcher_Helicopter.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Cargo_Dispatcher_Airplane.lua' ) + +__Moose.Include( 'Scripts/Moose/Actions/Act_Assign.lua' ) +__Moose.Include( 'Scripts/Moose/Actions/Act_Route.lua' ) +__Moose.Include( 'Scripts/Moose/Actions/Act_Account.lua' ) +__Moose.Include( 'Scripts/Moose/Actions/Act_Assist.lua' ) + +__Moose.Include( 'Scripts/Moose/Tasking/CommandCenter.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Mission.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/TaskInfo.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task_Manager.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/DetectionManager.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task_A2G_Dispatcher.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task_A2G.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task_A2A_Dispatcher.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task_A2A.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task_Cargo.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task_Cargo_Transport.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task_Cargo_CSAR.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task_Cargo_Dispatcher.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/TaskZoneCapture.lua' ) + +__Moose.Include( 'Scripts/Moose/Globals.lua' ) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua new file mode 100644 index 000000000..79b5a9970 --- /dev/null +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -0,0 +1,13109 @@ +--- **Ops** - (R2.5) - Manages aircraft recoveries for carrier operations. +-- +-- The AIRBOSS class manages recoveries of human pilots and AI aircraft on aircraft carriers. +-- +-- **Main Features:** +-- +-- * CASE I, II and III recoveries. +-- * Supports human pilots as well as AI flight groups. +-- * Automatic LSO grading including (optional) live grading while in the groove. +-- * Different skill levels from on-the-fly tips for flight students to *ziplip* for pros. Can be set for each player individually. +-- * Define recovery time windows with individual recovery cases in the same mission. +-- * Option to let the carrier steam into the wind automatically. +-- * Automatic TACAN and ICLS channel setting of carrier. +-- * Separate radio channels for LSO and Marshal transmissions. +-- * Voice over support for LSO and Marshal radio transmissions. +-- * F10 radio menu including carrier info (weather, radio frequencies, TACAN/ICLS channels), player LSO grades, help function (aircraft attitude, marking of zones etc). +-- * Recovery tanker and refueling option via integration of @{Ops.RecoveryTanker} class. +-- * Rescue helicopter option via @{Ops.RescueHelo} class. +-- * Combine multiple human player to sections. +-- * Many parameters customizable by convenient user API functions. +-- * Multiple carrier support due to object oriented approach. +-- * Unlimited number of players. +-- * Persistence of player results (optional). LSO grading data is saved to csv file. +-- * Finite State Machine (FSM) implementation. +-- +-- **Supported Carriers:** +-- +-- * [USS John C. Stennis](https://en.wikipedia.org/wiki/USS_John_C._Stennis) (CVN-74) +-- +-- **Supported Aircraft:** +-- +-- * [F/A-18C Hornet Lot 20](https://forums.eagle.ru/forumdisplay.php?f=557) (Player & AI) +-- * [A-4E Skyhawk Community Mod](https://forums.eagle.ru/showthread.php?t=224989) (Player & AI) +-- * F/A-18C Hornet (AI) +-- * F-14A Tomcat (AI) +-- * E-2D Hawkeye (AI) +-- * S-3B Viking & tanker version (AI) +-- +-- At the moment, optimized parameters are available for the F/A-18C Hornet (Lot 20) as aircraft and the USS John C. Stennis as carrier. +-- The A-4E community mod is also supported in principle but may need further tweaking of parameters. Also the A-4E-C mod needs *easy comms* activated to interact with the F10 radio menu. +-- +-- The implementation is kept general. So other aircraft and carriers possible in future. [*Winter is coming!*](https://forums.eagle.ru/forumdisplay.php?f=395) +-- But each aircraft or carrier needs a different set of optimized individual parameters. +-- +-- **PLEASE NOTE** that his class is work in progress and in an early **alpha** stage. Many/most things work already very nicely but there a lot of cases I did not run into yet. +-- Therefore, your *constructive* feedback is both necessary and appreciated! +-- +-- ## Discussion +-- +-- If you have questions or suggestions, visit the MOOSE Discord [#ops-airboss](https://discordapp.com/channels/378590350614462464/527363141185830915) channel. +-- There you also find an example mission and the necessary voice over sound files. Check the **pinned messages**. +-- +-- ## IMPORTANT +-- +-- Due to technical restrictions of DCS make sure you have: +-- +-- * Each player slot in a separate group. DCS does only allow to send messages to groups and not to individual units. +-- * Players are identified by their player name. Ensure that no two player have the same name, e.g. "New Callsign", as this will lead to unexpected results. +-- +-- ## Youtube Videos +-- +-- * [[MOOSE] Airboss - Groove Testing (WIP)](https://www.youtube.com/watch?v=94KHQxxX3UI) +-- * [[MOOSE] Airboss - Groove Test A-4E Community Mod](https://www.youtube.com/watch?v=ZbjD7FHiaHo) +-- +-- +-- ### Open Questions? +-- +-- * Currently the script does not support spin patterns. Marshal releases flights only when there is a free slot in the landing pattern. How is this handled in real life? +-- * What is the next step after a pattern wave off during Case II or III recovery? +-- * What are the conditions for waving off flights when they get too close to a flight ahead in the pattern? At which pattern steps are flights waved off because of this? +-- * Some more LSO gradings could be added. What is missing and what are the conditions? +-- * For the A-4E-C, what are the AoA thresholds for being on speed, (a little) slow, (a little) fast in **degrees**? In know the numbers in units of the indexer but need a proper conversion to degrees. +-- +-- If you know the answer to any of this, please get in touch with me! The necessary infrastructure to implement it is most likely already there. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- ### Special Thanks To **Bankler** +-- For his great [Recovery Trainer](https://forums.eagle.ru/showthread.php?t=221412) mission and script! +-- His work was the initial inspiration for this class. Also note that this implementation uses some routines for determining the player position in Case I recoveries he developed. +-- Bankler was kind enough to allow me to add this to the class - thanks again! +-- +-- @module Ops.Airboss +-- @image Ops_Airboss.png + +--- AIRBOSS class. +-- @type AIRBOSS +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. Messages to all about status. +-- @field #string lid Class id string for output to DCS log file. +-- @field Wrapper.Unit#UNIT carrier Aircraft carrier unit on which we want to practice. +-- @field #string carriertype Type name of aircraft carrier. +-- @field #AIRBOSS.CarrierParameters carrierparam Carrier specific parameters. +-- @field #string alias Alias of the carrier. +-- @field Wrapper.Airbase#AIRBASE airbase Carrier airbase object. +-- @field #table waypoints Waypoint coordinates of carrier. +-- @field #number currentwp Current waypoint, i.e. the one that has been passed last. +-- @field Core.Radio#BEACON beacon Carrier beacon for TACAN and ICLS. +-- @field #boolean TACANon Automatic TACAN is activated. +-- @field #number TACANchannel TACAN channel. +-- @field #string TACANmode TACAN mode, i.e. "X" or "Y". +-- @field #string TACANmorse TACAN morse code, e.g. "STN". +-- @field #boolean ICLSon Automatic ICLS is activated. +-- @field #number ICLSchannel ICLS channel. +-- @field #string ICLSmorse ICLS morse code, e.g. "STN". +-- @field #AIRBOSS.Radio LSORadio Radio for LSO calls. +-- @field #number LSOFreq LSO radio frequency in MHz. +-- @field #string LSOModu LSO radio modulation "AM" or "FM". +-- @field #AIRBOSS.Radio MarshalRadio Radio for carrier calls. +-- @field #number MarshalFreq Marshal radio frequency in MHz. +-- @field #string MarshalModu Marshal radio modulation "AM" or "FM". +-- @field Core.Scheduler#SCHEDULER radiotimer Radio queue scheduler. +-- @field Core.Zone#ZONE_UNIT zoneCCA Carrier controlled area (CCA), i.e. a zone of 50 NM radius around the carrier. +-- @field Core.Zone#ZONE_UNIT zoneCCZ Carrier controlled zone (CCZ), i.e. a zone of 5 NM radius around the carrier. +-- @field #table players Table of players. +-- @field #table menuadded Table of units where the F10 radio menu was added. +-- @field #AIRBOSS.Checkpoint BreakEntry Break entry checkpoint. +-- @field #AIRBOSS.Checkpoint BreakEarly Early break checkpoint. +-- @field #AIRBOSS.Checkpoint BreakLate Late break checkpoint. +-- @field #AIRBOSS.Checkpoint Abeam Abeam checkpoint. +-- @field #AIRBOSS.Checkpoint Ninety At the ninety checkpoint. +-- @field #AIRBOSS.Checkpoint Wake Checkpoint right behind the carrier. +-- @field #AIRBOSS.Checkpoint Final Checkpoint when turning to final. +-- @field #AIRBOSS.Checkpoint Groove In the groove checkpoint. +-- @field #AIRBOSS.Checkpoint Platform Case II/III descent at 2000 ft/min at 5000 ft platform. +-- @field #AIRBOSS.Checkpoint DirtyUp Case II/III dirty up and on speed position at 1200 ft and 10-12 NM from the carrier. +-- @field #AIRBOSS.Checkpoint Bullseye Case III intercept glideslope and follow ICLS aka "bullseye". +-- @field #number defaultcase Default recovery case. This is the case used if not specified otherwise. +-- @field #number case Recovery case I, II or III currently in progress. +-- @field #table recoverytimes List of time windows when aircraft are recovered including the recovery case and holding offset. +-- @field #number defaultoffset Default holding pattern update if not specified otherwise. +-- @field #number holdingoffset Offset [degrees] of Case II/III holding pattern. +-- @field #table flights List of all flights in the CCA. +-- @field #table Qmarshal Queue of marshalling aircraft groups. +-- @field #table Qpattern Queue of aircraft groups in the landing pattern. +-- @field #table Qwaiting Queue of aircraft groups waiting outside 10 NM zone for the next free Marshal stack. +-- @field #table RQMarshal Radio queue of marshal. +-- @field #table RQLSO Radio queue of LSO. +-- @field #number Nmaxpattern Max number of aircraft in landing pattern. +-- @field #number Nmaxmarshal Number of max Case I Marshal stacks available. Default 3, i.e. angels 2, 3 and 4. +-- @field #number NmaxSection Number of max section members (excluding the lead itself), i.e. NmaxSection=1 is a section of two. +-- @field #number NmaxStack Number of max flights per stack. Default 2. +-- @field #boolean handleai If true (default), handle AI aircraft. +-- @field Ops.RecoveryTanker#RECOVERYTANKER tanker Recovery tanker flying overhead of carrier. +-- @field Functional.Warehouse#WAREHOUSE warehouse Warehouse object of the carrier. +-- @field DCS#Vec3 Corientation Carrier orientation in space. +-- @field DCS#Vec3 Corientlast Last known carrier orientation. +-- @field Core.Point#COORDINATE Cposition Carrier position. +-- @field #string defaultskill Default player skill @{#AIRBOSS.Difficulty}. +-- @field #boolean adinfinitum If true, carrier patrols ad infinitum, i.e. when reaching its last waypoint it starts at waypoint one again. +-- @field #number magvar Magnetic declination in degrees. +-- @field #number Tcollapse Last time timer.gettime() the stack collapsed. +-- @field #AIRBOSS.Recovery recoverywindow Current or next recovery window opened. +-- @field #boolean usersoundradio Use user sound output instead of radio transmissions. +-- @field #number Tqueue Last time in seconds of timer.getTime() the queue was updated. +-- @field #number dTqueue Time interval in seconds for updating the queues etc. +-- @field #number dTstatus Time interval for call FSM status updates. +-- @field #boolean menumarkzones If false, disables the option to mark zones via smoke or flares. +-- @field #boolean menusmokezones If false, disables the option to mark zones via smoke. +-- @field #table playerscores Table holding all player scores and grades. +-- @field #boolean autosave If true, all player grades are automatically saved to a file on disk. +-- @field #string autosavepath Path where the player grades file is saved on auto save. +-- @field #string autosavefilename File name of the auto player grades save file. Default is auto generated from carrier name/alias. +-- @field #number marshalradius Radius of the Marshal stack zone. +-- @field #boolean airbossnice Airboss is a nice guy. +-- @field #boolean staticweather Mission uses static rather than dynamic weather. +-- @field #number windowcount Running number counting the recovery windows. +-- @field #number LSOdT Time interval in seconds before the LSO will make its next call. +-- @field #string senderac Name of the aircraft acting as sender for broadcasting radio messages from the carrier. DCS shortcoming workaround. +-- @field #boolean turnintowind If true, carrier is currently turning into the wind. +-- @field #boolean detour If true, carrier is currently making a detour from its path along the ME waypoints. +-- @field Core.Point#COORDINATE Creturnto Position to return to after turn into the wind leg is over. +-- @field Core.Set#SET_GROUP squadsetAI AI groups in this set will be handled by the airboss. +-- @field #boolean menusingle If true, menu is optimized for a single carrier. +-- @field #number collisiondist Distance up to which collision checks are done. +-- @field #number Tmessage Default duration in seconds messages are displayed to players. +-- @field #string soundfolder Folder within the mission (miz) file where airboss sound files are located. +-- @extends Core.Fsm#FSM + +--- Be the boss! +-- +-- === +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Main.png) +-- +-- # The AIRBOSS Concept +-- +-- On a carrier, the AIRBOSS is guy who is really in charge - don't mess with him! +-- +-- # Recovery Cases +-- +-- The AIRBOSS class supports all three commonly used recovery cases, i.e. +-- +-- * **CASE I** during daytime and good weather, +-- * **CASE II** during daytime but poor visibility conditions, +-- * **CASE III** during nighttime recoveries. +-- +-- That being said, this script allows you to use any of the three cases to be used at any time. Or, in other words, *you* need to specify when which case is safe and appropriate. +-- +-- This is a lot of responsability. *You* are the boss, but *you* need to make the right decisions or things will go terribly wrong! +-- +-- Recovery windows can be set up via the @{#AIRBOSS.AddRecoveryWindow} function as explained below. With this it is possible to seamlessly (within reason!) switch recovery cases in the same mission. +-- +-- ## CASE I +-- +-- As mentioned before, Case I recovery is the standard procedure during daytime and good visibility conditions. +-- +-- ### Holding Pattern +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case1_Holding.png) +-- +-- The graphic depicts a the standard holding pattern during a Case I recovery. Incoming aircraft enter the holding pattern, which is a counter clockwise turn with a +-- diameter of 5 NM, at their assigned altitude. The holding altitude of the first stack is 2000 ft. The interval between stacks is 1000 ft. +-- +-- Once a recovery window opens, the aircraft of the lowest stack commence their landing approach and the rest of the Marshal stack collapses, i.e. aircraft switch from +-- their current stack to the next lower stack. +-- +-- The flight that transitions form the holding pattern to the landing approach, it should leave the Marshal stack at the 3 position and make a left hand turn to the *Initial* +-- position, which is 3 NM astern of the boat. +-- +-- ### Landing Pattern +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case1_Landing.png) +-- +-- Once the aircraft reaches the Inital, the landing pattern begins. The important steps of the pattern are shown in the image above. +-- +-- +-- ## CASE III +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case3.png) +-- +-- A Case III recovery is conducted during nighttime. The holding position and the landing pattern are rather different from a Case I recovery as can be seen in the image above. +-- +-- The first holding zone starts 21 NM astern the carrier at angels 6. The separation between the stacks is 1000 ft just like in Case I. However, the distance to the boat +-- increases by 1 NM with each stack. The general form can be written as D=15+6+(N-1), where D is the distance to the boat in NM and N the number of the stack starting at N=1. +-- +-- Once the aircraft of the lowest stack is allowed to commence to the landing pattern, it starts a descent at 4000 ft/min until it reaches the "*Platform*" at 5000 ft and +-- ~19 NM DME. From there a shallower descent at 2000 ft/min should be performed. At an altitude of 1200 ft the aircraft should level out and "*Dirty Up*" (gear, flaps & hook down). +-- +-- At 3 NM distance to the carrier, the aircraft should intercept the 3.5 degrees glideslope at the "*Bullseye*". From there the pilot should "follow the needles" of the ICLS. +-- +-- ## CASE II +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case2.png) +-- +-- Case II is the common recovery procedure at daytime if visibility conditions are poor. It can be viewed as hybrid between Case I and III. +-- The holding pattern is very similar to that of the Case III recovery with the difference the the radial is the inverse of the BRC instead of the FB. +-- From the holding zone aircraft are follow the Case III path until they reach the Initial position 3 NM astern the boat. From there a standard Case I recovery procedure is +-- in place. +-- +-- Note that the image depicts the case, where the holding zone has an angle offset of 30 degrees with respect to the BRC. This is optional. Commonly used offset angels +-- are 0 (no offset), +-15 or +-30 degrees. The AIRBOSS class supports all these scenarios which are used during Case II and III recoveries. +-- +-- === +-- +-- # The F10 Radio Menu +-- +-- The F10 radio menu can be used to post requests to Marshal but also provides information about the player and carrier status. Additionally, helper functions +-- can be called. +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuMain.png) +-- +-- By default, the script creates a submenu "Airboss" in the "F10 Other ..." menu and each @{#AIRBOSS} carrier gets its own submenu. +-- If you intend to have only one carrier, you can simplify the menu structure using the @{#AIRBOSS.SetMenuSingleCarrier} function, which will create all carrier specific menu entries directly +-- in the "Airboss" submenu. (Needless to say, that if you enable this and define multiple carriers, the menu structure will get completely screwed up.) +-- +-- ## Root Menu +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuRoot.png) +-- +-- The general structure +-- +-- * **F1 Help...**: Help submenu, see below. +-- * **F2 Kneeboard...**: Kneeboard submenu, see below. Carrier information, weather report, player status. +-- * **F3 Request Marshal** +-- * **F4 Request Commence** +-- * **F5 Request Refueling** +-- * **F6 [Reset My Status]** +-- +-- ### Request Marshal +-- +-- This radio command can be used to request a stack in the holding pattern from Marshal. Necessary conditions are that the flight is inside the Carrier Controlled Area (CCA) +-- (see @{#AIRBOSS.SetCarrierControlledArea}). +-- +-- Marshal will assign an individual stack for each player group depending on the current or next open recovery case window. +-- If multiple players have registered as a section, the section lead will be assigned a stack and is responsible to guide his section to the assigned holding position. +-- +-- ### Request Commence +-- +-- This command can be used to request commencing from the marshal stack to the landing pattern. Necessary condition is that the player is in the lowest marshal stack +-- and that the number of aircraft in the landing pattern is smaller than four (or the number set by the mission designer). +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case1Pattern.png) +-- +-- The image displays the standard Case I Marshal pattern recovery. Pilots are supposed to fly a clockwise circle and descent between the **3** and **1** positions. +-- +-- Commence should be performed at around the **3** position. If the pilot is in the lowest Marshal stack, and flies through this area, he is automatically cleared for the +-- landing pattern. In other words, there is no need for the "Request Commence" radio command. The zone can be marked via smoke or flared using the player's F10 radio menu. +-- +-- A player can also request commencing if he is not registered in a marshal stack yet. If the pattern is free, Marshal will allow him to directly enter the landing pattern. +-- However, this is only possible when the Airboss has a nice day - see @{#AIRBOSS.SetAirbossNiceGuy}. +-- +-- ### Request Refueling +-- +-- If a recovery taker has been set up via the @{#AIRBOSS.SetRecoveryTanker}, the player can request refueling at any time. If currently in the marshal stack, the stack above will collapse. +-- The player will be informed if the tanker is currently busy or going RTB to refuel itself at its home base. Once the re-fueling is complete, the player has to re-register to the marshal stack. +-- +-- ### [Reset My Status] +-- +-- This will reset the current player status. If player is currently in a marshal stack, he will be removed from the marshal queue and the stack above will collapse. +-- The player needs to re-register later if desired. If player is currently in the landing pattern, he will be removed from the pattern queue. +-- +-- ## Help Menu +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuHelp.png) +-- +-- This menu provides commands to help the player. +-- +-- ### Mark Zones Submenu +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuMarkZones.png) +-- +-- These commands can be used to mark marshal or landing pattern zones. +-- +-- * **Smoke Pattern Zones** Smoke is used to mark the landing pattern zone of the player depending on his recovery case. +-- For Case I this is the initial zone. For Case II/III and three these are the Platform, Arc turn, Dirty Up, Bullseye/Initial zones as well as the approach corridor. +-- * **Flare Pattern Zones** Similar to smoke but uses flares to mark the pattern zones. +-- * **Smoke Marshal Zone** This smokes the surrounding area of the currently assigned Marshal zone of the player. Player has to be registered in Marshal queue. +-- * **Flare Marshal Zone** Similar to smoke but uses flares to mark the Marshal zone. +-- +-- Note that the smoke lasts ~5 minutes but the zones are moving along with the carrier. So after some time, the smoke gives shows you a picture of the past. +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case3_FlarePattern.png) +-- +-- ### Skill Level Submenu +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuSkill.png) +-- +-- The player can choose between three skill or difficulty levels. +-- +-- * **Flight Student**: The player receives tips at certain stages of the pattern, e.g. if he is at the right altitude, speed, etc. +-- * **Naval Aviator**: Less tips are show. Player should be familiar with the procedures and its aircraft parameters. +-- * **TOPGUN Graduate**: Only very few information is provided to the player. This is for the pros. +-- +-- ### My Status +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuMyStatus.png) +-- +-- This command provides information about the current player status. For example, his current step in the pattern. +-- +-- ### Attitude Monitor +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuAttitudeMonitor.png) +-- +-- This command displays the current aircraft attitude of the player aircraft in short intervals as message on the screen. +-- It provides information about current pitch, roll, yaw, orientation of the plane with respect to the carrier's orientation (*Gamma*) etc. +-- +-- If you are in the groove, current lineup and glideslope errors are displayed and you get an on-the-fly LSO grade. +-- +-- ### LSO Radio Check +-- +-- LSO will transmit a short message on his radio frequency. See @{#AIRBOSS.SetLSORadio}. +-- +-- ### Marshal Radio Check +-- +-- Marshal will transmit a short message on his radio frequency. See @{#AIRBOSS.SetMarshalRadio}. +-- +-- ### Subtitles On/Off +-- +-- This command toggles the display of radio message subtitles. By default subtitles are on. +-- Note that subtitles for radio messages which do not have a complete voice over are always displayed. +-- +-- ## Kneeboard Menu +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuKneeboard.png) +-- +-- The Kneeboard menu provides information about the carrier, weather and player results. +-- +-- ### Results Submenu +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuResults.png) +-- +-- Here you find your LSO grading results as well as scores of other players. +-- +-- * **Greenie Board** lists average scores of all players obtained during landing approaches. +-- * **My LSO Grades** lists all grades the player has received for his approaches in this mission. +-- * **Last Debrief** shows the detailed debriefing of the player's last approach. +-- +-- ### Carrier Info +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuCarrierInfo.png) +-- +-- Information about the current carrier status is displayed. This includes current BRC, FB, LSO and Marshal frequencies, list of next recovery windows. +-- +-- ### Weather Report +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuWeatherReport.png) +-- +-- Displays information about the current weather at the carrier such as QFE, wind and temperature. +-- +-- For missions using static weather, more information such as cloud base, thickness, precipitation, visibility distance, fog and dust are displayed. +-- If you mission uses dynamic weather, you can disable this output via the @{#AIRBOSS.SetStaticWeather}(**false**) function. +-- +-- ### Set Section +-- +-- With this command, you can define a section of human flights. The player how issues the command becomes the section lead and all other human players +-- within a radius of 100 meters become members of the section. +-- +-- The responsibilities of the section leader are: +-- +-- * To request Marshal. The section members are not allowed to do this and have to follow the lead to his assigned stack. +-- * To lead the right way to the pattern if the flight is allowed to commence. +-- * The lead is also the only one who can request commence if the flight wants to bypass the Marshal stack. +-- +-- Each time the command is issued by the lead, the complete section is set up from scratch. Members which are not inside the 100 m radius any more are +-- removed and/or new members which are now in range are added. +-- +-- If a section member issues this command, it is removed from the section of his lead. All flights which are not yet in another section will become members. +-- +-- The default maximum size of a section is two human players. This can be adjusted by the @{#AIRBOSS.SetMaxSectionSize}(*size*) function. The maximum allowed size +-- is four. +-- +-- ### Marshal Queue +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuMarshalQueue.png) +-- +-- Lists all flights currently in the Marshal queue including their assigned stack, recovery case and Charie time estimate. +-- By default, the number of available Case I stacks is three, i.e. at angels 2, 3 and 4. Usually, the recovery thanker orbits at angels 6. +-- The number of available stacks can be set by the @{#AIRBOSS.SetMaxMarshalStack} function. +-- +-- The default number of human players per stack is two. This can be set via the @{#AIRBOSS.SetMaxFlightsPerStack} function but has to be between one and four. +-- +-- Due to technical reasons, each AI group always gets its own stack. DCS does not allow to control the AI in a manner that more than one group per stack would make sense unfortunately. +-- +-- ### Pattern Queue +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuPatternQueue.png) +-- +-- Lists all flights currently in the landing pattern queue showing the time since they entered the pattern. +-- By default, a maximum of four flights is allowed to enter the pattern. This can be set via the @{#AIRBOSS.SetMaxLandingPattern} function. +-- +-- ### Waiting Queue +-- +-- Lists all flights currently waiting for a free Case I Marshal stack. Note, stacks are limited only for Case I recovery ops but not for Case II or III. +-- If the carrier is switches recovery ops form Case I to Case II or III, all waiting flights will be assigned a stack. +-- +-- # Landing Signal Officer (LSO) +-- +-- The LSO will first contact you on his radio channel when you are at the the abeam position (Case I) with the phrase "Paddles, contact.". +-- Once you are in the groove the LSO will ask you to "Call the ball." and then acknowledge your ball call by "Roger Ball." +-- +-- During the groove the LSO will give you advice if you deviate from the correct landing path. These advices will be given when you are +-- +-- * too low or too high with respect to the glideslope, +-- * too fast or too slow with respect to the optimal AoA, +-- * too far left or too far right with respect to the lineup of the (angled) runway. +-- +-- ## LSO Grading +-- +-- LSO grading starts when the player enters the groove. The flight path and aircraft attitude is evaluated at certain steps (distances measured from rundown): +-- +-- * **X** At the Start (0.75 NM = 1390 m). +-- * **IM** In the Middle (0.5 NM = 926 m), middle one third of the glideslope. +-- * **IC** In Close (0.25 NM = 463 m), last one third of the glideslope. +-- * **AR** At the Ramp (0.027 NM = 50 m). +-- * **IW** In the Wires (at the landing position). +-- +-- Grading at each step includes the above calls, i.e. +-- +-- * **L**ined **U**p **L**eft or **R**ight: LUL, LUR +-- * Too **H**igh or too **LO**w: H, LO +-- * Too **F**ast or too **SLO**w: F, SLO +-- * **Fly through** glideslope **down** or **up**: \\ , / +-- +-- Each grading, x, is subdivided by +-- +-- * (x): parenthesis, indicating "a little" for a minor deviation and +-- * \_x\_: underline, indicating "a lot" for major deviations. +-- +-- The position at the landing event is analyzed and the corresponding trapped wire calculated. If no wire was caught, the LSO will give the bolter call. +-- +-- If a player is significantly off from the ideal parameters from IC to AR, the LSO will wave the player off. Thresholds for wave off are +-- +-- * Line up error > 3.0 degrees left or right and/or +-- * Glideslope error < -1.2 degrees or > 1.8 degrees and/or +-- * AOA depending on aircraft type and only applied if skill level is "TOPGUN graduate". +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_LSOPlatcam.png) +-- +-- Line up and glideslope error thresholds were tested extensively using [VFA-113 Stingers LSO Mod](https://forums.eagle.ru/showthread.php?t=211557), +-- if the aircraft is outside the red box. In the picture above, **blue** numbers denote the line up thresholds while the **blacks** refer to the glideslope. +-- +-- A wave off is called, when the aircraft is outside the red rectangle. The measurement stops already ~50 m before the rundown, since the error in the calculation +-- increases the closer the aircraft gets to the origin/reference point. +-- +-- The optimal glideslope is assumed to be 3.5 degrees leading to a touch down point between the second and third wire. +-- The height of the carrier deck and the exact wire locations are taken into account in the calculations. +-- +-- ## Pattern Waveoff +-- +-- The player's aircraft position is evaluated at certain critical locations in the landing pattern. If the player is far off from the ideal approach, the LSO will +-- issue a pattern wave off. Currently, this is only implemented for Case I recoveries and the Case I part in the Case II recovery, i.e. +-- +-- * Break Entry +-- * Early Break +-- * Late Break +-- * Abeam +-- * Ninety +-- * Wake +-- * Groove +-- +-- At these points it is also checked if a player comes too close to another aircraft ahead of him in the pattern. +-- +-- ## Grading Points +-- +-- Currently grades are given by as follows +-- +-- * 5.0 Points **\_OK\_**: "Okay underline", given only for a perfect pass, i.e. when no deviations at all were observed by the LSO. The unicorn! +-- * 4.0 Points **OK**: "Okay pass" when only minor () deviations happened. +-- * 3.0 Points **(OK)**: "Fair pass", when only "normal" deviations were detected. +-- * 2.0 Points **--**: "No grade", for larger deviations. +-- +-- Furthermore, we have the cases: +-- +-- * 2.5 Points **B**: "Bolder", when the player landed but did not catch a wire. +-- * 1.0 Points **WO**: "Wave-Off": Player got waved off in the final parts of the groove. +-- * 1.0 Points **PWO**: "Pattern Wave-Off", when pilot was far away from where he should be in the pattern. For example, being long in the groove gives a "LIG PWO". +-- * 0.0 Point **CUT**: "Cut pass", when player was waved off but landed anyway. +-- +-- ## Foul Deck Waveoff +-- +-- A foul deck waveoff is called by the LSO if an aircraft is detected within the landing area when an approaching aircraft is crossing the ship's wake during Case I/II operations, +-- or with an aircraft approaching the 3/4 NM during Case III operations. +-- +-- The approaching aircraft will be notified via LSO radio comms and is supposed to overfly the landing area to enter the Bolter pattern. **This pass is not graded**. +-- +-- === +-- +-- # Scripting +-- +-- Writing a basic script is easy and can be done in two lines. +-- +-- local airbossStennis=AIRBOSS:New("USS Stennis", "Stennis") +-- airbossStennis:Start() +-- +-- The **first line** creates and AIRBOSS object via the @{#AIRBOSS.New}(*carriername*, *alias*) constructor. The first parameter *carriername* is name of the carrier unit as +-- defined in the mission editor. The second parameter *alias* is optional. This name will, e.g., be used for the F10 radio menu entry. If not given, the alias is identical +-- to the *carriername* of the first parameter. +-- +-- This simple script initializes a lot of parameters with default values: +-- +-- * TACAN channel is set to 74X, see @{#AIRBOSS.SetTACAN}, +-- * ICSL channel is set to 1, see @{#AIRBOSS.SetICLS}, +-- * LSO radio is set to 264 MHz FM, see @{#AIRBOSS.SetLSORadio}, +-- * Marshal radio is set to 305 MHz FM, see @{#AIRBOSS.SetMarshalRadio}, +-- * Default recovery case is set to 1, see @{#AIRBOSS.SetRecoveryCase}, +-- * Carrier Controlled Area (CCA) is set to 50 NM, see @{#AIRBOSS.SetCarrierControlledArea}, +-- * Default player skill "Flight Student" (easy), see @{#AIRBOSS.SetDefaultPlayerSkill}, +-- * Once the carrier reaches its final waypoint, it will restart its route, see @{#AIRBOSS.SetPatrolAdInfinitum}. +-- +-- The **second line** starts the AIRBOSS class. If you set options this should happen after the @{#AIRBOSS.New} and before @{#AIRBOSS.Start} command. +-- +-- If no recovery window is set like in the basic example, a window will automatically open 15 minutes after mission start and close again after three hours. +-- The next section explains how to set your own recovery times. +-- +-- ## Recovery Windows +-- +-- Recovery of aircraft is only allowed during defined time slots. You can define these slots via the @{#AIRBOSS.AddRecoveryWindow}(*start*, *stop*, *case*, *holdingoffset*) function. +-- The parameters are: +-- +-- * *start*: The start time as a string. For example "8:00" for a window opening at 8 am. Or "13:30+1" for half past one on the next day. Default (nil) is ASAP. +-- * *stop*: Time when the window closes as a string. Same format as *start*. Default is 90 minutes after start time. +-- * *case*: The recovery case during that window (1, 2 or 3). Default 1. +-- * *holdingoffset*: Holding offset angle in degrees. Only for Case II or III recoveries. Default 0 deg. Common +-15 deg or +-30 deg. +-- +-- If recovery is closed, AI flights will be send to marshal stacks and orbit there until the next window opens. +-- Players can request marshal via the F10 menu and will also be given a marshal stack. Currently, human players can request commence via the F10 radio regardless of +-- whether a window is open or not and will be allowed to enter the pattern (if not already full). This will probably change in the future. +-- +-- At the moment there is no automatic recovery case set depending on weather or daytime. So it is the AIRBOSS (i.e. you as mission designer) who needs to make that decision. +-- It is probably a good idea to synchronize the timing with the waypoints of the carrier. For example, setting up the waypoints such that the carrier +-- already has turning into the wind, when a recovery window opens. +-- +-- The code for setting up multiple recovery windows could look like this +-- local airbossStennis=AIRBOSS:New("USS Stennis", "Stennis") +-- airbossStennis:AddRecoveryWindow("8:30", "9:30", 1) +-- airbossStennis:AddRecoveryWindow("12:00", "13:15", 2, 15) +-- airbossStennis:AddRecoveryWindow("23:30", "00:30+1", 3, -30) +-- airbossStennis:Start() +-- +-- This will open a Case I recovery window from 8:30 to 9:30. Then a Case II recovery from 12:00 to 13:15, where the holing offset is +15 degrees wrt BRC. +-- Finally, a Case III window opens 23:30 on the day the mission starts and closes 0:30 on the following day. The holding offset is -30 degrees wrt FB. +-- +-- Note that incoming flights will be assigned a holding pattern for the next opening window case if no window is open at the moment. So in the above example, +-- all flights incoming after 13:15 will be assigned to a Case III marshal stack. Therefore, you should make sure that no flights are incoming long before the +-- next window opens or adjust the recovery planning accordingly. +-- +-- The following example shows how you set up a recovery window for the next week: +-- +-- for i=0,7 do +-- AddRecoveryWindow(string.format("08:05:00+%d", i), string.format("08:50:00+%d", i)) +-- end +-- +-- ### Turning into the Wind +-- +-- For each recovery window, you can define if the carrier should automatically turn into the wind. This is done by passing one or two additional arguments to the @{#AIRBOSS.AddRecoveryWindow} function: +-- +-- airbossStennis:AddRecoveryWindow("8:30", "9:30", 1, nil, true, 20) +-- +-- Setting the fifth parameter to *true* enables the automatic turning into the wind. The sixth parameter (here 20) specifies the speed in knots the carrier will go. +-- The carrier will steam into the wind for as long as the recovery window is open. The distance up to which possible collisions are detected can be set by the @{#AIRBOSS.SetCollisionDistance} function. +-- +-- However, the airboss scans the type of the surface up to 5 NM in the direction of movement of the carrier. If he detects anything but deep water, he will stop the current course and head back to +-- the point where he initially turned into the wind. +-- +-- The same holds true after the recovery window closes. The carrier will head back to the place where he left its assigned route and resume the path to the next waypoint defined in the mission editor. +-- +-- === +-- +-- # Persistence of Player Results +-- +-- LSO grades of players can be saved to disk and later reloaded when a new mission is started. +-- +-- ## Prerequisites +-- +-- **Important** By default, DCS does not allow for writing data to files. Therefore, one first has to comment out the line "sanitizeModule('io')" and "sanitizeModule('lfs')", i.e. +-- +-- do +-- sanitizeModule('os') +-- --sanitizeModule('io') -- required for saving files +-- --sanitizeModule('lfs') -- optional for setting the default path to your "Saved Games\DCS" folder +-- require = nil +-- loadlib = nil +-- end +-- +-- in the file "MissionScripting.lua", which is located in the subdirectory "Scripts" of your DCS installation root directory. +-- +-- **WARNING** Desanitizing the "io" and "lfs" modules makes your machine or server vulnerable to attacks from the outside! Use this at your own risk. +-- +-- ## Save Results +-- +-- Saving asset data to file is achieved by the @{AIRBOSS.Save}(*path*, *filename*) function. +-- +-- The parameter *path* specifies the path on the file system where the +-- player grades are saved. If you do not specify a path, the file is saved your the DCS installation root directory if the **lfs** module is *not* desanizied or +-- your "Saved Games\\DCS" folder in case you did desanitize the **lfs** module. +-- +-- The parameter *filename* is optional and defines the name of the saved file. By default this is automatically created from the AIRBOSS carrier name/alias, i.e. +-- "Airboss-USS Stennis_LSOgrades.csv", if the alias is "USS Stennis". +-- +-- In the easiest case, you desanitize the **io** and **lfs** modules and just add the line +-- +-- airbossStennis:Save() +-- +-- If you want to specify an explicit path you can do this by +-- +-- airbossStennis:Save("D:\\My Airboss Data\\") +-- +-- This will save all player grades to in "D:\\My Airboss Data\\Airboss-USS Stennis_LSOgrades.csv". +-- +-- ### Automatic Saving +-- +-- The player grades can be saved automatically after each graded player pass via the @{AIRBOSS.SetAutoSave}(*path*, *filename*) function. Again the parameters *path* and *filename* are optional. +-- In the simplest case, you desanitize the **lfs** module and just add +-- +-- +-- airbossStennis:SetAutoSave() +-- +-- Note that the the stats are saved after the *final* grade has been given, i.e. the player has landed on the carrier. After intermediate results such as bolters or waveoffs the stats are not automatically saved. +-- +-- In case you want to specify an explicit path, you can write +-- +-- airbossStennis:SetAutoSave("D:\\My Airboss Data\\") +-- +-- ## Results Output +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_PersistenceResultsTable.png) +-- +-- The results file is stored as comma separated file. The columns are +-- +-- * *Name*: The player name. +-- * *Pass*: A running number counting the passes of the player +-- * *Points Final*: The final points (i.e. when the player has landed). This is the average over all previous bolters or waveoffs, if any. +-- * *Points Pass*: The points of each pass including bolters and waveoffs. +-- * *Grade*: LSO grade. +-- * *Details*: Detailed analysis of deviations within the groove. +-- * *Wire*: Trapped wire, if any. +-- * *Tgroove*: Time in the groove in seconds (not applicable during Case III). +-- * *Case*: The recovery case operations in progress during the pass. +-- +-- ## Load Results +-- +-- Loading player grades from file is achieved by the @{AIRBOSS.Load}(*path*, *filename*) function. The parameter *path* specifies the path on the file system where the +-- data is loaded from. If you do not specify a path, the file is loaded from your the DCS installation root directory or, if **lfs** was desanitized from you "Saved Games\DCS" directory. +-- The parameter *filename* is optional and defines the name of the file to load. By default this is automatically generated from the AIBOSS carrier name/alias, for example +-- "Airboss-USS Stennis_LSOgrades.csv". +-- +-- Note that the AIRBOSS FSM **must not be started** in order to load the data. In other words, loading should happen **after** the +-- @{#AIRBOSS.New} command is specified in the code but **before** the @{#AIRBOSS.Start} command is given. +-- +-- The easiest was to load player results is +-- +-- airbossStennis:New("USS Stennis") +-- airbossStennis:Load() +-- airbossStennis:SetAutoSave() +-- -- Additional specification of parameters such as recovery windows etc, if required. +-- airbossStennis:Start() +-- +-- This sequence loads all available player grades from the default file and automatically saved them when a player received a (final) grade. Again, if **lfs** was desanitized, the files are save to and loaded +-- from the "Saved Games\DCS" directory. If **lfs** was *not* desanitized, the DCS root installation folder is the default path. +-- +-- === +-- +-- # Sound Files +-- +-- An important aspect of the AIRBOSS is that it uses voice overs for greater immersion. The necessary sound files can be obtained from the +-- MOOSE Discord in the [#ops-airboss](https://discordapp.com/channels/378590350614462464/527363141185830915) channel. Check out the **pinned messages**. +-- +-- However, including sound files into a new mission is tedious as these usually need to be included into the mission **miz** file via (unused) triggers. +-- +-- The default location inside the miz file is "l10n/DEFAULT/". But simply opening the *miz* file with e.g. [7-zip](https://www.7-zip.org/) and copying the files into that folder does not work. +-- The next time the mission is saved, files not included via trigger are automatically removed by DCS. +-- +-- However, if you create a new folder inside the miz file, which contains the sounds, it will not be deleted and can be used. The location of the sound files can be specified +-- via the @{#AIRBOSS.SetSoundfilesFolder}(*folderpath*) function. The parameter *folderpath* defines the location of the sound files folder within the mission *miz* file. +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_SoundfilesFolder.png) +-- +-- For example as +-- +-- airbossStennis:SetSoundfilesFolder("Airboss Soundfiles/") +-- +-- === +-- +-- # AI Handling +-- +-- The @{#AIRBOSS} class allows to handle incoming AI units and integrate them into the marshal and landing pattern. +-- +-- By default, incoming carrier capable aircraft which are detecting inside the Carrier Controlled Area (CCA) and approach the carrier by more than 5 NM are automatically guided to the holding zone. +-- Each AI group gets its own marshal stack in the holding pattern. Once a recovery window opens, the AI group of the lowest stack is transitioning to the landing pattern +-- and the Marshal stack collapses. +-- +-- If no AI handling is desired, this can be turned off via the @{#AIRBOSS.SetHandleAIOFF} function. +-- +-- ## Known Issues +-- +-- Dealing with the DCS AI is a big challenge and there is only so much one can do. Please bear this in mind! +-- +-- ### Pattern Updates +-- +-- The holding position of the AI is updated regularly when the carrier has changed its position by more then 2.5 NM or changed its course significantly. +-- The patterns are realized by orbit or racetrack patterns of the DCS scripting API. +-- However, when the position is updated or the marshal stack collapses, it comes to disruptions of the regular orbit because a new waypoint with a new +-- orbit task needs to be created. +-- +-- ### Recovery Cases +-- +-- The AI performs a very realistic Case I recovery. Therefore, we already have a good Case I and II recovery simulation since the final part of Case II is a +-- Case I recovery. However, I don't think the AI can do a proper Case III recovery. If you give the AI the landing command, it is out of our hands and will +-- always go for a Case I in the final pattern part. Maybe this will improve in future DCS version but right now, there is not much we can do about it. +-- +-- === +-- +-- # Examples +-- +-- In this section a few simple examples are given to illustrate the scripting part. +-- +-- ## Simple Case +-- +-- -- Create AIRBOSS object. +-- local AirbossStennis=AIRBOSS:New("USS Stennis") +-- +-- -- Add recovery windows: +-- -- Case I from 9 to 12 am. +-- local window1=AirbossStennis:AddRecoveryWindow("8:55", "12:00", 1) +-- -- Case II with +15 degrees holding offset from 1500 for 90 min. +-- local window2=AirbossStennis:AddRecoveryWindow("14:55", "16:30", 2, 15) +-- -- Case III with +30 degrees holding offset from 2100 to 2330. +-- local window3=AirbossStennis:AddRecoveryWindow("21:00", "23:30", 3, 30) +-- +-- -- Load all saved player grades from your "Saved Games\DCS" folder (if lfs was desanitized). +-- AirbossStennis:Load() +-- +-- -- Automatically save player results to your "Saved Games\DCS" folder each time a player get a final grade from the LSO. +-- AirbossStennis:SetAutoSave() +-- +-- -- Start airboss class. +-- AirbossStennis:Start() +-- +-- === +-- +-- # Debugging +-- +-- In case you have problems, it is always a good idea to have a look at your DCS log file. You find it in your "Saved Games" folder, so for example in +-- C:\Users\\Saved Games\DCS\Logs\dcs.log +-- All output concerning the @{#AIRBOSS} class should have the string "AIRBOSS" in the corresponding line. +-- Searching for lines that contain the string "error" or "nil" can also give you a hint what's wrong. +-- +-- The verbosity of the output can be increased by adding the following lines to your script: +-- +-- BASE:TraceOnOff(true) +-- BASE:TraceLevel(1) +-- BASE:TraceClass("AIRBOSS") +-- +-- To get even more output you can increase the trace level to 2 or even 3, c.f. @{Core.Base#BASE} for more details. +-- +-- ### Debug Mode +-- +-- You have the option to enable the debug mode for this class via the @{#AIRBOSS.SetDebugModeON} function. +-- If enabled, status and debug text messages will be displayed on the screen. Also informative marks on the F10 map are created. +-- +-- @field #AIRBOSS +AIRBOSS = { + ClassName = "AIRBOSS", + Debug = false, + lid = nil, + carrier = nil, + carriertype = nil, + carrierparam = {}, + alias = nil, + airbase = nil, + waypoints = {}, + currentwp = nil, + beacon = nil, + TACANon = nil, + TACANchannel = nil, + TACANmode = nil, + TACANmorse = nil, + ICLSon = nil, + ICLSchannel = nil, + ICLSmorse = nil, + LSORadio = nil, + LSOFreq = nil, + LSOModu = nil, + MarshalRadio = nil, + MarshalFreq = nil, + MarshalModu = nil, + radiotimer = nil, + zoneCCA = nil, + zoneCCZ = nil, + players = {}, + menuadded = {}, + BreakEntry = {}, + BreakEarly = {}, + BreakLate = {}, + Abeam = {}, + Ninety = {}, + Wake = {}, + Final = {}, + Groove = {}, + Platform = {}, + DirtyUp = {}, + Bullseye = {}, + defaultcase = nil, + case = nil, + defaultoffset = nil, + holdingoffset = nil, + recoverytimes = {}, + flights = {}, + Qpattern = {}, + Qmarshal = {}, + Qwaiting = {}, + RQMarshal = {}, + RQLSO = {}, + Nmaxpattern = nil, + Nmaxmarshal = nil, + NmaxSection = nil, + NmaxStack = nil, + handleai = nil, + tanker = nil, + warehouse = nil, + Corientation = nil, + Corientlast = nil, + Cposition = nil, + defaultskill = nil, + adinfinitum = nil, + magvar = nil, + Tcollapse = nil, + recoverywindow = nil, + usersoundradio = nil, + Tqueue = nil, + dTqueue = nil, + dTstatus = nil, + menumarkzones = nil, + menusmokezones = nil, + playerscores = nil, + autosave = nil, + autosavefile = nil, + autosavepath = nil, + marshalradius = nil, + airbossnice = nil, + staticweather = nil, + windowcount = 0, + LSOdT = nil, + senderac = nil, + turnintowind = nil, + detour = nil, + squadsetAI = nil, + menusingle = nil, + collisiondist = nil, + Tmessage = nil, + soundfolder = nil, +} + +--- Player aircraft types capable of landing on carriers. +-- @type AIRBOSS.AircraftPlayer +-- @field #string AV8B AV-8B Night Harrier (not yet supported). +-- @field #string HORNET F/A-18C Lot 20 Hornet. +-- @field #string A4EC A-4E Community mod. +AIRBOSS.AircraftPlayer={ + --AV8B="AV8BNA", + HORNET="FA-18C_hornet", + A4EC="A-4E-C", +} + +--- Aircraft types capable of landing on carrier (human+AI). +-- @type AIRBOSS.AircraftCarrier +-- @field #string AV8B AV-8B Night Harrier (not yet supported). +-- @field #string A4EC A-4E Community mod. +-- @field #string HORNET F/A-18C Lot 20 Hornet by Eagle Dynamics. +-- @field #string F14A F-14A by Heatblur. +-- @field #string F14B F-14B by Heatblur. +-- @field #string F14A_AI F-14A Tomcat (AI). +-- @field #string FA18C F/A-18C Hornet (AI). +-- @field #string S3B Lockheed S-3B Viking. +-- @field #string S3BTANKER Lockheed S-3B Viking tanker. +-- @field #string E2D Grumman E-2D Hawkeye AWACS. +AIRBOSS.AircraftCarrier={ + --AV8B="AV8BNA", + HORNET="FA-18C_hornet", + A4EC="A-4E-C", + F14A="F-14A_tomcat", + F14B="F-14B_tomcat", + F14A_AI="F-14A", + FA18C="F/A-18C", + S3B="S-3B", + S3BTANKER="S-3B Tanker", + E2D="E-2C", +} + +--- Carrier types. +-- @type AIRBOSS.CarrierType +-- @field #string STENNIS USS John C. Stennis (CVN-74) +-- @field #string VINSON USS Carl Vinson (CVN-70) +-- @field #string TARAWA USS Tarawa (LHA-1) +-- @field #string KUZNETSOV Admiral Kuznetsov (CV 1143.5) +AIRBOSS.CarrierType={ + STENNIS="Stennis", + VINSON="VINSON", + TARAWA="LHA_Tarawa", + KUZNETSOV="KUZNECOW", +} + +--- Carrier specific parameters. +-- @type AIRBOSS.CarrierParameters +-- @field #number rwyangle Runway angle in degrees. for carriers with angled deck. For USS Stennis -9 degrees. +-- @field #number sterndist Distance in meters from carrier position to stern of carrier. For USS Stennis -150 meters. +-- @field #number deckheight Height of deck in meters. For USS Stennis ~63 ft = 19 meters. +-- @field #number wire1 Distance in meters from carrier position to first wire. +-- @field #number wire2 Distance in meters from carrier position to second wire. +-- @field #number wire3 Distance in meters from carrier position to third wire. +-- @field #number wire4 Distance in meters from carrier position to fourth wire. +-- @field #number rwylength Length of the landing runway in meters. +-- @field #number rwywidth Width of the landing runway in meters. +-- @field #number totlength Total length of carrier. +-- @field #number totwidthstarboard Total with of the carrier from stern position to starboard side (asymmetric carriers). +-- @field #number totwidthport Total with of the carrier from stern position to port side (asymmetric carriers). + +--- Aircraft specific Angle of Attack (AoA) (or alpha) parameters. +-- @type AIRBOSS.AircraftAoA +-- @field #number OnSpeedMin Minimum on speed AoA. Values below are fast +-- @field #number OnSpeedMax Maximum on speed AoA. Values above are slow. +-- @field #number OnSpeed Optimal on-speed AoA. +-- @field #number Fast Fast AoA threshold. Smaller means faster. +-- @field #number Slow Slow AoA threshold. Larger means slower. +-- @field #number FAST Really fast AoA threshold. +-- @field #number SLOW Really slow AoA threshold. + +--- Pattern steps. +-- @type AIRBOSS.PatternStep +-- @field #string UNDEFINED "Undefined". +-- @field #string REFUELING "Refueling". +-- @field #string SPINNING "Spinning". +-- @field #string COMMENCING "Commencing". +-- @field #string HOLDING "Holding". +-- @field #string WAITING "Waiting for free Marshal stack". +-- @field #string PLATFORM "Platform". +-- @field #string ARCIN "Arc Turn In". +-- @field #string ARCOUT "Arc Turn Out". +-- @field #string DIRTYUP "Dirty Up". +-- @field #string BULLSEYE "Bullseye". +-- @field #string INITIAL "Initial". +-- @field #string BREAKENTRY "Break Entry". +-- @field #string EARLYBREAK "Early Break". +-- @field #string LATEBREAK "Late Break". +-- @field #string ABEAM "Abeam". +-- @field #string NINETY "Ninety". +-- @field #string WAKE "Wake". +-- @field #string FINAL "Final". +-- @field #string GROOVE_XX "Groove X". +-- @field #string GROOVE_IM "Groove In the Middle". +-- @field #string GROOVE_IC "Groove In Close". +-- @field #string GROOVE_AR "Groove At the Ramp". +-- @field #string GROOVE_IW "Groove In the Wires". +-- @field #string BOLTER "Bolter Pattern". +-- @field #string DEBRIEF "Debrief". +AIRBOSS.PatternStep={ + UNDEFINED="Undefined", + REFUELING="Refueling", + SPINNING="Spinning", + COMMENCING="Commencing", + HOLDING="Holding", + WAITING="Waiting for free Marshal stack", + PLATFORM="Platform", + ARCIN="Arc Turn In", + ARCOUT="Arc Turn Out", + DIRTYUP="Dirty Up", + BULLSEYE="Bullseye", + INITIAL="Initial", + BREAKENTRY="Break Entry", + EARLYBREAK="Early Break", + LATEBREAK="Late Break", + ABEAM="Abeam", + NINETY="Ninety", + WAKE="Wake", + FINAL="Turn Final", + GROOVE_XX="Groove X", + GROOVE_IM="Groove In the Middle", + GROOVE_IC="Groove In Close", + GROOVE_AR="Groove At the Ramp", + GROOVE_IW="Groove In the Wires", + BOLTER="Bolter Pattern", + DEBRIEF="Debrief", +} + +--- Groove position. +-- @type AIRBOSS.GroovePos +-- @field #string X0 "X0": Entering the groove. +-- @field #string XX "XX": At the start, i.e. 3/4 from the run down. +-- @field #string IM "IM": In the middle. +-- @field #string IC "IC": In close. +-- @field #string AR "AR": At the ramp. +-- @field #string IW "IW": In the wires. +AIRBOSS.GroovePos={ + X0="X0", + XX="XX", + IM="IM", + IC="IC", + AR="AR", + IW="IW", +} + +--- Radio. +-- @type AIRBOSS.Radio +-- @field #number frequency Frequency in Hz. +-- @field #number modulation Band modulation. +-- @field #string alias Radio alias. + +--- Radio sound file and subtitle. +-- @type AIRBOSS.RadioCall +-- @field #string file Sound file name without suffix. +-- @field #string suffix File suffix/extension, e.g. "ogg". +-- @field #boolean loud Loud version of sound file available. +-- @field #string subtitle Subtitle displayed during transmission. +-- @field #number duration Duration of the sound in seconds. This is also the duration the subtitle is displayed. +-- @field #number subduration Duration in seconds the subtitle is displayed. +-- @field #string modexsender Onboard number of the sender (optional). +-- @field #string modexreceiver Onboard number of the receiver (optional). +-- @field #string sender Sender of the message (optional). Default radia alias. + +--- LSO radio calls. +-- @type AIRBOSS.LSOCall +-- @field #AIRBOSS.RadioCall RADIOCHECK "Paddles, radio check" call. +-- @field #AIRBOSS.RadioCall RIGHTFORLINEUP "Right for line up" call. +-- @field #AIRBOSS.RadioCall COMELEFT "Come left" call. +-- @field #AIRBOSS.RadioCall HIGH "You're high" call. +-- @field #AIRBOSS.RadioCall LOW "You're low" call. +-- @field #AIRBOSS.RadioCall POWER "Power" call. +-- @field #AIRBOSS.RadioCall FAST "You're fast" call. +-- @field #AIRBOSS.RadioCall SLOW "You're slow" call. +-- @field #AIRBOSS.RadioCall PADDLESCONTACT "Paddles, contact" call. +-- @field #AIRBOSS.RadioCall CALLTHEBALL "Call the Ball" +-- @field #AIRBOSS.RadioCall ROGERBALL "Roger ball" call. +-- @field #AIRBOSS.RadioCall WAVEOFF "Wave off" call. +-- @field #AIRBOSS.RadioCall BOLTER "Bolter, Bolter" call +-- @field #AIRBOSS.RadioCall LONGINGROOVE "You're long in the groove" call. +-- @field #AIRBOSS.RadioCall FOULDECK "Foul Deck" call. +-- @field #AIRBOSS.RadioCall DEPARTANDREENTER "Depart and re-enter" call. +-- @field #AIRBOSS.RadioCall WELCOMEABOARD "Welcome aboard" call. +-- @field #AIRBOSS.RadioCall N0 "Zero" call. +-- @field #AIRBOSS.RadioCall N1 "One" call. +-- @field #AIRBOSS.RadioCall N2 "Two" call. +-- @field #AIRBOSS.RadioCall N3 "Three" call. +-- @field #AIRBOSS.RadioCall N4 "Four" call. +-- @field #AIRBOSS.RadioCall N5 "Five" call. +-- @field #AIRBOSS.RadioCall N6 "Six" call. +-- @field #AIRBOSS.RadioCall N7 "Seven" call. +-- @field #AIRBOSS.RadioCall N8 "Eight" call. +-- @field #AIRBOSS.RadioCall N9 "Nine" call. +-- @field #AIRBOSS.RadioCall CLICK Radio end transmission click sound. +-- @field #AIRBOSS.RadioCall NOISE Static noise sound. +AIRBOSS.LSOCall={ + RADIOCHECK={ + file="LSO-RadioCheck", + suffix="ogg", + loud=false, + subtitle="Paddles, radio check", + duration=1.1, + subduration=5, + }, + RIGHTFORLINEUP={ + file="LSO-RightForLineup", + suffix="ogg", + loud=true, + subtitle="Right for line up", + duration=0.80, + subduration=1, + }, + COMELEFT={ + file="LSO-ComeLeft", + suffix="ogg", + loud=true, + subtitle="Come left", + duration=0.60, + subduration=1, + }, + HIGH={ + file="LSO-High", + suffix="ogg", + loud=true, + subtitle="You're high", + duration=0.65, + subduration=1, + }, + LOW={ + file="LSO-Low", + suffix="ogg", + loud=true, + subtitle="You're low", + duration=0.50, + subduration=1, + }, + POWER={ + file="LSO-Power", + suffix="ogg", + loud=true, + subtitle="Power", + duration=0.50, --0.45 was too short + subduration=1, + }, + SLOW={ + file="LSO-Slow", + suffix="ogg", + loud=true, + subtitle="You're slow", + duration=0.65, + subduration=1, + }, + FAST={ + file="LSO-Fast", + suffix="ogg", + loud=true, + subtitle="You're fast", + duration=0.7, + subduration=1, + }, + CALLTHEBALL={ + file="LSO-CallTheBall", + suffix="ogg", + loud=false, + subtitle="Call the ball", + duration=0.6, + subduration=2, + }, + ROGERBALL={ + file="LSO-RogerBall", + suffix="ogg", + loud=false, + subtitle="Roger ball", + duration=0.7, + subduration=2, + }, + WAVEOFF={ + file="LSO-WaveOff", + suffix="ogg", + loud=false, + subtitle="Wave off", + duration=0.6, + subduration=5, + }, + BOLTER={ + file="LSO-BolterBolter", + suffix="ogg", + loud=false, + subtitle="Bolter, Bolter", + duration=0.75, + subduration=5, + }, + LONGINGROOVE={ + file="LSO-LongInTheGroove", + suffix="ogg", + loud=false, + subtitle="You're long in the groove", + duration=1.2, + subduration=5, + }, + FOULDECK={ + file="LSO-FoulDeck", + suffix="ogg", + loud=false, + subtitle="Foul deck", + duration=0.62, + subduration=5, + }, + DEPARTANDREENTER={ + file="LSO-DepartAndReenter", + suffix="ogg", + loud=false, + subtitle="Depart and re-enter", + duration=1.1, + subduration=5, + }, + PADDLESCONTACT={ + file="LSO-PaddlesContact", + suffix="ogg", + loud=false, + subtitle="Paddles, contact", + duration=1.0, + subduration=5, + }, + WELCOMEABOARD={ + file="LSO-WelcomeAboard", + suffix="ogg", + loud=false, + subtitle="Welcome aboard", + duration=1.0, + subduration=5, + }, + N0={ + file="LSO-N0", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N1={ + file="LSO-N1", + suffix="ogg", + loud=false, + subtitle="", + duration=0.25, + }, + N2={ + file="LSO-N2", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N3={ + file="LSO-N3", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N4={ + file="LSO-N4", + suffix="ogg", + loud=false, + subtitle="", + duration=0.39, + }, + N5={ + file="LSO-N5", + suffix="ogg", + loud=false, + subtitle="", + duration=0.39, + }, + N6={ + file="LSO-N6", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N7={ + file="LSO-N7", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N8={ + file="LSO-N8", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N9={ + file="LSO-N9", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + CLICK={ + file="AIRBOSS-RadioClick", + suffix="ogg", + loud=false, + subtitle="", + duration=0.35, + }, + NOISE={ + file="AIRBOSS-Noise", + suffix="ogg", + loud=false, + subtitle="", + duration=3.6, + }, +} + +--- Marshal radio calls. +-- @type AIRBOSS.MarshalCall +-- @field #AIRBOSS.RadioCall RADIOCHECK "Radio check" call. +-- @field #AIRBOSS.RadioCall SAYNEEDLES "Say needles" call. +-- @field #AIRBOSS.RadioCall FLYNEEDLES "Fly your needles" call. +-- @field #AIRBOSS.RadioCall N0 "Zero" call. +-- @field #AIRBOSS.RadioCall N1 "One" call. +-- @field #AIRBOSS.RadioCall N2 "Two" call. +-- @field #AIRBOSS.RadioCall N3 "Three" call. +-- @field #AIRBOSS.RadioCall N4 "Four" call. +-- @field #AIRBOSS.RadioCall N5 "Five" call. +-- @field #AIRBOSS.RadioCall N6 "Six" call. +-- @field #AIRBOSS.RadioCall N7 "Seven" call. +-- @field #AIRBOSS.RadioCall N8 "Eight" call. +-- @field #AIRBOSS.RadioCall N9 "Nine" call. +-- @field #AIRBOSS.RadioCall CLICK Radio end transmission click sound. +-- @field #AIRBOSS.RadioCall NOISE Static noise sound. +AIRBOSS.MarshalCall={ + RADIOCHECK={ + file="MARSHAL-RadioCheck", + suffix="ogg", + loud=false, + subtitle="Radio check", + duration=1.1, + subduration=5, + }, + SAYNEEDLES={ + file="MARSHAL-SayNeedles", + suffix="ogg", + loud=false, + subtitle="Say needles", + duration=0.9, + subduration=5, + }, + FLYNEEDLES={ + file="MARSHAL-FlyYourNeedles", + suffix="ogg", + loud=false, + subtitle="Fly your needles", + duration=0.9, + subduration=5, + }, + -- TODO: Other voice overs for marshal. + N0={ + file="LSO-N0", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N1={ + file="LSO-N1", + suffix="ogg", + loud=false, + subtitle="", + duration=0.25, + }, + N2={ + file="LSO-N2", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N3={ + file="LSO-N3", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N4={ + file="LSO-N4", + suffix="ogg", + loud=false, + subtitle="", + duration=0.39, + }, + N5={ + file="LSO-N5", + suffix="ogg", + loud=false, + subtitle="", + duration=0.39, + }, + N6={ + file="LSO-N6", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N7={ + file="LSO-N7", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N8={ + file="LSO-N8", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N9={ + file="LSO-N9", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, --0.38 too short + }, + CLICK={ + file="AIRBOSS-RadioClick", + suffix="ogg", + loud=false, + subtitle="", + duration=0.35, + }, + NOISE={ + file="AIRBOSS-Noise", + suffix="ogg", + loud=false, + subtitle="", + duration=3.6, + }, +} + +--- Difficulty level. +-- @type AIRBOSS.Difficulty +-- @field #string EASY Flight Student. Shows tips and hints in important phases of the approach. +-- @field #string NORMAL Naval aviator. Moderate number of hints but not really zip lip. +-- @field #string HARD TOPGUN graduate. For people who know what they are doing. Nearly *ziplip*. +AIRBOSS.Difficulty={ + EASY="Flight Student", + NORMAL="Naval Aviator", + HARD="TOPGUN Graduate", +} + +--- Recovery window parameters. +-- @type AIRBOSS.Recovery +-- @field #number START Start of recovery in seconds of abs mission time. +-- @field #number STOP End of recovery in seconds of abs mission time. +-- @field #number CASE Recovery case (1-3) of that time slot. +-- @field #number OFFSET Angle offset of the holding pattern in degrees. Usually 0, +-15, or +-30 degrees. +-- @field #boolean OPEN Recovery window is currently open. +-- @field #boolean OVER Recovery window is over and closed. +-- @field #boolean WIND Carrier will turn into the wind. +-- @field #number SPEED The speed in knots the carrier has during the recovery. +-- @field #number ID Recovery window ID. + +--- Groove data. +-- @type AIRBOSS.GrooveData +-- @field #number Step Current step. +-- @field #number AoA Angle of Attack. +-- @field #number Alt Altitude in meters. +-- @field #number GSE Glideslope error in degrees. +-- @field #number LUE Lineup error in degrees. +-- @field #number Roll Roll angle. +-- @field #number Rhdg Relative heading player to carrier. 0=parallel, +-90=perpendicular. +-- @field #number TGroove Time stamp when pilot entered the groove. +-- @field #string FlyThrough Fly through up "/" or fly through down "\\". + +--- LSO grade +-- @type AIRBOSS.LSOgrade +-- @field #string grade LSO grade, i.e. _OK_, OK, (OK), --, CUT +-- @field #number points Points received. +-- @field #number finalscore Points received after player has finally landed. This is the average over all incomplete passes (bolter, waveoff) before. +-- @field #string details Detailed flight analysis. +-- @field #number wire Wire caught. +-- @field #number Tgroove Time in the groove in seconds. +-- @field #number case Recovery case. + +--- Checkpoint parameters triggering the next step in the pattern. +-- @type AIRBOSS.Checkpoint +-- @field #string name Name of checkpoint. +-- @field #number Xmin Minimum allowed longitual distance to carrier. +-- @field #number Xmax Maximum allowed longitual distance to carrier. +-- @field #number Zmin Minimum allowed latitudal distance to carrier. +-- @field #number Zmax Maximum allowed latitudal distance to carrier. +-- @field #number LimitXmin Latitudal threshold for triggering the next step if XXmax. +-- @field #number LimitZmin Latitudal threshold for triggering the next step if ZZmax. + +--- Parameters of a flight group. +-- @type AIRBOSS.FlightGroup +-- @field Wrapper.Group#GROUP group Flight group. +-- @field #string groupname Name of the group. +-- @field #number nunits Number of units in group. +-- @field #number dist0 Distance to carrier in meters when the group was first detected inside the CCA. +-- @field #number time Timestamp in seconds of timer.getAbsTime() of the last important event, e.g. added to the queue. +-- @field Core.UserFlag#USERFLAG flag User flag for triggering events for the flight. +-- @field #boolean ai If true, flight is purly AI. +-- @field #string actype Aircraft type name. +-- @field #table onboardnumbers Onboard numbers of aircraft in the group. +-- @field #string onboard Onboard number of player or first unit in group. +-- @field #number case Recovery case of flight. +-- @field #string seclead Name of section lead. +-- @field #table section Other human flight groups belonging to this flight. This flight is the lead. +-- @field #boolean holding If true, flight is in holding zone. +-- @field #boolean ballcall If true, flight called the ball in the groove. +-- @field #table elements Flight group elements. +-- @field #number Tcharlie Charlie (abs) time in seconds. + +--- Parameters of an element in a flight group. +-- @type AIRBOSS.FlightElement +-- @field Wrapper.Unit#UNIT unit Aircraft unit. +-- @field #boolean ai If true, AI sits inside. If false, human player is flying. +-- @field #string onboard Onboard number of the aircraft. +-- @field #boolean ballcall If true, flight called the ball in the groove. +-- @field #boolean ai If true, element is AI. + +--- Player data table holding all important parameters of each player. +-- @type AIRBOSS.PlayerData +-- @field Wrapper.Unit#UNIT unit Aircraft of the player. +-- @field #string name Player name. +-- @field Wrapper.Client#CLIENT client Client object of player. +-- @field #string callsign Callsign of player. +-- @field #string difficulty Difficulty level. +-- @field #string step Current/next pattern step. +-- @field #boolean warning Set true once the player got a warning. +-- @field #number passes Number of passes. +-- @field #boolean attitudemonitor If true, display aircraft attitude and other parameters constantly. +-- @field #table debrief Debrief analysis of the current step of this pass. +-- @field #table lastdebrief Debrief of player performance of last completed pass. +-- @field #boolean landed If true, player landed or attempted to land. +-- @field #boolean boltered If true, player boltered. +-- @field #boolean waveoff If true, player was waved off during final approach. +-- @field #boolean patternwo If true, player was waved off during the pattern. +-- @field #boolean lig If true, player was long in the groove. +-- @field #boolean fouldeckwo If true, player was waved off because of a foul deck. +-- @field #number Tlso Last time the LSO gave an advice. +-- @field #number Tgroove Time in the groove in seconds. +-- @field #number wire Wire caught by player when trapped. +-- @field #AIRBOSS.GroovePos groove Data table at each position in the groove. Elements are of type @{#AIRBOSS.GrooveData}. +-- @field #table points Points of passes until finally landed. +-- @field #number finalscore Final score if points are averaged over multiple passes. +-- @field #boolean valid If true, player made a valid approach. Is set true on start of Groove X. +-- @field #boolean subtitles If true, display subtitles of radio messages. +-- @extends #AIRBOSS.FlightGroup + +--- Main group level radio menu: F10 Other/Airboss. +-- @field #table MenuF10 +AIRBOSS.MenuF10={} + +--- Airboss mission level F10 root menu. +-- @field #table MenuF10Root +AIRBOSS.MenuF10Root=nil + +--- Airboss class version. +-- @field #string version +AIRBOSS.version="0.9.3" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Spin pattern. Add radio menu entry. Not sure what to add though?! +-- TODO: Player eject and crash debrief "gradings". +-- TODO: What happens when section lead or member dies? +-- TODO: PWO during case 2/3. +-- TODO: PWO when player comes too close to other flight. +-- TODO: Option to filter AI groups for recovery. +-- DONE: Rework radio messages. Better control over player board numbers. +-- DONE: Case I & II/III zone so that player gets into pattern automatically. Case I 3 position on the circle. Case II/III when the player enters the approach corridor maybe? +-- DONE: Add static weather information. +-- DONE: Allow up to two flights per Case I marshal stack. +-- DONE: Add max stack for Case I and define waiting queue outside CCZ. +-- DONE: Maybe do an additional step at the initial (Case II) or bullseye (Case III) and register player in case he missed some steps. +-- DONE: Subtitles off options on player level. +-- DONE: Persistence of results. +-- DONE: Foul deck waveoff. +-- DONE: Get Charlie time estimate function. +-- DONE: Average player grades until landing. +-- DONE: Check player heading at zones, e.g. initial. +-- DONE: Fix bug that player leaves the approach zone if he boltered or was waved off during Case II or III. NOTE: Partly due to increasing approach zone size. +-- DONE: Fix bug that player gets an altitude warning if stack collapses. NOTE: Would not work if two stacks Case I and II/III are used. +-- DONE: Improve radio messages. Maybe usersound for messages which are only meant for players? +-- DONE: Add voice over fly needs and welcome aboard. +-- DONE: Improve trapped wire calculation. +-- DONE: Carrier zone with dimensions of carrier. to check if landing happened on deck. +-- DONE: Carrier runway zone for fould deck check. +-- DONE: More Hints for Case II/III. +-- DONE: Set magnetic declination function. +-- DONE: First send AI to marshal and then allow them into the landing pattern ==> task function when reaching the waypoint. +-- DONE: Extract (static) weather from mission for cloud cover etc. +-- DONE: Check distance to players during approach. +-- DONE: Option to turn AI handling off. +-- DONE: Add user functions. +-- DONE: Update AI holding pattern wrt to moving carrier. +-- DONE: Generalize parameters for other carriers. +-- DONE: Generalize parameters for other aircraft. +-- DONE: Add radio check (LSO, AIRBOSS) to F10 radio menu. +-- DONE: Right pattern step after bolter/wo/patternWO? Guess so. +-- DONE: Set case II and III times (via recovery time). +-- DONE: Get correct wire when trapped. DONE but might need further tweaking. +-- DONE: Add radio transmission queue for LSO and airboss. +-- DONE: CASE II. +-- DONE: CASE III. +-- NOPE: Strike group with helo bringing cargo etc. Not yet. +-- DONE: Handle crash event. Delete A/C from queue, send rescue helo. +-- DONE: Get fuel state in pounds. (working for the hornet, did not check others) +-- DONE: Add aircraft numbers in queue to carrier info F10 radio output. +-- DONE: Monitor holding of players/AI in zoneHolding. +-- DONE: Transmission via radio. +-- DONE: Get board numbers. +-- DONE: Get an _OK_ pass if long in groove. Possible other pattern wave offs as well?! +-- DONE: Add scoring to radio menu. +-- DONE: Optimized debrief. +-- DONE: Add automatic grading. +-- DONE: Fix radio menu. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new AIRBOSS class object for a specific aircraft carrier unit. +-- @param #AIRBOSS self +-- @param carriername Name of the aircraft carrier unit as defined in the mission editor. +-- @param alias (Optional) Alias for the carrier. This will be used for radio messages and the F10 radius menu. Default is the carrier name as defined in the mission editor. +-- @return #AIRBOSS self or nil if carrier unit does not exist. +function AIRBOSS:New(carriername, alias) + + -- Inherit everthing from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #AIRBOSS + + -- Debug. + self:F2({carriername=carriername, alias=alias}) + + -- Set carrier unit. + self.carrier=UNIT:FindByName(carriername) + + -- Check if carrier unit exists. + if self.carrier==nil then + -- Error message. + local text=string.format("ERROR: Carrier unit %s could not be found! Make sure this UNIT is defined in the mission editor and check the spelling of the unit name carefully.", carriername) + MESSAGE:New(text, 120):ToAll() + self:E(text) + return nil + end + + -- Set some string id for output to DCS.log file. + self.lid=string.format("AIRBOSS %s | ", carriername) + + -- Get carrier type. + self.carriertype=self.carrier:GetTypeName() + + -- Set alias. + self.alias=alias or carriername + + -- Set carrier airbase object. + self.airbase=AIRBASE:FindByName(carriername) + + -- Create carrier beacon. + self.beacon=BEACON:New(self.carrier) + + -- Init player scores table. + self.playerscores={} + + ------------- + --- Defaults: + ------------- + + -- Set up Airboss radio. + self:SetMarshalRadio() + + -- Set up LSO radio. + self:SetLSORadio() + + -- Set LSO call interval. Default 4 sec. + self:SetLSOCallInterval() + + -- Radio scheduler. + self.radiotimer=SCHEDULER:New() + + -- Set magnetic declination. + self:SetMagneticDeclination() + + -- Set ICSL to channel 1. + self:SetICLS() + + -- Set TACAN to channel 74X. + self:SetTACAN() + + -- Set max aircraft in landing pattern. Default 4. + self:SetMaxLandingPattern() + + -- Set max Case I Marshal stacks. Default 3. + self:SetMaxMarshalStacks() + + -- Set max section members. Default 2. + self:SetMaxSectionSize() + + -- Set max flights per stack. Default is 2. + self:SetMaxFlightsPerStack() + + -- Set AI handling On. + self:SetHandleAION() + + -- Airboss is a nice guy. + self:SetAirbossNiceGuy() + + -- Mission uses static weather by default. + self:SetStaticWeather() + + -- Default recovery case. This sets self.defaultcase and self.case. Default Case I. + self:SetRecoveryCase() + + -- Set holding offset to 0 degrees. This set self.defaultoffset and self.holdingoffset. + self:SetHoldingOffsetAngle() + + -- Set Marshal stack radius. Default 2.75 NM, which gives a diameter of 5.5 NM. + self:SetMarshalRadius() + + -- Default player skill EASY. + self:SetDefaultPlayerSkill(AIRBOSS.Difficulty.EASY) + + -- CCA 50 NM radius zone around the carrier. + self:SetCarrierControlledArea() + + -- CCZ 5 NM radius zone around the carrier. + self:SetCarrierControlledZone() + + -- Carrier patrols its waypoints until the end of time. + self:SetPatrolAdInfinitum(true) + + -- Collision check distance. Default 5 NM. + self:SetCollisionDistance() + + -- Set update time intervals. + self:SetQueueUpdateTime() + self:SetStatusUpdateTime() + self:SetDefaultMessageDuration() + + -- Menu options. + self:SetMenuMarkZones() + self:SetMenuSmokeZones() + self:SetMenuSingleCarrier(false) + + -- Init carrier parameters. + if self.carriertype==AIRBOSS.CarrierType.STENNIS then + self:_InitStennis() + elseif self.carriertype==AIRBOSS.CarrierType.VINSON then + -- TODO: Carl Vinson parameters. + self:_InitStennis() + elseif self.carriertype==AIRBOSS.CarrierType.TARAWA then + -- TODO: Tarawa parameters. + self:_InitStennis() + elseif self.carriertype==AIRBOSS.CarrierType.KUZNETSOV then + -- Kusnetsov parameters - maybe... + self:_InitStennis() + else + self:E(self.lid..string.format("ERROR: Unknown carrier type %s!", tostring(self.carriertype))) + return nil + end + + ------------------- + -- Debug Section -- + ------------------- + + -- Debug trace. + if false then + self.Debug=true + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(1) + self.dTstatus=0.1 + end + + -- Smoke zones. + if self.Debug and false then + local case=2 + self:_GetZoneBullseye(case):SmokeZone(SMOKECOLOR.White, 45) + self:_GetZoneDirtyUp(case):SmokeZone(SMOKECOLOR.Orange, 45) + self:_GetZoneArcIn(case):SmokeZone(SMOKECOLOR.Blue, 45) + self:_GetZoneArcOut(case):SmokeZone(SMOKECOLOR.Blue, 45) + self:_GetZonePlatform(case):SmokeZone(SMOKECOLOR.Red, 45) + self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) + end + + -- Carrier parameter debug tests. + if false then + -- Stern coordinate. + local FB=self:GetFinalBearing(false) + local hdg=self:GetHeading(false) + + -- Stern pos. + local stern=self:_GetSternCoord() + + -- Bow pos. + local bow=stern:Translate(self.carrierparam.totlength, hdg) + + -- End of rwy. + local rwy=stern:Translate(self.carrierparam.rwylength, FB, true) + + --- Flare points and zones. + local function flareme() + + -- Carrier pos. + self:GetCoordinate():FlareYellow() + + -- Stern + stern:FlareGreen() + + -- Bow + bow:FlareYellow() + + -- Runway half width = 10 m. + local r1=stern:Translate(self.carrierparam.rwywidth*0.5, FB+90) + local r2=stern:Translate(self.carrierparam.rwywidth*0.5, FB-90) + r1:FlareWhite() + r2:FlareWhite() + + -- End of runway. + rwy:FlareRed() + + -- Right 30 meters from stern. + local cR=stern:Translate(self.carrierparam.totwidthstarboard, hdg+90) + cR:FlareYellow() + + -- Left 40 meters from stern. + local cL=stern:Translate(self.carrierparam.totwidthport, hdg-90) + cL:FlareYellow() + + -- Flare wires. + local w1=stern:Translate(self.carrierparam.wire1, FB) + local w2=stern:Translate(self.carrierparam.wire2, FB) + local w3=stern:Translate(self.carrierparam.wire3, FB) + local w4=stern:Translate(self.carrierparam.wire4, FB) + w1:FlareWhite() + w2:FlareYellow() + w3:FlareWhite() + w4:FlareYellow() + + -- Flare carrier and landing runway. + local cbox=self:_GetZoneCarrierBox() + local rbox=self:_GetZoneRunwayBox() + cbox:FlareZone(FLARECOLOR.Green, 5, nil, self.carrierparam.deckheight) + rbox:FlareZone(FLARECOLOR.White, 5, nil, self.carrierparam.deckheight) + end + + -- Flare points every 3 seconds for 3 minutes. + SCHEDULER:New(nil, flareme, {}, 1, 3, nil, 180) + end + + -- If calls should be part of self and individual for different carriers. + --[[ + -- Init default sound files. + for _name,_sound in pairs(AIRBOSS.LSOCall) do + local sound=_sound --#AIRBOSS.RadioCall + local text=string.format() + sound.subtitle=1 + sound.loud=1 + --self.radiocall[_name]=sound + end + ]] + + -- Debug: + if false then + local text="Playing default sound files:" + for _name,_call in pairs(AIRBOSS.LSOCall) do + local call=_call --#AIRBOSS.RadioCall + + -- Debug text. + text=text..string.format("\nFile=%s.%s, duration=%.2f sec, loud=%s, subtitle=\"%s\".", call.file, call.suffix, call.duration, tostring(call.loud), call.subtitle) + + -- Radio transmission to queue. + self:RadioTransmission(self.LSORadio, call, false, 10) + + -- Also play the loud version. + if call.loud then + self:RadioTransmission(self.LSORadio, call, true, 10) + end + end + self:I(self.lid..text) + end + + + ----------------------- + --- FSM Transitions --- + ----------------------- + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Load", "Stopped") -- Load player scores from file. + self:AddTransition("Stopped", "Start", "Idle") -- Start AIRBOSS script. + self:AddTransition("*", "Idle", "Idle") -- Carrier is idling. + self:AddTransition("Idle", "RecoveryStart", "Recovering") -- Start recovering aircraft. + self:AddTransition("Recovering", "RecoveryStop", "Idle") -- Stop recovering aircraft. + self:AddTransition("Recovering", "RecoveryPause", "Paused") -- Pause recovering aircraft. + self:AddTransition("Paused", "RecoveryUnpause", "Recovering") -- Unpause recovering aircraft. + self:AddTransition("*", "Status", "*") -- Update status of players and queues. + self:AddTransition("*", "RecoveryCase", "*") -- Switch to another case recovery. + self:AddTransition("*", "Save", "*") -- Save player scores to file. + self:AddTransition("*", "Stop", "Stopped") -- Stop AIRBOSS FMS. + + + --- Triggers the FSM event "Start" that starts the airboss. Initializes parameters and starts event handlers. + -- @function [parent=#AIRBOSS] Start + -- @param #AIRBOSS self + + --- Triggers the FSM event "Start" that starts the airboss after a delay. Initializes parameters and starts event handlers. + -- @function [parent=#AIRBOSS] __Start + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Idle" that puts the carrier into state "Idle" where no recoveries are carried out. + -- @function [parent=#AIRBOSS] Idle + -- @param #AIRBOSS self + + --- Triggers the FSM delayed event "Idle" that puts the carrier into state "Idle" where no recoveries are carried out. + -- @function [parent=#AIRBOSS] __Idle + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "RecoveryStart" that starts the recovery of aircraft. Marshalling aircraft are send to the landing pattern. + -- @function [parent=#AIRBOSS] RecoveryStart + -- @param #AIRBOSS self + -- @param #number Case Recovery case (1, 2 or 3) that is started. + -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. + + --- Triggers the FSM delayed event "RecoveryStart" that starts the recovery of aircraft. Marshalling aircraft are send to the landing pattern. + -- @function [parent=#AIRBOSS] __RecoveryStart + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + -- @param #number Case Recovery case (1, 2 or 3) that is started. + -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. + + --- On after "RecoveryStart" user function. Called when recovery of aircraft is started and carrier switches to state "Recovering". + -- @function [parent=#AIRBOSS] OnAfterRecoveryStart + -- @param #AIRBOSS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number Case The recovery case (1, 2 or 3) to start. + -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. + + --- Triggers the FSM event "RecoveryStop" that stops the recovery of aircraft. + -- @function [parent=#AIRBOSS] RecoveryStop + -- @param #AIRBOSS self + + --- Triggers the FSM delayed event "RecoveryStop" that stops the recovery of aircraft. + -- @function [parent=#AIRBOSS] __RecoveryStop + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "RecoveryPause" that pauses the recovery of aircraft. + -- @function [parent=#AIRBOSS] RecoveryPause + -- @param #AIRBOSS self + -- @param #number duration Duration of pause in seconds. After that recovery is automatically resumed. + + --- Triggers the FSM delayed event "RecoveryPause" that pauses the recovery of aircraft. + -- @function [parent=#AIRBOSS] __RecoveryPause + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + -- @param #number duration Duration of pause in seconds. After that recovery is automatically resumed. + + --- Triggers the FSM event "RecoveryUnpause" that resumes the recovery of aircraft if it was paused. + -- @function [parent=#AIRBOSS] RecoveryUnpause + -- @param #AIRBOSS self + + --- Triggers the FSM delayed event "RecoveryUnpause" that resumes the recovery of aircraft if it was paused. + -- @function [parent=#AIRBOSS] __RecoveryUnpause + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "RecoveryCase" that switches the aircraft recovery case. + -- @function [parent=#AIRBOSS] RecoveryCase + -- @param #AIRBOSS self + -- @param #number Case The new recovery case (1, 2 or 3). + -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. + + --- Triggers the delayed FSM event "RecoveryCase" that sets the used aircraft recovery case. + -- @function [parent=#AIRBOSS] __RecoveryCase + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + -- @param #number Case The new recovery case (1, 2 or 3). + -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. + + + --- Triggers the FSM event "Save" that saved the player scores to a file. + -- @function [parent=#AIRBOSS] Save + -- @param #AIRBOSS self + -- @param #string path Path where the file is saved. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized. + -- @param #string filename (Optional) File name. Default is AIRBOSS-*ALIAS*_LSOgrades.csv. + + --- Triggers the FSM delayed event "Save" that saved the player scores to a file. + -- @function [parent=#AIRBOSS] __Save + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + -- @param #string path Path where the file is saved. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized. + -- @param #string filename (Optional) File name. Default is AIRBOSS-*ALIAS*_LSOgrades.csv. + + --- On after "Save" event user function. Called when the player scores are saved to disk. + -- @function [parent=#AIRBOSS] OnAfterSave + -- @param #AIRBOSS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string path Path where the file is saved. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized. + -- @param #string filename (Optional) File name. Default is AIRBOSS-*ALIAS*_LSOgrades.csv. + + + --- Triggers the FSM event "Load" that loads the player scores from a file. AIRBOSS FSM must **not** be started at this point. + -- @function [parent=#AIRBOSS] Load + -- @param #AIRBOSS self + -- @param #string path Path where the file is located. Default is the DCS installation root directory. + -- @param #string filename (Optional) File name. Default is AIRBOSS-_LSOgrades.csv. + + --- Triggers the FSM delayed event "Load" that loads the player scores from a file. AIRBOSS FSM must **not** be started at this point. + -- @function [parent=#AIRBOSS] __Load + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + -- @param #string path Path where the file is located. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized. + -- @param #string filename (Optional) File name. Default is AIRBOSS-*ALIAS*_LSOgrades.csv. + + --- On after "Load" event user function. Called when the player scores are loaded from disk. + -- @function [parent=#AIRBOSS] OnAfterLoad + -- @param #AIRBOSS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string path Path where the file is located. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized. + -- @param #string filename (Optional) File name. Default is AIRBOSS-*ALIAS*_LSOgrades.csv. + + + --- Triggers the FSM event "Stop" that stops the airboss. Event handlers are stopped. + -- @function [parent=#AIRBOSS] Stop + -- @param #AIRBOSS self + + --- Triggers the FSM event "Stop" that stops the airboss after a delay. Event handlers are stopped. + -- @function [parent=#AIRBOSS] __Stop + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- USER API Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set carrier controlled area (CCA). +-- This is a large zone around the carrier, which is constantly updated wrt the carrier position. +-- @param #AIRBOSS self +-- @param #number radius Radius of zone in nautical miles (NM). Default 50 NM. +-- @return #AIRBOSS self +function AIRBOSS:SetCarrierControlledArea(radius) + + radius=UTILS.NMToMeters(radius or 50) + + self.zoneCCA=ZONE_UNIT:New("Carrier Controlled Area", self.carrier, radius) + + return self +end + +--- Set carrier controlled zone (CCZ). +-- This is a small zone (usually 5 NM radius) around the carrier, which is constantly updated wrt the carrier position. +-- @param #AIRBOSS self +-- @param #number radius Radius of zone in nautical miles (NM). Default 5 NM. +-- @return #AIRBOSS self +function AIRBOSS:SetCarrierControlledZone(radius) + + radius=UTILS.NMToMeters(radius or 5) + + self.zoneCCZ=ZONE_UNIT:New("Carrier Controlled Zone", self.carrier, radius) + + return self +end + +--- Set distance up to which water ahead is scanned for collisions. +-- @param #AIRBOSS self +-- @param #number dist Distance in NM. Default 5 NM. +-- @return #AIRBOSS self +function AIRBOSS:SetCollisionDistance(distance) + self.collisiondist=UTILS.NMToMeters(distance or 5) + return self +end + +--- Set the default recovery case. +-- @param #AIRBOSS self +-- @param #number case Case of recovery. Either 1, 2 or 3. Default 1. +-- @return #AIRBOSS self +function AIRBOSS:SetRecoveryCase(case) + + -- Set default case or 1. + self.defaultcase=case or 1 + + -- Current case init. + self.case=self.defaultcase + + return self +end + +--- Set holding pattern offset from final bearing for Case II/III recoveries. +-- Usually, this is +-15 or +-30 degrees. You should not use and offet angle >= 90 degrees, because this will cause a devision by zero in some of the equations used to calculate the approach corridor. +-- So best stick to the defaults up to 30 degrees. +-- @param #AIRBOSS self +-- @param #number offset Offset angle in degrees. Default 0. +-- @return #AIRBOSS self +function AIRBOSS:SetHoldingOffsetAngle(offset) + + -- Set default angle or 0. + self.defaultoffset=offset or 0 + + -- Current offset init. + self.holdingoffset=self.defaultoffset + + return self +end + +--- Add aircraft recovery time window and recovery case. +-- @param #AIRBOSS self +-- @param #string starttime Start time, e.g. "8:00" for eight o'clock. Default now. +-- @param #string stoptime Stop time, e.g. "9:00" for nine o'clock. Default 90 minutes after start time. +-- @param #number case Recovery case for that time slot. Number between one and three. +-- @param #number holdingoffset Only for CASE II/III: Angle in degrees the holding pattern is offset. +-- @param #boolean turnintowind If true, carrier will turn into the wind 5 minutes before the recovery window opens. +-- @param #number speed Speed in knots during turn into wind leg. +-- @return #AIRBOSS.Recovery Recovery window. +function AIRBOSS:AddRecoveryWindow(starttime, stoptime, case, holdingoffset, turnintowind, speed) + + -- Absolute mission time in seconds. + local Tnow=timer.getAbsTime() + + -- Input or now. + starttime=starttime or UTILS.SecondsToClock(Tnow) + + -- Set start time. + local Tstart=UTILS.ClockToSeconds(starttime) + + -- Set stop time. + local Tstop=UTILS.ClockToSeconds(stoptime or Tstart+90*60) + + -- Consistancy check for timing. + if Tstart>Tstop then + self:E(string.format("ERROR: Recovery stop time %s lies before recovery start time %s! Recovery window rejected.", UTILS.SecondsToClock(Tstart), UTILS.SecondsToClock(Tstop))) + return self + end + if Tstop<=Tnow then + self:I(string.format("WARNING: Recovery stop time %s already over. Tnow=%s! Recovery window rejected.", UTILS.SecondsToClock(Tstop), UTILS.SecondsToClock(Tnow))) + return self + end + + -- Case or default value. + case=case or self.defaultcase + + -- Holding offset or default value. + holdingoffset=holdingoffset or self.defaultoffset + + -- Offset zero for case I. + if case==1 then + holdingoffset=0 + end + + -- Increase counter. + self.windowcount=self.windowcount+1 + + -- Recovery window. + local recovery={} --#AIRBOSS.Recovery + recovery.START=Tstart + recovery.STOP=Tstop + recovery.CASE=case + recovery.OFFSET=holdingoffset + recovery.OPEN=false + recovery.OVER=false + recovery.WIND=turnintowind + recovery.SPEED=speed or 20 + recovery.ID=self.windowcount + + -- Add to table + table.insert(self.recoverytimes, recovery) + + return recovery +end + +--- Define a set of AI groups that are handled by the airboss. +-- @param #AIRBOSS self +-- @param Core.Set#SET_GROUP setgroup The set of AI groups which are handled by the airboss. +-- @return #AIRBOSS self +function AIRBOSS:SetSquadronAI(setgroup) + self.squadsetAI=setgroup + return self +end + +--- Close currently running recovery window and stop recovery ops. Recovery window is deleted. +-- @param #AIRBOSS self +-- @param #number delay (Optional) Delay in seconds before the window is deleted. +function AIRBOSS:CloseCurrentRecoveryWindow(delay) + + if delay and delay>0 then + SCHEDULER:New(nil, self.CloseCurrentRecoveryWindow, {self}, delay) + else + if self:IsRecovering() and self.recoverywindow and self.recoverywindow.OPEN then + self:RecoveryStop() + self.recoverywindow.OPEN=false + self.recoverywindow.OVER=true + self:DeleteRecoveryWindow(self.recoverywindow) + end + end +end + +--- Delete all recovery windows. +-- @param #AIRBOSS self +-- @param #number delay (Optional) Delay in seconds before the windows are deleted. +-- @return #AIRBOSS self +function AIRBOSS:DeleteAllRecoveryWindows(delay) + + -- Loop over all recovery windows. + for _,recovery in pairs(self.recoverytimes) do + self:DeleteRecoveryWindow(recovery, delay) + end + + return self +end + +--- Return the recovery window of the given ID. +-- @param #AIRBOSS self +-- @param #number id The ID of the recovery window. +-- @return #AIRBOSS.Recovery Recovery window with the right ID or nil if no such window exists. +function AIRBOSS:GetRecoveryWindowByID(id) + if id then + for _,_window in pairs(self.recoverytimes) do + local window=_window --#AIRBOSS.Recovery + if window.ID==id then + return window + end + end + end + return nil +end + +--- Delete a recovery window. If the window is currently open, it is closed and the recovery stopped. +-- @param #AIRBOSS self +-- @param #AIRBOSS.Recovery window Recovery window. +-- @param #number delay Delay in seconds, before the window is deleted. +function AIRBOSS:DeleteRecoveryWindow(window, delay) + + if delay and delay>0 then + -- Delayed call. + SCHEDULER:New(nil, self.DeleteRecoveryWindow, {self, window}, delay) + else + + for i,_recovery in pairs(self.recoverytimes) do + local recovery=_recovery --#AIRBOSS.Recovery + + if window and window.ID==recovery.ID then + if window.OPEN then + -- Window is currently open. + self:RecoveryStop() + else + table.remove(self.recoverytimes, i) + end + + end + end + end +end + +--- Set time interval for updating queues and other stuff. +-- @param #AIRBOSS self +-- @param #number interval Time interval in seconds. Default 30 sec. +-- @return #AIRBOSS self +function AIRBOSS:SetQueueUpdateTime(interval) + self.dTqueue=interval or 30 + return self +end + +--- Set time interval between LSO calls. Optimal time in the groove is ~16 seconds. So the default of 4 seconds gives around 3-4 correction calls in the groove. +-- @param #AIRBOSS self +-- @param #number interval Time interval in seconds between LSO calls. Default 4 sec. +-- @return #AIRBOSS self +function AIRBOSS:SetLSOCallInterval(timeinterval) + self.LSOdT=timeinterval or 4 + return self +end + +--- Airboss is a rather nice guy and not strictly following the rules. Fore example, he does allow you into the landing pattern if you are not coming from the Marshal stack. +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil, Airboss bends the rules a bit. +-- @return #AIRBOSS self +function AIRBOSS:SetAirbossNiceGuy(switch) + if switch==true or switch==nil then + self.airbossnice=true + else + self.airbossnice=false + end + return self +end + +--- Set folder where the airboss sound files are located **within you mission (miz) file**. +-- The default path is "l10n/DEFAULT/" but sound files simply copied there will be removed by DCS the next time you save the mission. +-- However, if you create a new folder inside the miz file, which contains the sounds, it will not be deleted and can be used. +-- @param #AIRBOSS self +-- @param #string folderpath The path to the sound files, e.g. "Airboss Soundfiles/". +-- @return #AIRBOSS self +function AIRBOSS:SetSoundfilesFolder(folderpath) + + -- Check that it ends with / + if folderpath then + local lastchar=string.sub(folderpath, -1) + if lastchar~="/" then + folderpath=folderpath.."/" + end + end + + -- Folderpath. + self.soundfolder=folderpath + + -- Info message. + self:I(self.lid..string.format("Setting sound files folder to: %s", self.soundfolder)) + + return self +end + +--- Set time interval for updating player status and other things. +-- @param #AIRBOSS self +-- @param #number interval Time interval in seconds. Default 0.5 sec. +-- @return #AIRBOSS self +function AIRBOSS:SetStatusUpdateTime(interval) + self.dTstatus=interval or 0.5 + return self +end + +--- Set duration how long messages are displayed to players. +-- @param #AIRBOSS self +-- @param #number duration Duration in seconds. Default 10 sec. +-- @return #AIRBOSS self +function AIRBOSS:SetDefaultMessageDuration(duration) + self.Tmessage=duration or 10 + return self +end + +--- Set Case I Marshal radius. This is the radius of the valid zone around "the post" aircraft are supposed to be holding in the Case I Marshal stack. +-- The post is 2.5 NM port of the carrier. +-- @param #AIRBOSS self +-- @param #number Radius in NM. Default 2.8 NM, which gives a diameter of 5.6 NM. +-- @return #AIRBOSS self +function AIRBOSS:SetMarshalRadius(radius) + self.marshalradius=UTILS.NMToMeters(radius or 2.8) + return self +end + +--- Optimized F10 radio menu for a single carrier. The menu entries will be stored directly under F10 Other/Airboss/ and not F10 Other/Airboss/"Carrier Alias"/. +-- **WARNING**: If you use this with two airboss objects/carriers, the radio menu will be screwed up! +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil single menu is enabled. If false, menu is for multiple carriers in the mission. +-- @return #AIRBOSS self +function AIRBOSS:SetMenuSingleCarrier(switch) + if switch==true or switch==nil then + self.menusingle=true + else + self.menusingle=false + end + return self +end + +--- Enable or disable F10 radio menu for marking zones via smoke or flares. +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil, menu is enabled. If false, menu is not available to players. +-- @return #AIRBOSS self +function AIRBOSS:SetMenuMarkZones(switch) + if switch==nil or switch==true then + self.menumarkzones=true + else + self.menumarkzones=false + end + return self +end + +--- Enable or disable F10 radio menu for marking zones via smoke. +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil, menu is enabled. If false, menu is not available to players. +-- @return #AIRBOSS self +function AIRBOSS:SetMenuSmokeZones(switch) + if switch==nil or switch==true then + self.menusmokezones=true + else + self.menusmokezones=false + end + return self +end + +--- Specify weather the mission has set static or dynamic weather. +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil, mission uses static weather. If false, dynamic weather is used in this mission. +-- @return #AIRBOSS self +function AIRBOSS:SetStaticWeather(switch) + if switch==nil or switch==true then + self.staticweather=true + else + self.staticweather=false + end + return self +end + + +--- Disable automatic TACAN activation +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetTACANoff() + self.TACANon=false + return self +end + +--- Set TACAN channel of carrier. +-- @param #AIRBOSS self +-- @param #number channel TACAN channel. Default 74. +-- @param #string mode TACAN mode, i.e. "X" or "Y". Default "X". +-- @param #string morsecode Morse code identifier. Three letters, e.g. "STN". +-- @return #AIRBOSS self +function AIRBOSS:SetTACAN(channel, mode, morsecode) + + self.TACANchannel=channel or 74 + self.TACANmode=mode or "X" + self.TACANmorse=morsecode or "STN" + self.TACANon=true + + return self +end + +--- Disable automatic ICLS activation. +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetICLSoff() + self.ICLSon=false + return self +end + +--- Set ICLS channel of carrier. +-- @param #AIRBOSS self +-- @param #number channel ICLS channel. Default 1. +-- @param #string morsecode Morse code identifier. Three letters, e.g. "STN". Default "STN". +-- @return #AIRBOSS self +function AIRBOSS:SetICLS(channel, morsecode) + + self.ICLSchannel=channel or 1 + self.ICLSmorse=morsecode or "STN" + self.ICLSon=true + + return self +end + + +--- Set LSO radio frequency and modulation. Default frequency is 264 MHz AM. +-- @param #AIRBOSS self +-- @param #number frequency Frequency in MHz. Default 264 MHz. +-- @param #string modulation Modulation, i.e. "AM" (default) or "FM". +-- @return #AIRBOSS self +function AIRBOSS:SetLSORadio(frequency, modulation) + + self.LSOFreq=(frequency or 264) + modulation=modulation or "AM" + + if modulation=="FM" then + self.LSOModu=radio.modulation.FM + else + self.LSOModu=radio.modulation.AM + end + + self.LSORadio={} --#AIRBOSS.Radio + self.LSORadio.frequency=self.LSOFreq + self.LSORadio.modulation=self.LSOModu + self.LSORadio.alias="LSO" + + return self +end + +--- Set carrier radio frequency and modulation. Default frequency is 305 MHz AM. +-- @param #AIRBOSS self +-- @param #number frequency Frequency in MHz. Default 305 MHz. +-- @param #string modulation Modulation, i.e. "AM" (default) or "FM". +-- @return #AIRBOSS self +function AIRBOSS:SetMarshalRadio(frequency, modulation) + + self.MarshalFreq=frequency or 305 + modulation=modulation or "AM" + + if modulation=="FM" then + self.MarshalModu=radio.modulation.FM + else + self.MarshalModu=radio.modulation.AM + end + + self.MarshalRadio={} --#AIRBOSS.Radio + self.MarshalRadio.frequency=self.MarshalFreq + self.MarshalRadio.modulation=self.MarshalModu + self.MarshalRadio.alias="MARSHAL" + + return self +end + +--- Set unit name for sending radio messages. +-- @param #AIRBOSS self +-- @param #string unitname Name of the unit. +-- @return #AIRBOSS self +function AIRBOSS:SetRadioUnitName(unitname) + self.senderac=unitname + return self +end + +--- Use user sound output instead of radio transmission for messages. Might be handy if radio transmissions are broken. +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetUserSoundRadio() + self.usersoundradio=true + return self +end + +--- Set number of aircraft units, which can be in the landing pattern before the pattern is full. +-- @param #AIRBOSS self +-- @param #number nmax Max number. Default 4. Minimum is 1, maximum is 6. +-- @return #AIRBOSS self +function AIRBOSS:SetMaxLandingPattern(nmax) + nmax=nmax or 4 + nmax=math.max(nmax,1) + nmax=math.min(nmax,6) + self.Nmaxpattern=nmax + return self +end + +--- Set number available Case I Marshal stacks. If Marshal stacks are full, flights requesting Marshal will be told to hold outside 10 NM zone until a stack becomes available again. +-- Marshal stacks for Case II/III are unlimited. +-- @param #AIRBOSS self +-- @param #number nmax Max number of stacks available to players and AI flights. Default 3, i.e. angels 2, 3, 4. Minimum is 1. +-- @return #AIRBOSS self +function AIRBOSS:SetMaxMarshalStacks(nmax) + self.Nmaxmarshal=nmax or 3 + self.Nmaxmarshal=math.max(self.Nmaxmarshal, 1) + return self +end + +--- Set max number of section members. Minimum is one, i.e. the section lead itself. Maximum number is four. Default is two, i.e. the lead and one other human flight. +-- @param #AIRBOSS self +-- @param #number nmax Number of max allowed members including the lead itself. For example, Nmax=2 means a section lead plus one member. +-- @return #AIRBOSS self +function AIRBOSS:SetMaxSectionSize(nmax) + nmax=nmax or 2 + nmax=math.max(nmax,1) + nmax=math.min(nmax,4) + self.NmaxSection=nmax-1 -- We substract one because internally the section lead is not counted! + return self +end + +--- Set max number of flights per stack. All members of a section count as one "flight". +-- @param #AIRBOSS self +-- @param #number nmax Number of max allowed flights per stack. Default is two. Minimum is one, maximum is 4. +-- @return #AIRBOSS self +function AIRBOSS:SetMaxFlightsPerStack(nmax) + nmax=nmax or 2 + nmax=math.max(nmax,1) + nmax=math.min(nmax,4) + self.NmaxStack=nmax + return self +end + + +--- Handle AI aircraft. +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetHandleAION() + self.handleai=true + return self +end + +--- Do not handle AI aircraft. +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetHandleAIOFF() + self.handleai=false + return self +end + + +--- Define recovery tanker associated with the carrier. +-- @param #AIRBOSS self +-- @param Ops.RecoveryTanker#RECOVERYTANKER recoverytanker Recovery tanker object. +-- @return #AIRBOSS self +function AIRBOSS:SetRecoveryTanker(recoverytanker) + self.tanker=recoverytanker + return self +end + +--- Define warehouse associated with the carrier. +-- @param #AIRBOSS self +-- @param Functional.Warehouse#WAREHOUSE warehouse Warehouse object of the carrier. +-- @return #AIRBOSS self +function AIRBOSS:SetWarehouse(warehouse) + self.warehouse=warehouse + return self +end + +--- Set default player skill. New players will be initialized with this skill. +-- +-- * "Flight Student" = @{#AIRBOSS.Difficulty.Easy} +-- * "Naval Aviator" = @{#AIRBOSS.Difficulty.Normal} +-- * "TOPGUN Graduate" = @{#AIRBOSS.Difficulty.Hard} +-- @param #AIRBOSS self +-- @param #string skill Player skill. Default "Naval Aviator". +-- @return #AIRBOSS self +function AIRBOSS:SetDefaultPlayerSkill(skill) + + -- Set skill or normal. + self.defaultskill=skill or AIRBOSS.Difficulty.NORMAL + + -- Check that defualt skill is valid. + local gotit=false + for _,_skill in pairs(AIRBOSS.Difficulty) do + if _skill==self.defaultskill then + gotit=true + end + end + + -- If invalid user input, fall back to normal. + if not gotit then + self.defaultskill=AIRBOSS.Difficulty.NORMAL + self:E(self.lid..string.format("ERROR: Invalid default skill = %s. Resetting to Naval Aviator.", tostring(skill))) + end + + return self +end + +--- Enable auto save of player results each time a player is *finally* graded. *Finally* means after the player landed on the carrier! After intermediate passes (bolter or waveoff) the stats are *not* saved. +-- @param #AIRBOSS self +-- @param #string path Path where to save the asset data file. Default is the DCS root installation directory or your "Saved Games\\DCS" folder if lfs was desanitized. +-- @param #string filename File name. Default is generated automatically from airboss carrier name/alias. +-- @return #AIRBOSS self +function AIRBOSS:SetAutoSave(path, filename) + self.autosave=true + self.autosavepath=path + self.autosavefile=filename + return self +end + +--- Activate debug mode. Display debug messages on screen. +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetDebugModeON() + self.Debug=true + return self +end + +--- Carrier patrols ad inifintum. If the last waypoint is reached, it will go to waypoint one and repeat its route. +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil, patrol until the end of time. If false, go along the waypoints once and stop. +-- @return #AIRBOSS self +function AIRBOSS:SetPatrolAdInfinitum(switch) + if switch==false then + self.adinfinitum=false + else + self.adinfinitum=true + end + return self +end + +--- Set the magnetic declination (or variation). By default this is set to the standard declination of the map. +-- @param #AIRBOSS self +-- @param #number declination Declination in degrees or nil for default declination of the map. +-- @return #AIRBOSS self +function AIRBOSS:SetMagneticDeclination(declination) + self.magvar=declination or UTILS.GetMagneticDeclination() + return self +end + +--- Deactivate debug mode. This is also the default setting. +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetDebugModeOFF() + self.Debug=false + return self +end + +--- Check if carrier is recovering aircraft. +-- @param #AIRBOSS self +-- @return #boolean If true, time slot for recovery is open. +function AIRBOSS:IsRecovering() + return self:is("Recovering") +end + +--- Check if carrier is idle, i.e. no operations are carried out. +-- @param #AIRBOSS self +-- @return #boolean If true, carrier is in idle state. +function AIRBOSS:IsIdle() + return self:is("Idle") +end + +--- Check if recovery of aircraft is paused. +-- @param #AIRBOSS self +-- @return #boolean If true, recovery is paused +function AIRBOSS:IsPaused() + return self:is("Paused") +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM event functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the warehouse. Addes event handlers and schedules status updates of reqests and queue. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AIRBOSS:onafterStart(From, Event, To) + + -- Events are handled my MOOSE. + self:I(self.lid..string.format("Starting AIRBOSS v%s for carrier unit %s of type %s.", AIRBOSS.version, self.carrier:GetName(), self.carriertype)) + + -- Current map. + local theatre=env.mission.theatre + self:T2(self.lid..string.format("Theatre = %s", tostring(theatre))) + + -- Activate TACAN. + if self.TACANon then + self.beacon:ActivateTACAN(self.TACANchannel, self.TACANmode, self.TACANmorse, true) + end + + -- Activate ICLS. + if self.ICLSon then + self.beacon:ActivateICLS(self.ICLSchannel, self.ICLSmorse) + end + + -- Schedule radio queue checks. + self.RQLid=self.radiotimer:Schedule(self, self._CheckRadioQueue, {self.RQLSO, "LSO"}, 1, 0.01) + self.RQMid=self.radiotimer:Schedule(self, self._CheckRadioQueue, {self.RQMarshal, "MARSHAL"}, 1, 0.01) + + -- Initial carrier position and orientation. + self.Cposition=self:GetCoordinate() + self.Corientation=self.carrier:GetOrientationX() + self.Corientlast=self.Corientation + self.Tpupdate=timer.getTime() + + -- Init patrol route of carrier. + self:_PatrolRoute() + + -- Check if no recovery window is set. + if #self.recoverytimes==0 then + + -- Open window in 15 minutes for 3 hours. + local Topen=timer.getAbsTime()+15*60 + local Tclose=Topen+3*60*60 + + -- Add window. + self:AddRecoveryWindow(UTILS.SecondsToClock(Topen), UTILS.SecondsToClock(Tclose)) + end + + -- Check Recovery time.s + self:_CheckRecoveryTimes() + + -- Time stamp for checking queues. We substract 60 seconds so the routine is called right after status is called the first time. + self.Tqueue=timer.getTime()-60 + + -- Handle events. + self:HandleEvent(EVENTS.Birth) + self:HandleEvent(EVENTS.Land) + self:HandleEvent(EVENTS.Crash) + self:HandleEvent(EVENTS.Ejection) + self:HandleEvent(EVENTS.PlayerLeaveUnit, self._PlayerLeft) + --self:HandleEvent(EVENTS.MissionEnd) + + -- Start status check in 1 second. + self:__Status(1) +end + +--- On after Status event. Checks for new flights, updates queue and checks player status. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AIRBOSS:onafterStatus(From, Event, To) + + -- Get current time. + local time=timer.getTime() + + -- Update marshal and pattern queue every 30 seconds. + if time-self.Tqueue>self.dTqueue then + + -- Get time. + local clock=UTILS.SecondsToClock(timer.getAbsTime()) + local eta=UTILS.SecondsToClock(self:_GetETAatNextWP()) + + -- Current heading and position of the carrier. + local hdg=self:GetHeading() + local pos=self:GetCoordinate() + + -- Check water is ahead. + local collision=self:_CheckCollisionCoord(pos:Translate(self.collisiondist, hdg)) + + -- Debug info. + local text=string.format("Time %s - Status %s (case=%d) - Speed=%.1f kts - Heading=%d - WP=%d - ETA=%s - Collision Warning=%s", + clock, self:GetState(), self.case, self.carrier:GetVelocityKNOTS(), hdg, self.currentwp, eta, tostring(collision)) + self:T(self.lid..text) + + -- Check for collision. + if collision then + + -- We are currently turning into the wind. + if self.turnintowind then + + -- Carrier resumes its initial route. This disables turnintowind switch. + self:CarrierResumeRoute(self.Creturnto) + + -- Since current window would stay open, we disable the WIND switch. + if self:IsRecovering() and self.recoverywindow and self.recoverywindow.WIND then + -- Disable turn into the wind for this window so that we do not do this all over again. + self.recoverywindow.WIND=false + end + + else + + -- Find path around the obstacle. + if not self.detour then + --self:_Pathfinder() + end + + end + end + + + -- Check recovery times and start/stop recovery mode if necessary. + self:_CheckRecoveryTimes() + + -- Remove dead/zombie flight groups. Player leaving the server whilst in pattern etc. + --self:_RemoveDeadFlightGroups() + + -- Scan carrier zone for new aircraft. + self:_ScanCarrierZone() + + -- Check marshal and pattern queues. + self:_CheckQueue() + + -- Check if marshal pattern of AI needs an update. + self:_CheckPatternUpdate() + + -- Time stamp. + self.Tqueue=time + end + + -- Check player status. + self:_CheckPlayerStatus() + + -- Check AI landing pattern status + self:_CheckAIStatus() + + -- Call status every ~0.5 seconds. + self:__Status(-self.dTstatus) +end + +--- Check AI status. Pattern queue AI in the groove? Marshal queue AI arrived in holding zone? +-- @param #AIRBOSS self +function AIRBOSS:_CheckAIStatus() + + -- Loop over all flights in landing pattern. + for _,_flight in pairs(self.Qpattern) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Only AI! + if flight.ai then + + -- Loop over all units in AI flight. + for _,_element in pairs(flight.elements) do + local element=_element --#AIRBOSS.FlightElement + + -- Unit + local unit=element.unit + + -- Get lineup and distance to carrier. + local lineup=self:_Lineup(unit, true) + + -- Distance in NM. + local distance=UTILS.MetersToNM(unit:GetCoordinate():Get2DDistance(self:GetCoordinate())) + + -- Altitude in ft. + local alt=UTILS.MetersToFeet(unit:GetAltitude()) + + -- Check if parameters are right and flight is in the groove. + if lineup<2 and distance<=0.75 and alt<500 and not element.ballcall then + + -- Paddles: Call the ball! + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.CALLTHEBALL, false, 0) + + -- Pilot: "405, Hornet Ball, 3.2" + -- TODO: Voice over. + local text=string.format("%s Ball, %.1f.", self:_GetACNickname(unit:GetTypeName()), self:_GetFuelState(unit)/1000) + self:MessageToPattern(text, element.onboard, "", 5) + + -- Debug message. + MESSAGE:New(string.format("%s, %s", element.onboard, text), 15, "DEBUG"):ToAllIf(self.Debug) + + -- Paddles: Roger ball after 3 seconds. + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.ROGERBALL, false, 6) + + -- Flight element called the ball. + element.ballcall=true + + -- This is for the whole flight. Maybe we need it. + flight.ballcall=true + end + + end + end + end + +end + +--- Check if player in the landing pattern is too close to another aircarft in the pattern. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData player Player data. +function AIRBOSS:_CheckPlayerPatternDistance(player) + + -- Nothing to do since we check only in the pattern. + if #self.Qpattern==0 then + return + end + + --- Function that checks if unit1 is too close to unit2. + local function _checkclose(_unit1, _unit2) + + local unit1=_unit1 --Wrapper.Unit#UNIT + local unit2=_unit2 --Wrapper.Unit#UNIT + + if (not unit1) or (not unit2) then + return false + end + + -- Check that this is not the same unit. + if unit1:GetName()==unit2:GetName() then + return false + end + + -- Return false when unit2 is not in air? Could be on the carrier. + if not unit2:InAir() then + return false + end + + -- Positions of units. + local c1=unit1:GetCoordinate() + local c2=unit2:GetCoordinate() + + -- Vector from unit1 to unit2 + local vec12={x=c2.x-c1.x, y=0, z=c2.z-c1.z} --DCS#Vec3 + + -- Distance between units. + local dist=UTILS.VecNorm(vec12) + + -- Orientation of unit 1 in space. + local vec1=unit1:GetOrientationX() + vec1.y=0 + + -- Get angle between the two orientation vectors. Does the player aircraft nose point into the direction of the other aircraft? (Could be behind him!) + local rhdg=math.deg(math.acos(UTILS.VecDot(vec12,vec1)/UTILS.VecNorm(vec12)/UTILS.VecNorm(vec1))) + + -- Check altitude difference? + local dalt=math.abs(c2.y-c1.y) + + -- 650 feet ~= 200 meters distance between flights + local dcrit=UTILS.FeetToMeters(650) + + -- Direction in 30 degrees cone and distance < 200 meters and altitude difference <50 + -- TODO: Test parameter values. + if math.abs(rhdg)<10 and dist=recovery.START then + -- Start time has passed. + + if time %d", self.case, Case) + if Case>1 then + text=text..string.format(" Holding offset angle %d degrees.", Offset) + end + MESSAGE:New(text, 20, self.alias):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- Set new recovery case. + self.case=Case + + -- Set holding offset. + self.holdingoffset=Offset + + -- Update case of all flights not in Marshal or Pattern queue. + for _,_flight in pairs(self.flights) do + local flight=_flight --#AIRBOSS.FlightGroup + if not (self:_InQueue(self.Qmarshal, flight.group) or self:_InQueue(self.Qpattern, flight.group)) then + + -- Also not for section members. These are not in the marshal or pattern queue if the lead is. + if flight.name~=flight.seclead then + local lead=self.players[flight.seclead] + + if lead and not (self:_InQueue(self.Qmarshal, lead.group) or self:_InQueue(self.Qpattern, lead.group)) then + -- This is section member and the lead is not in the Marshal or Pattern queue. + flight.case=self.case + end + + else + + -- This is a flight without section or the section lead. + flight.case=self.case + + end + + end + end +end + +--- On after "RecoveryStart" event. Recovery of aircraft is started and carrier switches to state "Recovering". +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number Case The recovery case (1, 2 or 3) to start. +-- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. +function AIRBOSS:onafterRecoveryStart(From, Event, To, Case, Offset) + + -- Input or default value. + Case=Case or self.defaultcase + + -- Input or default value. + Offset=Offset or self.defaultoffset + + -- Debug output. + local text=string.format("Starting aircraft recovery Case %d ops.", Case) + if Case>1 then + local radial=self:GetRadial(Case, true, true, true) + text=text..string.format(" Marshal radial %03d°.", radial) + end + MESSAGE:New(text, 20, self.alias):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- Message to all players inside CCA. + self:MessageToMarshal(text, "AIRBOSS", "99") + + -- Switch to case. + self:RecoveryCase(Case, Offset) +end + +--- On after "RecoveryStop" event. Recovery of aircraft is stopped and carrier switches to state "Idle". Running recovery window is deleted. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AIRBOSS:onafterRecoveryStop(From, Event, To) + -- Debug output. + self:T(self.lid..string.format("Stopping aircraft recovery.")) + + -- Delete current recovery window if open. + if self.recoverywindow and self.recoverywindow.OPEN==true then + self.recoverywindow.OPEN=false + self.recoverywindow.OVER=true + self:DeleteRecoveryWindow(self.recoverywindow) + end + + -- Message text. + local text=string.format("Case %d recovery ops are stopped.", self.case) + + -- Message to Marshal. + self:MessageToMarshal(text, "AIRBOSS", "99") + + -- If carrier is currently heading into the wind, we resume the original route. + if self.turnintowind then + self:CarrierResumeRoute(self.Creturnto) + end + + -- Check recovery windows. This sets self.recoverywindow to the next window. + self:_CheckRecoveryTimes() +end + + +--- On after "RecoveryPause" event. Recovery of aircraft is paused. Marshal queue stays intact. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number duration Duration of pause in seconds. After that recovery is resumed automatically. +function AIRBOSS:onafterRecoveryPause(From, Event, To, duration) + -- Debug output. + self:T(self.lid..string.format("Pausing aircraft recovery.")) + + -- Message text + local text=string.format("aircraft recovery is paused until further notice.") + if duration then + + -- Auto resume. + self:__RecoveryUnpause(duration) + + -- Message text. + local clock=UTILS.SecondsToClock(timer.getAbsTime()+duration) + text=string.format("aircraft recovery is paused and will be resumed at %s.", clock) + end + + -- Message to Marshal. + self:MessageToMarshal(text, "AIRBOSS", "99") +end + +--- On after "RecoveryUnpause" event. Recovery of aircraft is resumed. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AIRBOSS:onafterRecoveryUnpause(From, Event, To) + -- Debug output. + self:T(self.lid..string.format("Unpausing aircraft recovery.")) + + -- Message text. + local text=string.format("resuming aircraft recovery.") + + -- Message to Marshal. + self:MessageToMarshal(text, "AIRBOSS", "99") +end + + +--- On after "Idle" event. Carrier goes to state "Idle". +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AIRBOSS:onafterIdle(From, Event, To) + -- Debug output. + self:T(self.lid..string.format("Carrier goes to idle.")) +end + +--- On after Stop event. Unhandle events. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AIRBOSS:onafterStop(From, Event, To) + self:UnHandleEvent(EVENTS.Birth) + self:UnHandleEvent(EVENTS.Land) + self:UnHandleEvent(EVENTS.Crash) + self:UnHandleEvent(EVENTS.Ejection) +end + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Parameter initialization +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Init parameters for USS Stennis carrier. +-- @param #AIRBOSS self +function AIRBOSS:_InitStennis() + + -- Carrier Parameters. + self.carrierparam.sterndist =-153 + self.carrierparam.deckheight = 19 + + -- Total size of the carrier (approx as rectangle). + self.carrierparam.totlength=310 -- Wiki says 332.8 meters overall length. + self.carrierparam.totwidthport=40 -- Wiki says 76.8 meters overall beam. + self.carrierparam.totwidthstarboard=30 + + -- Landing runway. + self.carrierparam.rwyangle = -9 + self.carrierparam.rwylength = 225 + self.carrierparam.rwywidth = 20 + + -- Wires. + self.carrierparam.wire1 = 46 -- Distance from stern to first wire. + self.carrierparam.wire2 = 46+12 + self.carrierparam.wire3 = 46+24 + self.carrierparam.wire4 = 46+35 -- Last wire is strangely one meter closer. + + + -- Platform at 5k. Reduce descent rate to 2000 ft/min to 1200 dirty up level flight. + self.Platform.name="Platform 5k" + self.Platform.Xmin=-UTILS.NMToMeters(22) -- Not more than 22 NM behind the boat. Last check was at 21 NM. + self.Platform.Xmax =nil + self.Platform.Zmin=-UTILS.NMToMeters(30) -- Not more than 30 NM port of boat. + self.Platform.Zmax= UTILS.NMToMeters(30) -- Not more than 30 NM starboard of boat. + self.Platform.LimitXmin=nil -- Limits via zone + self.Platform.LimitXmax=nil + self.Platform.LimitZmin=nil + self.Platform.LimitZmax=nil + + -- Level out at 1200 ft and dirty up. + self.DirtyUp.name="Dirty Up" + self.DirtyUp.Xmin=-UTILS.NMToMeters(21) -- Not more than 21 NM behind the boat. + self.DirtyUp.Xmax= nil + self.DirtyUp.Zmin=-UTILS.NMToMeters(30) -- Not more than 30 NM port of boat. + self.DirtyUp.Zmax= UTILS.NMToMeters(30) -- Not more than 30 NM starboard of boat. + self.DirtyUp.LimitXmin=nil -- Limits via zone + self.DirtyUp.LimitXmax=nil + self.DirtyUp.LimitZmin=nil + self.DirtyUp.LimitZmax=nil + + -- Intercept glide slope and follow bullseye. + self.Bullseye.name="Bullseye" + self.Bullseye.Xmin=-UTILS.NMToMeters(11) -- Not more than 11 NM behind the boat. Last check was at 10 NM. + self.Bullseye.Xmax= nil + self.Bullseye.Zmin=-UTILS.NMToMeters(30) -- Not more than 30 NM port. + self.Bullseye.Zmax= UTILS.NMToMeters(30) -- Not more than 30 NM starboard. + self.Bullseye.LimitXmin=nil -- Limits via zone. + self.Bullseye.LimitXmax=nil + self.Bullseye.LimitZmin=nil + self.Bullseye.LimitZmax=nil + + -- Break entry. + self.BreakEntry.name="Break Entry" + self.BreakEntry.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. Check for initial is at 3 NM with a radius of 500 m and 100 m starboard. + self.BreakEntry.Xmax= nil + self.BreakEntry.Zmin=-UTILS.NMToMeters(0.5) -- Not more than 0.5 NM port of boat. + self.BreakEntry.Zmax= UTILS.NMToMeters(1.5) -- Not more than 1.5 NM starboard. + self.BreakEntry.LimitXmin=0 -- Check and next step when at carrier and starboard of carrier. + self.BreakEntry.LimitXmax=nil + self.BreakEntry.LimitZmin=nil + self.BreakEntry.LimitZmax=nil + + -- Early break. + self.BreakEarly.name="Early Break" + self.BreakEarly.Xmin=-UTILS.NMToMeters(1) -- Not more than 1 NM behind the boat. Last check was at 0. + self.BreakEarly.Xmax= UTILS.NMToMeters(5) -- Not more than 5 NM in front of the boat. Enough for late breaks? + self.BreakEarly.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port. + self.BreakEarly.Zmax= UTILS.NMToMeters(1) -- Not more than 1 NM starboard. + self.BreakEarly.LimitXmin= 0 -- Check and next step 0.2 NM port and in front of boat. + self.BreakEarly.LimitXmax= nil + self.BreakEarly.LimitZmin=-UTILS.NMToMeters(0.2) -- -370 m port + self.BreakEarly.LimitZmax= nil + + -- Late break. + self.BreakLate.name="Late Break" + self.BreakLate.Xmin=-UTILS.NMToMeters(1) -- Not more than 1 NM behind the boat. Last check was at 0. + self.BreakLate.Xmax= UTILS.NMToMeters(5) -- Not more than 5 NM in front of the boat. Enough for late breaks? + self.BreakLate.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port. + self.BreakLate.Zmax= UTILS.NMToMeters(1) -- Not more than 1 NM starboard. + self.BreakLate.LimitXmin= 0 -- Check and next step 0.8 NM port and in front of boat. + self.BreakLate.LimitXmax= nil + self.BreakLate.LimitZmin=-UTILS.NMToMeters(0.8) -- -1470 m port + self.BreakLate.LimitZmax= nil + + -- Abeam position. + self.Abeam.name="Abeam Position" + self.Abeam.Xmin=-UTILS.NMToMeters(5) -- Not more then 5 NM astern of boat. Should be LIG call anyway. + self.Abeam.Xmax= UTILS.NMToMeters(5) -- Not more then 5 NM ahead of boat. + self.Abeam.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port. + self.Abeam.Zmax= 500 -- Not more than 500 m starboard. Must be port! + self.Abeam.LimitXmin=-200 -- Check and next step 200 meters behind the ship. + self.Abeam.LimitXmax= nil + self.Abeam.LimitZmin= nil + self.Abeam.LimitZmax= nil + + -- At the Ninety. + self.Ninety.name="Ninety" + self.Ninety.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. LIG check anyway. + self.Ninety.Xmax= 0 -- Must be behind the boat. + self.Ninety.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port of boat. + self.Ninety.Zmax= nil + self.Ninety.LimitXmin=nil + self.Ninety.LimitXmax=nil + self.Ninety.LimitZmin=nil + self.Ninety.LimitZmax=-UTILS.NMToMeters(0.6) -- Check and next step when 0.6 NM port. + + -- At the Wake. + self.Wake.name="Wake" + self.Wake.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. + self.Wake.Xmax= 0 -- Must be behind the boat. + self.Wake.Zmin=-2000 -- Not more than 2 km port of boat. + self.Wake.Zmax= nil + self.Wake.LimitXmin=nil + self.Wake.LimitXmax=nil + self.Wake.LimitZmin=0 -- Check and next step when directly behind the boat. + self.Wake.LimitZmax=nil + + -- Turn to final. + self.Final.name="Final" + self.Final.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. + self.Final.Xmax= 0 -- Must be behind the boat. + self.Final.Zmin=-1000 -- Not more than 1 km port. + self.Final.Zmax= nil + self.Final.LimitXmin=nil -- No limits. Check is carried out differently. + self.Final.LimitXmax=nil + self.Final.LimitZmin=nil + self.Final.LimitZmax=nil + + -- In the Groove. + self.Groove.name="Groove" + self.Groove.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. + self.Groove.Xmax= nil + self.Groove.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port + self.Groove.Zmax= UTILS.NMToMeters(2) -- Not more than 2 NM starboard. + self.Groove.LimitXmin=nil -- No limits. Check is carried out differently. + self.Groove.LimitXmax=nil + self.Groove.LimitZmin=nil + self.Groove.LimitZmax=nil + +end + +--- Get optimal aircraft AoA parameters.. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @return #AIRBOSS.AircraftAoA AoA parameters for the given aircraft type. +function AIRBOSS:_GetAircraftAoA(playerData) + + -- Get AC type. + local hornet=playerData.actype==AIRBOSS.AircraftCarrier.HORNET + local skyhawk=playerData.actype==AIRBOSS.AircraftCarrier.A4EC + local harrier=playerData.actype==AIRBOSS.AircraftCarrier.AV8B + local tomcat=playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B + + -- Table with AoA values. + local aoa={} -- #AIRBOSS.AircraftAoA + + if hornet then + -- F/A-18C Hornet parameters. + aoa.SLOW=9.8 + aoa.Slow=9.3 + aoa.OnSpeedMax=8.8 + aoa.OnSpeed=8.1 + aoa.OnSpeedMin=7.4 + aoa.Fast=6.9 + aoa.FAST=6.3 + elseif tomcat then + -- F-14A/B Tomcat parameters (taken from NATOPS). Converted from units 0-30 to degrees. + -- Currently assuming a linear relationship with 0=-10 degrees and 30=+40 degrees as stated in NATOPS. + aoa.SLOW=18.33 --17.0 units + aoa.Slow=16.67 --16.0 units + aoa.OnSpeedMax=15.83 --15.5 units + aoa.OnSpeed=15.0 --15.0 units + aoa.OnSpeedMin=14.17 --14.5 units + aoa.Fast=13.33 --14.0 units + aoa.FAST=11.67 --13.0 units + elseif skyhawk then + -- A-4E-C Skyhawk parameters from https://forums.eagle.ru/showpost.php?p=3703467&postcount=390 + -- Note that these are arbitrary UNITS and not degrees. We need a conversion formula! + --[[ + aoa.SLOW=self:_AoAUnit2Deg(playerData, 19.0) + aoa.Slow=self:_AoAUnit2Deg(playerData, 18.5) + aoa.OnSpeedMax=self:_AoAUnit2Deg(playerData, 18.0) + aoa.OnSpeed=self:_AoAUnit2Deg(playerData, 17.5) + aoa.OnSpeedMin=self:_AoAUnit2Deg(playerData, 17.0) + aoa.Fast=self:_AoAUnit2Deg(playerData, 16.5) + aoa.FAST=self:_AoAUnit2Deg(playerData, 16.0) + ]] + aoa.SLOW=9.8 + aoa.Slow=9.3 + aoa.OnSpeedMax=8.8 + aoa.OnSpeed=8.1 + aoa.OnSpeedMin=7.4 + aoa.Fast=6.9 + aoa.FAST=6.3 + elseif harrier then + -- AV-8B Harrier parameters. This might need further tuning. + aoa.SLOW=14.0 + aoa.Slow=13.0 + aoa.OnSpeedMax=12.0 + aoa.OnSpeed=11.0 + aoa.OnSpeedMin=10.0 + aoa.Fast=9.0 + aoa.FAST=8.0 + end + + return aoa +end + +--- Convert AoA from arbitrary units to degrees. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #number aoaunits AoA in arbitrary units. +-- @return #number AoA in degrees. +function AIRBOSS:_AoAUnit2Deg(playerData, aoaunits) + + -- Init. + local degrees=aoaunits + + -- Check aircraft type of player. + if playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then + -- F-14A/B + + -- NATOPS: + -- unit=0 ==> alpha=-10 degrees. + -- unit=30 ==> alpha=+40 degrees. + + -- Assuming a linear relationship between these to points of the graph. + degrees=-10+50/30*aoaunits + + elseif playerData.actype==AIRBOSS.AircraftCarrier.A4EC then + -- A-4E + + -- Assuming same conversion as for the Tomcat. Indexer also goes from 0-30 units. Maybe it is right, maybe not. + local a=1.9/5.5 + local a=1/3 + local b=8.0-17.5*a + degrees=a*aoaunits+b + + end + + return degrees +end + +--- Convert AoA from degrees to arbitrary units. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #number degrees AoA in degrees. +-- @return #number AoA in arbitrary units. +function AIRBOSS:_AoADeg2Units(playerData, degrees) + + -- Init. + local aoaunits=degrees + + -- Check aircraft type of player. + if playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then + -- F-14A/B + + -- NATOPS: + -- unit=0 ==> alpha=-10 degrees. + -- unit=30 ==> alpha=+40 degrees. + + -- Assuming a linear relationship between these to points of the graph. + aoaunits=(degrees+10)*30/50 + +--[[ + elseif playerData.actype==AIRBOSS.AircraftCarrier.A4EC then + -- A-4E + + -- Assuming same conversion as for the Tomcat. Indexer also goes from 0-30 units. Maybe it is right, maybe not. + aoaunits=30/50*degrees+10 + --aoaunits=2*degrees + aoaunits=(degrees-1.406)*5.5/1.9 + + local a=1.9/5.5 + local a=1/3 + local b=8.0-17.5*a + aoaunits=(degrees-b)/a +]] + end + + return aoaunits +end + +--- Get optimal aircraft flight parameters at checkpoint. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #string step Pattern step. +-- @return #number Altitude in meters or nil. +-- @return #number Angle of Attack or nil. +-- @return #number Distance to carrier in meters or nil. +-- @return #number Speed in m/s or nil. +function AIRBOSS:_GetAircraftParameters(playerData, step) + + -- Get parameters depended on step. + step=step or playerData.step + + -- Get AC type. + local hornet=playerData.actype==AIRBOSS.AircraftCarrier.HORNET + local skyhawk=playerData.actype==AIRBOSS.AircraftCarrier.A4EC + local tomcat=playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B + + -- Return values. + local alt + local aoa + local dist + local speed + + -- Aircraft specific AoA. + local aoaac=self:_GetAircraftAoA(playerData) + + if step==AIRBOSS.PatternStep.PLATFORM then + + alt=UTILS.FeetToMeters(5000) + + dist=UTILS.NMToMeters(20) + + speed=UTILS.KnotsToMps(250) + + elseif step==AIRBOSS.PatternStep.ARCIN then + + speed=UTILS.KnotsToMps(250) + + elseif step==AIRBOSS.PatternStep.ARCOUT then + + speed=UTILS.KnotsToMps(250) + + elseif step==AIRBOSS.PatternStep.DIRTYUP then + + alt=UTILS.FeetToMeters(1200) + + dist=UTILS.NMToMeters(12) + + speed=UTILS.KnotsToMps(250) + + elseif step==AIRBOSS.PatternStep.BULLSEYE then + + alt=UTILS.FeetToMeters(1200) + + dist=-UTILS.NMToMeters(3) + + aoa=aoaac.OnSpeed + + elseif step==AIRBOSS.PatternStep.INITIAL then + + if hornet or tomcat then + alt=UTILS.FeetToMeters(800) + speed=UTILS.KnotsToMps(350) + elseif skyhawk then + alt=UTILS.FeetToMeters(600) + speed=UTILS.KnotsToMps(250) + end + + elseif step==AIRBOSS.PatternStep.BREAKENTRY then + + if hornet or tomcat then + alt=UTILS.FeetToMeters(800) + speed=UTILS.KnotsToMps(350) + elseif skyhawk then + alt=UTILS.FeetToMeters(600) + speed=UTILS.KnotsToMps(250) + end + + elseif step==AIRBOSS.PatternStep.EARLYBREAK then + + if hornet or tomcat then + alt=UTILS.FeetToMeters(800) + elseif skyhawk then + alt=UTILS.FeetToMeters(600) + end + + elseif step==AIRBOSS.PatternStep.LATEBREAK then + + if hornet or tomcat then + alt=UTILS.FeetToMeters(800) + elseif skyhawk then + alt=UTILS.FeetToMeters(600) + end + + elseif step==AIRBOSS.PatternStep.ABEAM then + + if hornet or tomcat then + alt=UTILS.FeetToMeters(600) + elseif skyhawk then + alt=UTILS.FeetToMeters(500) + end + + aoa=aoaac.OnSpeed + + dist=UTILS.NMToMeters(1.2) + + elseif step==AIRBOSS.PatternStep.NINETY then + + if hornet or tomcat then + alt=UTILS.FeetToMeters(500) + elseif skyhawk then + alt=UTILS.FeetToMeters(500) + end + + aoa=aoaac.OnSpeed + + elseif step==AIRBOSS.PatternStep.WAKE then + + if hornet or tomcat then + alt=UTILS.FeetToMeters(370) + elseif skyhawk then + alt=UTILS.FeetToMeters(370) --? + end + + aoa=aoaac.OnSpeed + + elseif step==AIRBOSS.PatternStep.FINAL then + + if hornet or tomcat then + alt=UTILS.FeetToMeters(300) + elseif skyhawk then + alt=UTILS.FeetToMeters(300) --? + end + + aoa=aoaac.OnSpeed + + end + + return alt, aoa, dist, speed +end + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- QUEUE Functions +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get next marshal flight which is ready to enter the landing pattern. +-- @param #AIRBOSS self +-- @return #AIRBOSS.FlightGroup Marshal flight next in line and ready to enter the pattern. Or nil if no flight is ready. +function AIRBOSS:_GetNextMarshalFight() + + -- Loop over all marshal flights. + for _,_flight in pairs(self.Qmarshal) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Current stack. + local stack=flight.flag:Get() + + -- Total marshal time in seconds. + local Tmarshal=timer.getAbsTime()-flight.time + + -- Min time in marshal stack. + local TmarshalMin=2*60 --Two minutes for human players. + if flight.ai then + TmarshalMin=3*60 -- Three minutes for AI. + end + + -- Check if conditions are right. + if stack==1 and flight.holding~=nil and Tmarshal>=TmarshalMin then + return flight + end + end + + return nil +end + + +--- Check marshal and pattern queues. +-- @param #AIRBOSS self +function AIRBOSS:_CheckQueue() + + -- Print queues. + if self.Debug then + self:_PrintQueue(self.flights, "All Flights") + end + self:_PrintQueue(self.Qmarshal, "Marshal") + self:_PrintQueue(self.Qpattern, "Pattern") + self:_PrintQueue(self.Qwaiting, "Waiting") + + -- If flights are waiting outside 10 NM zone and carrier switches from Case I to Case II/III, they should be added to the Marshal stack as now there is no stack limit any more. + if self.case>1 then + for _,_flight in pairs(self.Qwaiting) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Remove flight from waiting queue. + local removed=self:_RemoveFlightFromQueue(self.Qwaiting, flight) + + if removed then + + -- Get free stack + local stack=self:_GetFreeStack(flight.ai) + + -- Debug info. + self:T(self.lid..string.format("Moving flight %s onboard %s from Waiting queue to Case %d Marshal stack %d", flight.groupname, flight.onboard, self.case, stack)) + + -- Send flight to marshal stack. + if flight.ai then + self:_MarshalAI(flight, stack) + else + self:_MarshalPlayer(flight, stack) + end + + -- Break the loop so that only one flight per 30 seconds is removed. + break + end + + end + end + + -- Check if carrier is currently in recovery mode. + if not self:IsRecovering() then + + -- Loop over all flights currently in the marshal queue. + for _,_flight in pairs(self.Qmarshal) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Check if they have the right case. + if flight.case~=self.case then + + -- Remove flight from marshal queue. + local removed=self:_RemoveFlightFromQueue(self.Qmarshal, flight) + + if removed then + + -- Get free stack + local stack=self:_GetFreeStack(flight.ai) + + -- Debug output. + self:T(self.lid..string.format("Moving flight %s onboard %s from Marshal Case %d ==> %d Marshal stack %d", flight.groupname, flight.onboard, flight.case, self.case, stack)) + + -- Send flight to marshal queue. + if flight.ai then + self:_MarshalAI(flight, stack) + else + self:_MarshalPlayer(flight, stack) + end + + -- Break the loop so that only one flight per 30 seconds is removed. No spam of messages, no conflict with the loop over queue entries. + break + end + end + end + + + -- Not recovering ==> skip the rest! + return + end + + -- Get number of airborne aircraft units(!) currently in pattern. + local _,npattern=self:_GetQueueInfo(self.Qpattern) + + -- Get next marshal flight. + local marshalflight=self:_GetNextMarshalFight() + + -- Check if there are flights waiting in the Marshal stack and if the pattern is free. + if marshalflight and npattern0 then + + -- Last flight group send to pattern. + local patternflight=self.Qpattern[#self.Qpattern] --#AIRBOSS.FlightGroup + + -- Recovery case of pattern flight. + pcase=patternflight.case + + -- Number of airborne aircraft in this group. Count includes section members. + local npunits=self:_GetFlightUnits(patternflight, false) + + -- Get time in pattern. + Tpattern=timer.getAbsTime()-patternflight.time + self:T(self.lid..string.format("Pattern time of last group %s = %d seconds. # of units=%d.", patternflight.groupname, Tpattern, npunits)) + end + + -- Min time in pattern before next aircraft is allowed. + local TpatternMin + if pcase==1 then + TpatternMin=2*60*npunits --45*npunits -- 45 seconds interval per plane! + else + TpatternMin=2*60*npunits --120*npunits -- 120 seconds interval per plane! + end + + -- Check interval to last pattern flight. + if Tpattern>TpatternMin then + self:T(self.lid..string.format("Sending marshal flight %s to pattern.", marshalflight.groupname)) + self:_ClearForLanding(marshalflight) + end + + end +end + +--- Clear flight for landing. AI are removed from Marshal queue and the Marshal stack is collapsed. +-- If next in line is an AI flight, this is done. If human player is next, we wait for "Commence" via F10 radio menu command. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight to go to pattern. +function AIRBOSS:_ClearForLanding(flight) + + -- Check if flight is AI or human. If AI, we collapse the stack and commence. If human, we suggest to commence. + if flight.ai then + + -- Collapse stack and send AI to pattern. + self:_RemoveFlightFromMarshalQueue(flight, false) + self:_LandAI(flight) + + else + + -- Set step to commencing. This will trigger the zone check until the player is in the right place. + self:_SetPlayerStep(flight, AIRBOSS.PatternStep.COMMENCING, 3) + + end + + -- Inform all Marshal flights. + local text=string.format("you are cleared for Case %d recovery.", flight.case) + + -- Add a little delay because message that recovery window opened could come just before. + self:MessageToMarshal(text, "MARSHAL", flight.onboard, nil, false, 2) + +end + +--- Set player step. Any warning is erased and next step hint shown. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string step Next step. +-- @param #number delay (Optional) Set set after a delay in seconds. +function AIRBOSS:_SetPlayerStep(playerData, step, delay) + + if delay and delay>0 then + -- Delayed call. + SCHEDULER:New(nil, self._SetPlayerStep, {self, playerData, step}, delay) + else + + -- Check if player still exists after possible delay. + if playerData then + + -- Set player step. + playerData.step=step + + -- Erase warning. + playerData.warning=nil + + -- Next step hint. + self:_StepHint(playerData) + end + + end + +end + +--- Scan carrier zone for (new) units. +-- @param #AIRBOSS self +function AIRBOSS:_ScanCarrierZone() + + -- Carrier position. + local coord=self:GetCoordinate() + + -- Scan radius = radius of the CCA. + local RCCZ=self.zoneCCA:GetRadius() + + -- Debug info. + self:T(self.lid..string.format("Scanning Carrier Controlled Area. Radius=%.1f NM.", UTILS.MetersToNM(RCCZ))) + + -- Scan units in carrier zone. + local _,_,_,unitscan=coord:ScanObjects(RCCZ, true, false, false) + + + -- Make a table with all groups currently in the CCA zone. + local insideCCA={} + for _,_unit in pairs(unitscan) do + local unit=_unit --Wrapper.Unit#UNIT + + -- Necessary conditions to be met: + local airborne=unit:IsAir() and unit:InAir() + local inzone=unit:IsInZone(self.zoneCCA) + local friendly=self:GetCoalition()==unit:GetCoalition() + local carrierac=self:_IsCarrierAircraft(unit) + + -- Check if this an aircraft and that it is airborne and closing in. + if airborne and inzone and friendly and carrierac then + + local group=unit:GetGroup() + local groupname=group:GetName() + + if insideCCA[groupname]==nil then + insideCCA[groupname]=group + end + + end + end + + + -- Find new flights that are inside CCA. + for groupname,_group in pairs(insideCCA) do + local group=_group --Wrapper.Group#GROUP + + -- Get flight group if possible. + local knownflight=self:_GetFlightFromGroupInQueue(group, self.flights) + + -- Get aircraft type name. + local actype=group:GetTypeName() + + -- Create a new flight group + if knownflight then + + -- Debug output. + self:T2(self.lid..string.format("Known flight group %s of type %s in CCA.", groupname, actype)) + + -- Check if flight is AI and if we want to handle it at all. + if knownflight.ai and self.handleai then + + -- Check if AI group is part of the group set if a set was defined. + local iscarriersquad=true + if self.squadsetAI then + local group=self.squadsetAI:FindGroup(groupname) + if group then + iscarriersquad=true + else + iscarriersquad=false + end + end + + -- Get distance to carrier. + local dist=knownflight.group:GetCoordinate():Get2DDistance(self:GetCoordinate()) + + -- Close in distance. Is >0 if AC comes closer wrt to first detected distance d0. + local closein=knownflight.dist0-dist + + -- Debug info. + self:T3(self.lid..string.format("Known AI flight group %s closed in by %.1f NM", knownflight.groupname, UTILS.MetersToNM(closein))) + + -- Send AI flight to marshal stack if group closes in more than 5 and has initial flag value. + if closein>UTILS.NMToMeters(5) and knownflight.flag:Get()==-100 and iscarriersquad then + + -- Check that we do not add a recovery tanker for marshaling. + if self.tanker and self.tanker.tanker:GetName()==groupname then + + -- Don't touch the recovery tanker! + + else + + -- Get the next free stack for current recovery case. + local stack=self:_GetFreeStack(knownflight.ai) + + if stack then + + -- Send AI to marshal stack. + self:_MarshalAI(knownflight, stack) + + else + + -- Send AI to orbit outside 10 NM zone and wait until the next Marshal stack is available. + if not self:_InQueue(self.Qwaiting, knownflight.group) then + self:_WaitAI(knownflight) + end + + end + + -- Break the loop to not have all flights at once! Spams the message screen. + break + + end -- Tanker + end -- Closed in + end -- AI + + else + + -- Unknown new AI flight. Create a new flight group. + if not self:_IsHuman(group) then + self:_CreateFlightGroup(group) + end + + end + + end + + + -- Find flights that are not in CCA. + local remove={} + for _,_flight in pairs(self.flights) do + local flight=_flight --#AIRBOSS.FlightGroup + if insideCCA[flight.groupname]==nil then + -- Do not remove flights in marshal pattern. At least for case 2 & 3. If zone is set small, they might be outside in the holding pattern. + if flight.ai and not (self:_InQueue(self.Qmarshal, flight.group) or self:_InQueue(self.Qpattern, flight.group)) then + table.insert(remove, flight) + end + end + end + + -- Remove flight groups outside CCA. + for _,flight in pairs(remove) do + self:_RemoveFlightFromQueue(self.flights, flight) + end + +end + +--- Tell player to wait outside the 10 NM zone until a Marshal stack is available. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_WaitPlayer(playerData) + + -- Check if flight is known to the airboss already. + if playerData then + + -- Number of waiting flights + local nwaiting=#self.Qwaiting + + -- Message text. + local text=string.format("Marshal stack is currently full. Hold outside 10 NM zone and wait for further instructions. ") + if nwaiting==1 then + text=text..string.format("There is one flight ahead of you.") + elseif nwaiting>1 then + text=text..string.format("There are %d flights ahead of you.", nwaiting) + else + text=text..string.format("You are next in line.") + end + + -- Send message. + self:MessageToMarshal(text, "AIRBOSS", playerData.onboard) + + -- Add player flight to waiting queue. + table.insert(self.Qwaiting, playerData) + + -- Set time stamp. + playerData.time=timer.getAbsTime() + + -- Set step to waiting. + playerData.step=AIRBOSS.PatternStep.WAITING + playerData.warning=nil + + -- Set all flights in section to waiting. + for _,_flight in pairs(playerData.section) do + local flight=_flight --#AIRBOSS.PlayerData + flight.step=AIRBOSS.PatternStep.WAITING + flight.time=timer.getAbsTime() + flight.warning=nil + end + + end + +end + + +--- Orbit at a specified position at a specified altitude with a specified speed. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #number stack The Marshal stack the player gets. +function AIRBOSS:_MarshalPlayer(playerData, stack) + + -- Check if flight is known to the airboss already. + if playerData then + + -- Add group to marshal stack. + self:_AddMarshalGroup(playerData, stack) + + -- Set step to holding. + playerData.step=AIRBOSS.PatternStep.HOLDING + playerData.warning=nil + + -- Holding switch to nil until player arrives in the holding zone. + playerData.holding=nil + + -- Set same stack for all flights in section. + for _,_flight in pairs(playerData.section) do + -- TODO: inform section members. + local flight=_flight --#AIRBOSS.PlayerData + flight.case=playerData.case + flight.step=AIRBOSS.PatternStep.HOLDING + flight.holding=nil + flight.flag:Set(stack) + end + + else + self:E(self.lid.."ERROR: Could not add player to Marshal stack! playerData=nil") + end + +end + +--- Command AI flight to orbit outside the 10 NM zone and wait for a free Marshal stack. +-- If the flight is not already holding in the Marshal stack, it is guided there first. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight group. +function AIRBOSS:_WaitAI(flight) + + -- Set flag to something other than -100 and <0 + flight.flag:Set(-99) + + -- Add AI flight to waiting queue. + table.insert(self.Qwaiting, flight) + + -- Flight group name. + local group=flight.group + local groupname=flight.groupname + + -- Aircraft speed 274 knots TAS ~= 250 KIAS when orbiting the pattern. (Orbit expects m/s.) + local speedOrbitMps=UTILS.KnotsToMps(274) + + -- Orbit speed in km/h for waypoints. + local speedOrbitKmh=UTILS.KnotsToKmph(274) + + -- Aircraft speed 400 knots when transiting to holding zone. (Waypoint expects km/h.) + local speedTransit=UTILS.KnotsToKmph(370) + + -- Carrier coordinate + local cv=self:GetCoordinate() + + -- Coordinate of flight group + local fc=group:GetCoordinate() + + -- Carrier heading + local hdg=self:GetHeading(false) + + -- Heading from carrier to flight group + local hdgto=cv:HeadingTo(fc) + + -- Holding alitude between angels 6 and 10 (random). + local angels=math.random(6,10) + local altitude=UTILS.FeetToMeters(angels*1000) + + -- Point outsize 10 NM zone of the carrier. + local p0=cv:Translate(UTILS.NMToMeters(11), hdgto):Translate(UTILS.NMToMeters(5), hdg):SetAltitude(altitude) + + -- Waypoints array to be filled depending on case etc. + local wp={} + + -- Current position. Always good for as the first waypoint. + wp[1]=group:GetCoordinate():WaypointAirTurningPoint(nil, speedTransit, {}, "Current Position") + + -- Set orbit task. + local taskorbit=group:TaskOrbit(p0, altitude, speedOrbitMps) + + -- Orbit at waypoint. + wp[#wp+1]=p0:WaypointAirTurningPoint(nil, speedOrbitKmh, {taskorbit}, string.format("Waiting Orbit at Angels %d", angels)) + + -- Debug markers. + if self.Debug then + p0:MarkToAll(string.format("Waiting Orbit of flight %s at Angels %s", groupname, angels)) + end + + -- Reinit waypoints. + group:WayPointInitialize(wp) + + -- Route group. + group:Route(wp, 0) +end + +--- Command AI flight to orbit at a specified position at a specified altitude with a specified speed. If flight is not in the Marshal queue yet, it is added. This fixes the recovery case. +-- If the flight is not already holding in the Marshal stack, it is guided there first. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight group. +-- @param #number nstack Stack number of group. Can also be the current stack if AI position needs to be updated wrt to changed carrier position. +function AIRBOSS:_MarshalAI(flight, nstack) + + -- Check if flight is already in Marshal queue. + if not self:_InQueue(self.Qmarshal,flight.group) then + -- Add group to marshal stack queue. + self:_AddMarshalGroup(flight, nstack) + end + + -- Recovery case. + local case=flight.case + + -- Get old/current stack. + local ostack=flight.flag:Get() + + -- Flight group name. + local group=flight.group + local groupname=flight.groupname + + -- Set new stack. + flight.flag:Set(nstack) + + -- Current carrier position. + local Carrier=self:GetCoordinate() + + -- Carrier heading. + local hdg=self:GetHeading() + + -- Aircraft speed 274 knots TAS ~= 250 KIAS when orbiting the pattern. (Orbit expects m/s.) + local speedOrbitMps=UTILS.KnotsToMps(274) + + -- Orbit speed in km/h for waypoints. + local speedOrbitKmh=UTILS.KnotsToKmph(274) + + -- Aircraft speed 400 knots when transiting to holding zone. (Waypoint expects km/h.) + local speedTransit=UTILS.KnotsToKmph(370) + + local altitude + local p0 --Core.Point#COORDINATE + local p1 --Core.Point#COORDINATE + local p2 --Core.Point#COORDINATE + + -- Get altitude and positions. + altitude, p1, p2=self:_GetMarshalAltitude(nstack, case) + + -- Waypoints array to be filled depending on case etc. + local wp={} + + -- If flight has not arrived in the holding zone, we guide it there. + if not flight.holding then + + ---------------------- + -- Route to Holding -- + ---------------------- + + -- Debug info. + self:T(self.lid..string.format("Guiding AI flight %s to marshal stack %d-->%d.", groupname, ostack, nstack)) + + -- Current position. Always good for as the first waypoint. + wp[1]=group:GetCoordinate():WaypointAirTurningPoint(nil, speedTransit, {}, "Current Position") + + -- Task function when arriving at the holding zone. This will set flight.holding=true. + local TaskArrivedHolding=flight.group:TaskFunction("AIRBOSS._ReachedHoldingZone", self, flight) + + -- Select case. + if case==1 then + + -- Initial point 7 NM and a bit port of carrier. + -- TODO: Test and tune! + local pE=Carrier:Translate(UTILS.NMToMeters(7), hdg-30):SetAltitude(altitude) + + -- Entry point 5 NM port and slightly astern the boat. + p0=Carrier:Translate(UTILS.NMToMeters(5), hdg-135):SetAltitude(altitude) + + -- Waypoint ahead of carrier's holding zone. + wp[#wp+1]=pE:WaypointAirTurningPoint(nil, speedTransit, {TaskArrivedHolding}, "Entering Case I Marshal Pattern") + + else + + -- Get correct radial depending on recovery case including offset. + local radial=self:GetRadial(case, false, true) + + -- Point in the middle of the race track and a 5 NM more port perpendicular. + p0=p2:Translate(UTILS.NMToMeters(5), radial+90):Translate(UTILS.NMToMeters(5), radial, true) + + -- Entering Case II/III marshal pattern waypoint. + wp[#wp+1]=p0:WaypointAirTurningPoint(nil, speedTransit, {TaskArrivedHolding}, "Entering Case II/III Marshal Pattern") + + end + + else + + ------------------------ + -- In Marshal Pattern -- + ------------------------ + + -- Debug info. + self:T(self.lid..string.format("Updating AI flight %s at marshal stack %d-->%d.", groupname, ostack, nstack)) + + -- Current position. Speed expected in km/h. + wp[1]=group:GetCoordinate():WaypointAirTurningPoint(nil, speedOrbitKmh, {}, "Current Position") + + -- Create new waypoint 0.2 Nm ahead of current positon. + p0=group:GetCoordinate():Translate(UTILS.NMToMeters(0.2), group:GetHeading(), true) + + end + + -- Set orbit task. + local taskorbit=group:TaskOrbit(p1, altitude, speedOrbitMps, p2) + + -- Orbit at waypoint. + wp[#wp+1]=p0:WaypointAirTurningPoint(nil, speedOrbitKmh, {taskorbit}, string.format("Marshal Orbit Stack %d", nstack)) + + -- Debug markers. + if self.Debug then + p0:MarkToAll("WP P0 "..groupname) + p1:MarkToAll("RT P1 "..groupname) + p2:MarkToAll("RT P2 "..groupname) + end + + -- Reinit waypoints. + group:WayPointInitialize(wp) + + -- Route group. + group:Route(wp, 0) + +end + +--- Tell AI to land on the carrier. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight group. +function AIRBOSS:_LandAI(flight) + + -- Debug info. + self:T(self.lid..string.format("Landing AI flight %s.", flight.groupname)) + + -- NOTE: Looks like the AI needs to approach at the "correct" speed. If they are too fast, they fly an unnecessary circle to bleed of speed first. + -- Unfortunately, the correct speed depends on the aircraft type! + + -- Aircraft speed when flying the pattern. + local Speed=UTILS.KnotsToKmph(200) + + if flight.actype==AIRBOSS.AircraftCarrier.HORNET or flight.actype==AIRBOSS.AircraftCarrier.FA18C then + Speed=UTILS.KnotsToKmph(200) + elseif flight.actype==AIRBOSS.AircraftCarrier.E2D then + Speed=UTILS.KnotsToKmph(150) + elseif flight.actype==AIRBOSS.AircraftCarrier.F14A_AI or flight.actype==AIRBOSS.AircraftCarrier.F14A or flight.actype==AIRBOSS.AircraftCarrier.F14B then + Speed=UTILS.KnotsToKmph(175) + elseif flight.actype==AIRBOSS.AircraftCarrier.S3B or flight.actype==AIRBOSS.AircraftCarrier.S3BTANKER then + Speed=UTILS.KnotsToKmph(140) + end + + -- Carrier position. + local Carrier=self:GetCoordinate() + + -- Carrier heading. + local hdg=self:GetHeading() + + -- Waypoints array. + local wp={} + + local CurrentSpeed=flight.group:GetVelocityKMH() + + -- Current positon. + wp[#wp+1]=flight.group:GetCoordinate():WaypointAirTurningPoint(nil, CurrentSpeed, {}, "Current position") + + -- Altitude 800 ft. Looks like this works best. + local alt=UTILS.FeetToMeters(800) + + -- Landing waypoint 5 NM behind carrier at 2000 ft = 610 meters ASL. + wp[#wp+1]=Carrier:Translate(UTILS.NMToMeters(4), hdg-160):SetAltitude(alt):WaypointAirLanding(Speed, self.airbase, nil, "Landing") + --wp[#wp+1]=self:GetCoordinate():Translate(UTILS.NMToMeters(3), hdg-160):SetAltitude(alt):WaypointAirTurningPoint(nil,Speed, {}, "Before Initial") ---WaypointAirLanding(Speed, self.airbase, nil, "Landing") + -- + --wp[#wp+1]=self:GetCoordinate():WaypointAirLanding(Speed, self.airbase, nil, "Landing") + + -- Reinit waypoints. + flight.group:WayPointInitialize(wp) + + -- Route group. + flight.group:Route(wp, 0) +end + +--- Get marshal altitude and two positions of a counter-clockwise race track pattern. +-- @param #AIRBOSS self +-- @param #number stack Assigned stack number. Counting starts at one, i.e. stack=1 is the first stack. +-- @param #number case Recovery case. Default is self.case. +-- @return #number Holding altitude in meters. +-- @return Core.Point#COORDINATE First race track coordinate. +-- @return Core.Point#COORDINATE Second race track coordinate. +function AIRBOSS:_GetMarshalAltitude(stack, case) + + -- Stack <= 0. + if stack<=0 then + return 0,nil,nil + end + + -- Recovery case. + case=case or self.case + + -- Carrier position. + local Carrier=self:GetCoordinate() + + -- Altitude of first stack. Depends on recovery case. + local angels0 + local Dist + local p1=nil --Core.Point#COORDINATE + local p2=nil --Core.Point#COORDINATE + + -- Stack number. + local nstack=stack-1 + + if case==1 then + + -- CASE I: Holding at 2000 ft on a circular pattern port of the carrier. Interval +1000 ft for next stack. + angels0=2 + + -- Get true heading of carrier. + local hdg=self.carrier:GetHeading() + + -- For CCW pattern: First point astern, second ahead of the carrier. + + -- First point over carrier. + p1=Carrier + + -- Second point 1.5 NM ahead. + p2=Carrier:Translate(UTILS.NMToMeters(1.5), hdg) + + else + + -- CASE II/III: Holding at 6000 ft on a racetrack pattern astern the carrier. + angels0=6 + + -- Distance: d=n*angels0+15 NM, so first stack is at 15+6=21 NM + Dist=UTILS.NMToMeters(nstack+angels0+15) + + -- Get correct radial depending on recovery case including offset. + local radial=self:GetRadial(case, false, true) + + -- For CCW pattern: p1 further astern than p2. + + -- Length of the race track pattern. + local l=UTILS.NMToMeters(7) + + -- First point of race track pattern. + p1=Carrier:Translate(Dist+l, radial) + + -- Second point. + p2=Carrier:Translate(Dist, radial) + + end + + -- Pattern altitude. + local altitude=UTILS.FeetToMeters((nstack+angels0)*1000) + + -- Set altitude of coordinate. + p1:SetAltitude(altitude, true) + p2:SetAltitude(altitude, true) + + return altitude, p1, p2 +end + +--- Calculate an estimate of the charlie time of the player based on how many other aircraft are in the marshal or pattern queue before him. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flightgroup Flight data. +-- @return #number Charlie (abs) time in seconds. Or nil, if stack<0 or no recovery window will open. +function AIRBOSS:_GetCharlieTime(flightgroup) + + -- Get current stack of player. + local stack=flightgroup.flag:Get() + + -- Flight is not in marshal stack. + if stack<=0 then + return nil + end + + -- Current abs time. + local Tnow=timer.getAbsTime() + + -- Time the player has to spend in marshal stack until all lower stacks are emptied. + local Tcharlie=0 + + local Trecovery=0 + if self.recoverywindow then + -- Time in seconds until the next recovery starts or 0 if window is already open. + Trecovery=math.max(self.recoverywindow.START-Tnow, 0) + else + return nil + end + + -- Loop over flights currently in the marshal queue. + for _,_flight in pairs(self.Qmarshal) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Stack of marshal flight. + local mstack=flight.flag:Get() + + -- Time to get to the marshal stack if not holding already. + local Tarrive=0 + + -- Minimum holding time per stack. + local Tholding=3*60 + + if stack>0 and mstack>0 and mstack<=stack then + + -- Check if flight is already holding or just on its way. + if flight.holding==nil then + -- Flight is also on its way to the marshal stack. + + -- Coordinate of the holding zone. + local holdingzone=self:_GetZoneHolding(flight.case, 1):GetCoordinate() + + -- Distance to holding zone. + local d0=holdingzone:Get2DDistance(flight.group:GetCoordinate()) + + -- Current velocity. + local v0=flight.group:GetVelocityMPS() + + -- Time to get to the carrier. + Tarrive=d0/v0 + + self:T3(self.lid..string.format("Tarrive=%.1f seconds, Clock %s", Tarrive, UTILS.SecondsToClock(Tnow+Tarrive))) + + else + -- Flight is already holding. + + -- Next in line. + if mstack==1 then + + -- Current holding time. flight.time stamp should be when entering holding or last time the stack collapsed. + local tholding=timer.getAbsTime()-flight.time + + -- Deduce current holding time. Ensure that is >=0. + Tholding=math.max(3*60-tholding, 0) + end + + end + + -- This is the approx time needed to get to the pattern. If we are already there, it is the time until the recovery window opens or 0 if it is already open. + local Tmin=math.max(Tarrive, Trecovery) + + -- Charlie time + 2 min holding in stack 1. + Tcharlie=math.max(Tmin, Tcharlie)+Tholding + end + + end + + -- Convert to abs time. + Tcharlie=Tcharlie+Tnow + + -- Debug info. + local text=string.format("Charlie time for flight %s (%s) %s", flightgroup.onboard, flightgroup.groupname, UTILS.SecondsToClock(Tcharlie)) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + return Tcharlie +end + +--- Add a flight group to the Marshal queue at a specific stack. Flight is informed via message. This fixes the recovery case to the current case ops in progress self.case). +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight group. +-- @param #number stack Marshal stack. This (re-)sets the flag value. +function AIRBOSS:_AddMarshalGroup(flight, stack) + + -- Set flag value. This corresponds to the stack number which starts at 1. + flight.flag:Set(stack) + + -- Set recovery case. + flight.case=self.case + + -- Add to marshal queue. + table.insert(self.Qmarshal, flight) + + -- Pressure. + local P=UTILS.hPa2inHg(self:GetCoordinate():GetPressure()) + + -- Stack altitude. + local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack, flight.case)) + + -- Current BRC. + local brc=self:GetBRC() + + -- If the carrier is supposed to turn into the wind, we take the wind coordinate. + if self.recoverywindow and self.recoverywindow.WIND then + brc=self:GetBRCintoWind() + end + + -- Get charlie time estimate. + flight.Tcharlie=self:_GetCharlieTime(flight) + + -- Convert to clock string. + local Ccharlie=UTILS.SecondsToClock(flight.Tcharlie) + + -- Marshal message. + local text=string.format("Case %d, expected BRC %03d°, hold at %d. Expected Charlie Time %s.\n", flight.case, brc, alt, tostring(Ccharlie)) + text=text..string.format("Altimeter %.2f. Report see me.", P) + + -- Message to all players. + self:MessageToMarshal(text, "MARSHAL", flight.onboard) + + -- Hint about TACAN bearing. + if self.TACANon and (not flight.ai) and flight.difficulty==AIRBOSS.Difficulty.EASY then + -- Get inverse magnetic radial potential offset. + local radial=self:GetRadial(flight.case, true, true, true) + if flight.case==1 then + -- For case 1 we want the BRC but above routine return FB. + radial=self:GetBRC() + end + local text=string.format("Select TACAN %03d°, channel %d%s (%s)", radial, self.TACANchannel,self.TACANmode, self.TACANmorse) + self:MessageToPlayer(flight, text, nil, "") + end + +end + +--- Collapse marshal stack. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight that left the marshal stack. +-- @param #boolean nopattern If true, flight does not go to pattern. +function AIRBOSS:_CollapseMarshalStack(flight, nopattern) + self:F2({flight=flight, nopattern=nopattern}) + + -- Recovery case of flight. + local case=flight.case + + -- Stack of flight. + local stack=flight.flag:Get() + + -- Check that stack > 0. + if stack<=0 then + self:E(self.lid..string.format("ERROR: Flight %s is has stack value %d<0. Cannot collapse stack!", flight.groupname, stack)) + return + end + + -- Memorize time when stack collapsed. Should better depend on case but for now we assume there are no two different stacks Case I or II/III. + self.Tcollapse=timer.getTime() + + -- Decrease flag values of all flight groups in marshal stack. + for _,_flight in pairs(self.Qmarshal) do + local mflight=_flight --#AIRBOSS.PlayerData + + -- Only collapse stack of which the flight left. CASE II/III stack is the same. + if (case==1 and mflight.case==1) or (case>1 and mflight.case>1) then + + -- Get current flag/stack value. + local mstack=mflight.flag:Get() + + -- Only collapse stacks above the new pattern flight. + if mstack>stack then + + -- OLD: New stack is old stack minus one. + --local newstack=mstack-1 + + -- NEW: Is this now right as we allow more flights per stack? + -- TODO: Question is, does the stack collapse if the lower stack is completely empty or do aircraft descent if just one flight leaves. + -- For now, assuming that the stack must be completely empty before the next higher AC are allowed to descent. + local newstack=self:_GetFreeStack(mflight.ai, mflight.case, true) + + -- Free stack has to be below. + if newstack and newstack %d.", mflight.groupname, mflight.case, mstack, newstack)) + + if mflight.ai then + + -- Command AI to decrease stack. Flag is set in the routine. + self:_MarshalAI(mflight, newstack) + + else + + -- Decrease stack/flag. Human player needs to take care himself. + mflight.flag:Set(newstack) + + -- Angels of new stack. + local angels=self:_GetAngels(self:_GetMarshalAltitude(newstack, case)) + + -- Inform players. + if mflight.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Send message to all non-pros that they can descent. + local text=string.format("descent to stack at Angels %d.", angels) + self:MessageToPlayer(mflight, text, "MARSHAL") + + end + + -- Set time stamp. + mflight.time=timer.getAbsTime() + + -- Loop over section members. + for _,_sec in pairs(mflight.section) do + local sec=_sec --#AIRBOSS.PlayerData + + -- Also decrease flag for section members of flight. + sec.flag:Set(newstack) + + -- Set new time stamp. + sec.time=timer.getAbsTime() + + -- Inform section member. + if sec.difficulty~=AIRBOSS.Difficulty.HARD then + local text=string.format("descent to stack at Angels %d.", angels) + self:MessageToPlayer(sec, text, "MARSHAL") + end + + end + + end + + end + end + end + end + + + if nopattern then + + -- Debug message. + self:T(self.lid..string.format("Flight %s is leaving stack but not going to pattern.", flight.groupname)) + + else + + -- Debug message. + local Tmarshal=UTILS.SecondsToClock(timer.getAbsTime()-flight.time) + self:T(self.lid..string.format("Flight %s is leaving marshal after %s and going pattern.", flight.groupname, Tmarshal)) + + -- Add flight to pattern queue. + table.insert(self.Qpattern, flight) + + end + + -- Set flag to -1 (-1 is rather arbitrary but it should not be positive or -100 or -42). + flight.flag:Set(-1) + + -- New time stamp for time in pattern. + flight.time=timer.getAbsTime() + +end + +--- Get next free Marshal stack. Depending on AI/human and recovery case. +-- @param #AIRBOSS self +-- @param #boolean ai If true, get a free stack for an AI flight group. +-- @param #number case Recovery case. Default current (self) case in progress. +-- @param #boolean empty Return lowest stack that is completely empty. +-- @return #number Lowest free stack available for the given case or nil if all Case I stacks are taken. +function AIRBOSS:_GetFreeStack(ai, case, empty) + + -- Recovery case. + case=case or self.case + + -- Max number of stacks available. + local nmaxstacks=100 + if case==1 then + nmaxstacks=self.Nmaxmarshal + end + + -- Assume up to two (human) flights per stack. All are free. + local stack={} + for i=1,nmaxstacks do + stack[i]=self.NmaxStack -- Number of human flights per stack. + end + + -- Loop over all flights in marshal stack. + for _,_flight in pairs(self.Qmarshal) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Check that the case is right. + if flight.case==case then + + -- Get stack of flight. + local n=flight.flag:Get() + + if n>0 then + if flight.ai then + stack[n]=0 -- AI get one stack on their own. + else + stack[n]=stack[n]-1 + end + else + self:E(string.format("ERROR: Flight %s in marshal stack has stack value <= 0. Stack value is %d.", flight.groupname, n)) + end + + end + end + + -- Loop over stacks and check which one has a place left. + local nfree=nil + for i=1,nmaxstacks do + self:T2(self.lid..string.format("FF Stack[%d]=%d", i, stack[i])) + if ai or empty then + -- AI need the whole stack. + if stack[i]==self.NmaxStack then + nfree=i + return i + end + else + -- Human players only need one free spot. + if stack[i]>0 then + nfree=i + return i + end + end + end + + return nfree +end + +--- Get number of (airborne) units in a flight. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight The flight group. +-- @param #boolean onground If true, include units on the ground. By default only airborne units are counted. +-- @return #number Number of units in flight including section members. +-- @return #number Number of units in flight excluding section members. +-- @return #number Number of section members. +function AIRBOSS:_GetFlightUnits(flight, onground) + + -- Default is only airborne. + local inair=true + if onground==true then + inair=false + end + + --- Count units of a group which are alive and in the air. + local function countunits(_group, inair) + local group=_group --Wrapper.Group#GROUP + local units=group:GetUnits() + local n=0 + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + if unit and unit:IsAlive() then + if inair then + -- Only count units in air. + if unit:InAir() then + n=n+1 + end + else + -- Count units in air or on the ground. + n=n+1 + end + end + end + return n + end + + + -- Count units of the group itself (alive units in air). + local nunits=countunits(flight.group, inair) + + -- Count section members. + local nsection=0 + for _,sec in pairs(flight.section) do + local secflight=sec --#AIRBOSS.PlayerData + -- Count alive units in air. + nsection=nsection+countunits(secflight.group, inair) + end + + return nunits+nsection, nunits, nsection +end + +--- Get number of groups and units in queue, which are alive and airborne. In units we count the section members as well. +-- @param #AIRBOSS self +-- @param #table queue The queue. Can be self.flights, self.Qmarshal or self.Qpattern. +-- @param #number case (Optional) Only count flights, which are in a specific recovery case. Note that you can use case=23 for flights that are either in Case II or III. By default all groups/units regardless of case are counted. +-- @return #number Total number of flight groups in queue. +-- @return #number Total number of aircraft in queue since each flight group can contain multiple aircraft. +function AIRBOSS:_GetQueueInfo(queue, case) + + local ngroup=0 + local Nunits=0 + + -- Loop over flight groups. + for _,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Check if a specific case was requested. + if case then + + ------------------------------------------------------------------------ + -- Only count specific case with special 23 = CASE II and III combined. + ------------------------------------------------------------------------ + + if (flight.case==case) or (case==23 and (flight.case==2 or flight.case==3)) then + + -- Number of total units, units in flight and section members ALIVE and AIRBORNE. + local ntot,nunits,nsection=self:_GetFlightUnits(flight) + + -- Add up total unit number. + Nunits=Nunits+ntot + + -- Increase group count. + if ntot>0 then + ngroup=ngroup+1 + end + + end + + else + + --------------------------------------------------------------------------- + -- No specific case requested. Count all groups & units in selected queue. + --------------------------------------------------------------------------- + + -- Number of total units, units in flight and section members ALIVE and AIRBORNE. + local ntot,nunits,nsection=self:_GetFlightUnits(flight) + + -- Add up total unit number. + Nunits=Nunits+ntot + + -- Increase group count. + if ntot>0 then + ngroup=ngroup+1 + end + + end + + end + + return ngroup, Nunits +end + +--- Print holding queue. +-- @param #AIRBOSS self +-- @param #table queue Queue to print. +-- @param #string name Queue name. +function AIRBOSS:_PrintQueue(queue, name) + + --local nqueue=#queue + local Nqueue, nqueue=self:_GetQueueInfo(queue) + + local text=string.format("%s Queue N=%d, n=%d:", name, Nqueue, nqueue) + if nqueue==0 then + text=text.." empty." + else + for i,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.FlightGroup + + local clock=UTILS.SecondsToClock(timer.getAbsTime()-flight.time) + local case=flight.case + local stack=flight.flag:Get() + local fuel=flight.group:GetFuelMin()*100 + local ai=tostring(flight.ai) + local lead=flight.seclead + local Nsec=#flight.section + local actype=self:_GetACNickname(flight.actype) + local onboard=flight.onboard + local holding=tostring(flight.holding) + + -- Airborne units. + local _, nunits, nsec=self:_GetFlightUnits(flight, false) + + -- XXX: Include player data. + --[[ + if not flight.ai then + local playerData=_flight --#AIRBOSS.PlayerData + e=playerData.name + c=playerData.difficulty + f=playerData.passes + g=playerData.step + j=playerData.warning + a=playerData.holding + b=playerData.landed + d=playerData.boltered + h=playerData.lig + i=playerData.patternwo + k=playerData.waveoff + end + ]] + text=text..string.format("\n[%d] %s*%d (%s): lead=%s (%d/%d), onboard=%s, flag=%d, case=%d, time=%s, fuel=%d, ai=%s, holding=%s", + i, flight.groupname, nunits, actype, lead, nsec, Nsec, onboard, stack, case, clock, fuel, ai, holding) + if stack>0 then + local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack, case)) + text=text..string.format(" stackalt=%d ft", alt) + end + end + end + self:T(self.lid..text) +end + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FLIGHT & PLAYER functions +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new flight group. Usually when a flight appears in the CCA. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @return #AIRBOSS.FlightGroup Flight group. +function AIRBOSS:_CreateFlightGroup(group) + + -- Debug info. + self:T(self.lid..string.format("Creating new flight for group %s of aircraft type %s.", group:GetName(), group:GetTypeName())) + + -- New flight. + local flight={} --#AIRBOSS.FlightGroup + + -- Check if not already in flights + if not self:_InQueue(self.flights, group) then + + -- Flight group name + local groupname=group:GetName() + local human, playername=self:_IsHuman(group) + + -- Queue table item. + flight.group=group + flight.groupname=group:GetName() + flight.nunits=#group:GetUnits() + flight.time=timer.getAbsTime() + flight.dist0=group:GetCoordinate():Get2DDistance(self:GetCoordinate()) + flight.flag=USERFLAG:New(groupname) + flight.flag:Set(-100) + flight.ai=not human + flight.actype=group:GetTypeName() + flight.onboardnumbers=self:_GetOnboardNumbers(group) + flight.seclead=flight.group:GetUnit(1):GetName() -- Sec lead is first unitname of group but player name for players. + flight.section={} + flight.ballcall=false + flight.holding=nil + + -- TODO Name should also be set for AI as it is used to get the section lead. Switch this from PlayerData to FlightGroup enumerator. + flight.name=flight.group:GetUnit(1):GetName() + + -- Note, this should be re-set elsewhere! + flight.case=self.case + + -- Flight elements. + local text=string.format("Flight elements of group %s:", flight.groupname) + flight.elements={} + local units=group:GetUnits() + for i,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + local name=unit:GetName() + local element={} --#AIRBOSS.FlightElement + element.unit=unit + element.onboard=flight.onboardnumbers[name] + element.ballcall=false + element.ai=not self:_IsHumanUnit(unit) + text=text..string.format("\n[%d] %s onboard #%s, AI=%s", i, name, tostring(element.onboard), tostring(element.ai)) + table.insert(flight.elements, element) + end + self:T(self.lid..text) + + -- Onboard + if flight.ai then + local onboard=flight.onboardnumbers[flight.seclead] + flight.onboard=onboard + else + flight.onboard=self:_GetOnboardNumberPlayer(group) + end + + -- Add to known flights. + table.insert(self.flights, flight) + + else + self:E(self.lid..string.format("ERROR: Flight group %s already exists in self.flights!", group:GetName())) + return nil + end + + return flight +end + + +--- Initialize player data after birth event of player unit. +-- @param #AIRBOSS self +-- @param #string unitname Name of the player unit. +-- @return #AIRBOSS.PlayerData Player data. +function AIRBOSS:_NewPlayer(unitname) + + -- Get player unit and name. + local playerunit, playername=self:_GetPlayerUnitAndName(unitname) + + if playerunit and playername then + + local group=playerunit:GetGroup() + + -- Player data. + local playerData --#AIRBOSS.PlayerData + + -- Create a flight group for the player. + playerData=self:_CreateFlightGroup(group) + + -- Player unit, client and callsign. + playerData.unit = playerunit + playerData.name = playername + playerData.callsign = playerData.unit:GetCallsign() + playerData.client = CLIENT:FindByName(unitname, nil, true) + playerData.seclead = playername + + -- Number of passes done by player in this slot. + playerData.passes=0 --playerData.passes or 0 + + -- Debriefing tables. + playerData.lastdebrief=playerData.lastdebrief or {} + + -- Attitude monitor. + playerData.attitudemonitor=false + + -- Set difficulty level. + playerData.difficulty=playerData.difficulty or self.defaultskill + + -- Subtitles of player + if playerData.subtitles==nil then + playerData.subtitles=true + end + + -- Points rewarded. + playerData.points={} + + -- Init stuff for this round. + playerData=self:_InitPlayer(playerData) + + -- Init player data. + self.players[playername]=playerData + + -- Init player grades table if necessary. + self.playerscores[playername]=self.playerscores[playername] or {} + + -- Welcome player message. + self:MessageToPlayer(playerData, string.format("Welcome, %s %s!", playerData.difficulty, playerData.name), string.format("AIRBOSS %s", self.alias), "", 5) + + -- Return player data table. + return playerData + end + + return nil +end + +--- Initialize player data by (re-)setting parmeters to initial values. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string step (Optional) New player step. Default UNDEFINED. +-- @return #AIRBOSS.PlayerData Initialized player data. +function AIRBOSS:_InitPlayer(playerData, step) + self:T(self.lid..string.format("Initializing player data for %s callsign %s.", playerData.name, playerData.callsign)) + + playerData.step=step or AIRBOSS.PatternStep.UNDEFINED + playerData.groove={} + playerData.debrief={} + playerData.warning=nil + playerData.holding=nil + playerData.valid=false + playerData.lig=false + playerData.patternwo=false + playerData.waveoff=false + playerData.fouldeckwo=false + playerData.boltered=false + playerData.landed=false + playerData.Tlso=timer.getTime() + playerData.Tgroove=nil + playerData.wire=nil + playerData.flag:Set(-100) + + -- Set us up on final if group name contains "Groove". But only for the first pass. + if playerData.group:GetName():match("Groove") and playerData.passes==0 then + self:MessageToPlayer(playerData, "Group name contains \"Groove\". Happy groove testing.") + playerData.attitudemonitor=true + playerData.step=AIRBOSS.PatternStep.FINAL + table.insert(self.Qpattern, playerData) + end + + return playerData +end + + +--- Get flight from group. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Group that will be removed from queue. +-- @param #table queue The queue from which the group will be removed. +-- @return #AIRBOSS.FlightGroup Flight group. +-- @return #number Queue index. +function AIRBOSS:_GetFlightFromGroupInQueue(group, queue) + + -- Group name + local name=group:GetName() + + -- Loop over all flight groups in queue + for i,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.FlightGroup + + if flight.groupname==name then + return flight, i + end + end + + self:T2(self.lid..string.format("WARNING: Flight group %s could not be found in queue.", name)) + return nil, nil +end + +--- Get element in flight. +-- @param #AIRBOSS self +-- @param #string unitname Name of the unit. +-- @param #AIRBOSS.FlightGroup flight Flight group. +-- @return #AIRBOSS.FlightElement Flight element. +-- @return #number Element index. +function AIRBOSS:_GetFlightElement(unitname, flight) + + -- Loop over all elements in flight group. + for i,_element in pairs(flight.elements) do + local element=_element --#AIRBOSS.FlightElement + + if element.unit:GetName()==unitname then + return element, i + end + end + + self:T2(self.lid..string.format("WARNING: Flight element %s could not be found in flight group.", unitname, flight.groupname)) + return nil, nil +end + +--- Get element in flight. +-- @param #AIRBOSS self +-- @param #string unitname Name of the unit. +-- @param #AIRBOSS.FlightGroup flight Flight group. +-- @return #boolean If true, element could be removed or nil otherwise. +function AIRBOSS:_RemoveFlightElement(unitname, flight) + + -- Get table index. + local element,idx=self:_GetFlightElement(unitname, flight) + + if idx then + table.remove(flight.elements, idx) + return true + else + self:T("WARNING: Flight element could not be removed from flight group. Index=nil!") + return nil + end +end + +--- Check if a group is in a queue. +-- @param #AIRBOSS self +-- @param #table queue The queue to check. +-- @param Wrapper.Group#GROUP group The group to be checked. +-- @return #boolean If true, group is in the queue. False otherwise. +function AIRBOSS:_InQueue(queue, group) + local name=group:GetName() + for _,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.FlightGroup + if name==flight.groupname then + return true + end + end + return false +end + +--- Remove dead flight groups from all queues. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @return #AIRBOSS.FlightGroup Flight group. +function AIRBOSS:_RemoveDeadFlightGroups() + + -- Remove dead flights from all flights table. + for i=#self.flight,1,-1 do + local flight=self.flights[i] --#AIRBOSS.FlightGroup + if not flight.group:IsAlive() then + self:T(string.format("Removing dead flight group %s from ALL flights table.", flight.groupname)) + table.remove(self.flights, i) + end + end + + -- Remove dead flights from Marhal queue table. + for i=#self.Qmarshal,1,-1 do + local flight=self.Qmarshal[i] --#AIRBOSS.FlightGroup + if not flight.group:IsAlive() then + self:T(string.format("Removing dead flight group %s from Marshal Queue table.", flight.groupname)) + table.remove(self.Qmarshal, i) + end + end + + -- Remove dead flights from Pattern queue table. + for i=#self.Qpattern,1,-1 do + local flight=self.Qpattern[i] --#AIRBOSS.FlightGroup + if not flight.group:IsAlive() then + self:T(string.format("Removing dead flight group %s from Pattern Queue table.", flight.groupname)) + table.remove(self.Qpattern, i) + end + end + +end + +--- Remove a flight group from the Marshal queue. Marshal stack is collapsed, too, if flight was in the queue. Waiting flights are send to marshal. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight group that will be removed from queue. +-- @param #boolean nopattern If true, flight is NOT going to landing pattern. +-- @return #boolean If true, flight was removed. +function AIRBOSS:_RemoveFlightFromMarshalQueue(flight, nopattern) + + -- Remove flight from marshal queue if it is in. + local removed=self:_RemoveFlightFromQueue(self.Qmarshal, flight) + + -- Collapse marshal stack if flight was removed. + if removed then + + -- Flight is not holding any more. + flight.holding=nil + + -- Collapse marshal stack if flight was removed. + self:_CollapseMarshalStack(flight, nopattern) + + -- Stacks are only limited for Case I. + if flight.case==1 and #self.Qwaiting>0 then + + -- Next flight in line waiting. + local nextflight=self.Qwaiting[1] --#AIRBOSS.FlightGroup + + -- Get free stack. + local freestack=self:_GetFreeStack(nextflight.ai) + + -- Send next flight to marshal stack. + if nextflight.ai then + + -- Send AI to Marshal Stack. + self:_MarshalAI(nextflight, freestack) + + else + + -- Send player to Marshal stack. + self:_MarshalPlayer(nextflight, freestack) + + end + + -- Remove flight from waiting queue. + self:_RemoveFlightFromQueue(self.Qwaiting, nextflight) + + end + + end + + return removed +end + +--- Remove a flight group from a queue. +-- @param #AIRBOSS self +-- @param #table queue The queue from which the group will be removed. +-- @param #AIRBOSS.FlightGroup flight Flight group that will be removed from queue. +-- @return #boolean If true, flight was in Queue and removed. +function AIRBOSS:_RemoveFlightFromQueue(queue, flight) + + -- Loop over all flights in group. + for i,_flight in pairs(queue) do + local qflight=_flight --#AIRBOSS.FlightGroup + + -- Check for name. + if qflight.groupname==flight.groupname then + self:T(self.lid..string.format("Removing flight group %s from queue.", flight.groupname)) + table.remove(queue, i) + return true + end + end + + return false +end + +--- Remove a unit from a flight group (e.g. when landed) and update all queues if the whole flight group is gone. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit The unit to be removed. +function AIRBOSS:_RemoveUnitFromFlight(unit) + + -- Check if unit exists. + if unit then + + -- Get group. + local group=unit:GetGroup() + + -- Check if group exists. + if group then + + -- Get flight. + local flight=self:_GetFlightFromGroupInQueue(group, self.flights) + + -- Check if flight exists. + if flight then + + -- Remove element from flight group. + local removed=self:_RemoveFlightElement(unit:GetName(), flight) + + if removed then + + -- Get number of units (excluding section members). For AI only those that are stil in air as we assume once they landed, they are out of the game. + local _,nunits=self:_GetFlightUnits(flight, not flight.ai) + + -- Check if no units are left. + if nunits==0 then + -- Remove flight from all queues. + self:_RemoveFlight(flight) + end + + end + end + end + end + +end + +--- Remove a flight from Marshal, Pattern and Waiting queues. If flight is in Marhal queue, the above stack is collapsed. +-- Also set player step to undefined if applicable or remove human flight if option *completely* is true. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData flight The flight to be removed. +-- @param #boolean completely If true, also remove human flight from all flights table. +function AIRBOSS:_RemoveFlight(flight, completely) + + -- Remove flight from all queues. + self:_RemoveFlightFromMarshalQueue(flight, true) + self:_RemoveFlightFromQueue(self.Qpattern, flight) + self:_RemoveFlightFromQueue(self.Qwaiting, flight) + + -- Check if player or AI + if flight.ai then + -- Remove AI flight completely. + self:_RemoveFlightFromQueue(self.flights, flight) + else + if completely then + -- Remove HUMAN flight completely. + self:_RemoveFlightFromQueue(self.flights, flight) + else + -- Set Playerstep to undefined. + flight.step=AIRBOSS.PatternStep.UNDEFINED + end + end + +end + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Player Status +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check current player status. +-- @param #AIRBOSS self +function AIRBOSS:_CheckPlayerStatus() + + -- Loop over all players. + for _playerName,_playerData in pairs(self.players) do + local playerData=_playerData --#AIRBOSS.PlayerData + + if playerData then + + -- Player unit. + local unit=playerData.unit + + -- Check if unit is alive and in air. + if unit:IsAlive() then + + -- Check if player is in carrier controlled area (zone with R=50 NM around the carrier). + if unit:IsInZone(self.zoneCCA) then + + -- Display aircraft attitude and other parameters as message text. + if playerData.attitudemonitor then + self:_AttitudeMonitor(playerData) + end + + -- Check if player is too close to another aircraft in the pattern. + -- TODO: At which steps is the really necessary. Case II/III? + if playerData.step==AIRBOSS.PatternStep.INITIAL or + playerData.step==AIRBOSS.PatternStep.BREAKENTRY or + playerData.step==AIRBOSS.PatternStep.EARLYBREAK or + playerData.step==AIRBOSS.PatternStep.LATEBREAK or + playerData.step==AIRBOSS.PatternStep.ABEAM or + playerData.step==AIRBOSS.PatternStep.GROOVE_XX or + playerData.step==AIRBOSS.PatternStep.GROOVE_IM then + --self:_CheckPlayerPatternDistance(playerData) + end + + -- Foul deck check. + -- TODO: Put steps check into the subroutine. + if playerData.case<3 then + + -- Case I/II: Check is done, when AC is at the wake according to NATOPS. At the wake we switch to final. + if playerData.step==AIRBOSS.PatternStep.FINAL or + playerData.step==AIRBOSS.PatternStep.GROOVE_XX or + playerData.step==AIRBOSS.PatternStep.GROOVE_IM or + playerData.step==AIRBOSS.PatternStep.GROOVE_IC then + + self:_CheckFoulDeck(playerData) + end + + else + + -- Case III: Check is done at 3/4 NM according to NATOPS + if playerData.step==AIRBOSS.PatternStep.GROOVE_XX or + playerData.step==AIRBOSS.PatternStep.GROOVE_IM or + playerData.step==AIRBOSS.PatternStep.GROOVE_IC then + + self:_CheckFoulDeck(playerData) + end + end + + if playerData.step==AIRBOSS.PatternStep.UNDEFINED then + + -- Status undefined. + --local time=timer.getAbsTime() + --local clock=UTILS.SecondsToClock(time) + --self:T3(string.format("Player status undefined. Waiting for next step. Time %s", clock)) + + elseif playerData.step==AIRBOSS.PatternStep.REFUELING then + + -- Nothing to do here at the moment. + + elseif playerData.step==AIRBOSS.PatternStep.SPINNING then + + -- Might still be better to stay in commencing? + + elseif playerData.step==AIRBOSS.PatternStep.HOLDING then + + -- CASE I/II/III: In holding pattern. + self:_Holding(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.WAITING then + + -- CASE I: Waiting outside 10 NM zone for next free Marshal stack. + self:_Waiting(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.COMMENCING then + + -- CASE I/II/III: New approach. + self:_Commencing(playerData, true) + + elseif playerData.step==AIRBOSS.PatternStep.BOLTER then + + -- CASE I/II/III: Bolter pattern. + self:_BolterPattern(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.PLATFORM then + + -- CASE II/III: Player has reached 5k "Platform". + self:_Platform(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.ARCIN then + + -- Case II/III if offset. + self:_ArcInTurn(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.ARCOUT then + + -- Case II/III if offset. + self:_ArcOutTurn(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.DIRTYUP then + + -- CASE III: Player has descended to 1200 ft and is going level from now on. + self:_DirtyUp(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.BULLSEYE then + + -- CASE III: Player has intercepted the glide slope and should follow "Bullseye" (ICLS). + self:_Bullseye(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.INITIAL then + + -- CASE I/II: Player is at the initial position entering the landing pattern. + self:_Initial(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.BREAKENTRY then + + -- CASE I/II: Break entry. + self:_BreakEntry(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.EARLYBREAK then + + -- CASE I/II: Early break. + self:_Break(playerData, AIRBOSS.PatternStep.EARLYBREAK) + + elseif playerData.step==AIRBOSS.PatternStep.LATEBREAK then + + -- CASE I/II: Late break. + self:_Break(playerData, AIRBOSS.PatternStep.LATEBREAK) + + elseif playerData.step==AIRBOSS.PatternStep.ABEAM then + + -- CASE I/II: Abeam position. + self:_Abeam(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.NINETY then + + -- CASE:I/II: Check long down wind leg. + self:_CheckForLongDownwind(playerData) + + -- At the ninety. + self:_Ninety(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.WAKE then + + -- CASE I/II: In the wake. + self:_Wake(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.FINAL then + + -- CASE I/II: Turn to final and enter the groove. + self:_Final(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.GROOVE_XX or + playerData.step==AIRBOSS.PatternStep.GROOVE_IM or + playerData.step==AIRBOSS.PatternStep.GROOVE_IC or + playerData.step==AIRBOSS.PatternStep.GROOVE_AR or + playerData.step==AIRBOSS.PatternStep.GROOVE_IW then + + -- CASE I/II: In the groove. + self:_Groove(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.DEBRIEF then + + -- Debriefing in 5 seconds. + SCHEDULER:New(nil, self._Debrief, {self, playerData}, 5) + + -- Undefined status. + playerData.step=AIRBOSS.PatternStep.UNDEFINED + + else + + -- Error, unknown step! + self:E(self.lid..string.format("ERROR: Unknown player step %s. Please report!", tostring(playerData.step))) + + end + + -- Check if player missed a step during Case II/III and allow him to enter the landing pattern. + self:_CheckMissedStepOnEntry(playerData) + + else + self:T2(self.lid.."WARNING: Player unit not inside the CCA!") + end + + else + -- Unit not alive. + self:T(self.lid.."WARNING: Player unit is not alive!") + end + end + end + +end + + +--- Checks if a player is in the pattern queue and has missed a step in Case II/III approach. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_CheckMissedStepOnEntry(playerData) + + -- Conditions to be met: Case II/III, in pattern queue, flag!=42 (will be set to 42 at the end if player missed a step). + local rightcase=playerData.case>1 + local rightqueue=self:_InQueue(self.Qpattern, playerData.group) + local rightflag=playerData.flag:Get()~=-42 + + -- Steps that the player could have missed during Case II/III. + local step=playerData.step + local missedstep=step==AIRBOSS.PatternStep.PLATFORM or step==AIRBOSS.PatternStep.ARCIN or step==AIRBOSS.PatternStep.ARCOUT or step==AIRBOSS.PatternStep.DIRTYUP + + -- Check if player is about to enter the initial or bullseye zones and maybe has missed a step in the pattern. + if rightcase and rightqueue and rightflag then + + -- Get right zone. + local zone=nil + if playerData.case==2 and missedstep then + + zone=self:_GetZoneInitial(playerData.case) + + elseif playerData.case==3 and missedstep then + + zone=self:_GetZoneBullseye(playerData.case) + + end + + -- Zone only exists if player is not at the initial or bullseye step. + if zone then + + -- Check if player is in initial or bullseye zone. + local inzone=playerData.unit:IsInZone(zone) + + -- Relative heading to carrier direction. + local relheading=self:_GetRelativeHeading(playerData.unit, false) + + -- Check if player is in zone and flying roughly in the right direction. + if inzone and math.abs(relheading)<60 then + + -- Player is in one of the initial zones short before the landing pattern. + local text=string.format("you missed an important step in the pattern!\nYour next step would have been %s.", playerData.step) + self:MessageToPlayer(playerData, text, "AIRBOSS", nil, 5) + + if playerData.case==2 then + -- Set next step to initial. + playerData.step=AIRBOSS.PatternStep.INITIAL + elseif playerData.case==3 then + -- Set next step to bullseye. + playerData.step=AIRBOSS.PatternStep.BULLSEYE + end + + -- Set flag value to -42. This is the value to ensure that this routine is not called again! + playerData.flag:Set(-42) + end + end + end +end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- EVENT functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Airboss event handler for event birth. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +function AIRBOSS:OnEventBirth(EventData) + self:F3({eventbirth = EventData}) + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + self:T2(self.lid.."BIRTH: unit = "..tostring(EventData.IniUnitName)) + self:T2(self.lid.."BIRTH: group = "..tostring(EventData.IniGroupName)) + self:T2(self.lid.."BIRTH: player = "..tostring(_playername)) + + if _unit and _playername then + + local _uid=_unit:GetID() + local _group=_unit:GetGroup() + local _callsign=_unit:GetCallsign() + + -- Debug output. + local text=string.format("Pilot %s, callsign %s entered unit %s of group %s.", _playername, _callsign, _unitName, _group:GetName()) + self:T(self.lid..text) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + + -- Check if aircraft type the player occupies is carrier capable. + local rightaircraft=self:_IsCarrierAircraft(_unit) + if rightaircraft==false then + local text=string.format("Player aircraft type %s not supported by AIRBOSS class.", _unit:GetTypeName()) + MESSAGE:New(text, 30):ToAllIf(self.Debug) + self:T2(self.lid..text) + return + end + + -- Check that coalition of the carrier and aircraft match. + if self:GetCoalition()~=_unit:GetCoalition() then + local text=string.format("Player entered aircraft of other coalition.") + MESSAGE:New(text, 30):ToAllIf(self.Debug) + self:T(self.lid..text) + return + end + + -- Add Menu commands. + self:_AddF10Commands(_unitName) + + -- Delaying the new player for a second, because AI units of the flight would not be registered correctly. + SCHEDULER:New(nil, self._NewPlayer, {self, _unitName}, 1) + + end +end + +--- Airboss event handler for event land. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +function AIRBOSS:OnEventLand(EventData) + self:F3({eventland = EventData}) + + -- Get unit name that landed. + local _unitName=EventData.IniUnitName + + -- Check if this was a player. + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + -- Debug output. + self:T3(self.lid.."LAND: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.lid.."LAND: group = "..tostring(EventData.IniGroupName)) + self:T3(self.lid.."LAND: player = "..tostring(_playername)) + + -- This would be the closest airbase. + local airbase=EventData.Place + + -- Nil check for airbase. Crashed as player gave me no airbase. + if airbase==nil then + return + end + + -- Get airbase name. + local airbasename=tostring(airbase:GetName()) + + -- Check if aircraft landed on the right airbase. + if airbasename==self.airbase:GetName() then + + -- Stern coordinate at the rundown. + local stern=self:_GetSternCoord() + + -- Polygon zone close around the carrier. + local zoneCarrier=self:_GetZoneCarrierBox() + + -- Check if player or AI landed. + if _unit and _playername then + -- Human Player landed. + + -- Get info. + local _uid=_unit:GetID() + local _group=_unit:GetGroup() + local _callsign=_unit:GetCallsign() + + -- Debug output. + local text=string.format("Player %s, callsign %s unit %s (ID=%d) of group %s landed at airbase %s", _playername, _callsign, _unitName, _uid, _group:GetName(), airbasename) + self:T(self.lid..text) + MESSAGE:New(text, 5, "DEBUG"):ToAllIf(self.Debug) + + -- Player data. + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + -- Check if playerData is okay. + if playerData==nil then + self:E(self.lid.."ERROR: playerData nil in landing event.") + return + end + + -- Check that player landed on the carrier. + if _unit:IsInZone(zoneCarrier) then + + -- Check if this was a valid approach. + if not playerData.valid then + -- Player missed at least one step in the pattern. + local text=string.format("you missed at least one important step in the pattern!\nYour next step would have been %s.\nThis pass is INVALID.", playerData.step) + self:MessageToPlayer(playerData, text, "AIRBOSS", nil, 30, true, 5) + + -- Reinitialize player data. + self:_InitPlayer(playerData) + + -- Clear queues just in case. + self:_RemoveFlightFromMarshalQueue(playerData, true) + self:_RemoveFlightFromQueue(self.Qpattern, playerData) + + return + end + + -- Check if player already landed. We dont need a second time. + if playerData.landed then + + self:E(self.lid..string.format("Player %s just landed a second time.", _playername)) + + else + + -- Coordinate at landing event. + local coord=playerData.unit:GetCoordinate() + + -- Get distances relative to + local X,Z,rho,phi=self:_GetDistances(_unit) + + -- Landing distance wrt to stern position. + local dist=coord:Get2DDistance(stern) + + -- Debug mark of player landing coord. + if self.Debug and false then + -- Debug mark of player landing coord. + local lp=coord:MarkToAll("Landing coord.") + coord:SmokeGreen() + end + + -- Get wire. We additionally shift the landing coord back because landing event for players is unfortunately delayed. + local wire=self:_GetWire(coord, 75) + + -- No wire ==> Bolter, Bolter radio call. + -- TODO: might need a better place for this. or check + if wire>4 then + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.BOLTER) + end + + -- Get time in the groove. + local gdataX0=playerData.groove.X0 --#AIRBOSS.GrooveData + if gdataX0 then + playerData.Tgroove=timer.getTime()-gdataX0.TGroove + else + playerData.Tgroove=999 + end + + -- Debug text. + local text=string.format("Player %s AC type %s landed at dist=%.1f m. Trapped wire=%d.", playerData.name, playerData.actype, dist, wire) + text=text..string.format(" X=%.1f m, Z=%.1f m, rho=%.1f m.", X, Z, rho) + self:T(self.lid..text) + + -- We did land. + playerData.landed=true + + -- Unkonwn step until we now more. + playerData.step=AIRBOSS.PatternStep.UNDEFINED + + -- Switch attitude monitor off if on. + playerData.attitudemonitor=false + + -- Call trapped function in 1 second to make sure we did not bolter. + SCHEDULER:New(nil, self._Trapped, {self, playerData}, 1) + + end + + else + -- TODO: Handle case where player did not land on the carrier. + if playerData then + self:E(self.lid..string.format("Player %s did not land in carrier box zone. Maybe in the water near the carrier?", playerData.name)) + end + end + + else + + -- AI unit landed. + + -- Coordinate at landing event + local coord=EventData.IniUnit:GetCoordinate() + + -- Debug mark of player landing coord. + local dist=coord:Get2DDistance(self:GetCoordinate()) + + -- Get wire + local wire=self:_GetWire(coord, 0) + + -- Aircraft type. + local _type=EventData.IniUnit:GetTypeName() + + -- Debug text. + local text=string.format("AI unit %s of type %s landed at dist=%.1f m. Trapped wire=%d.", _unitName, _type, dist, wire) + self:T(self.lid..text) + + -- AI always lands ==> remove unit from flight group and queues. + self:_RemoveUnitFromFlight(EventData.IniUnit) + end + end +end + +--- Airboss event handler for event crash. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +function AIRBOSS:OnEventCrash(EventData) + self:F3({eventland = EventData}) + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + self:T3(self.lid.."CRASH: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.lid.."CRASH: group = "..tostring(EventData.IniGroupName)) + self:T3(self.lid.."CARSH: player = "..tostring(_playername)) + + if _unit and _playername then + -- Debug message. + self:T(self.lid..string.format("Player %s crashed!",_playername)) + + -- Get player flight. + local flight=self.players[_playername] + + -- Remove flight completely from all queues and collapse marshal if necessary. + if flight then + self:_RemoveFlight(flight, true) + end + + -- Remove all grades until a final grade is reached. + local grades=self.playerscores[_playername] + if grades and #grades>0 then + while #grades>0 and grades[#grades].finalscore==nil do + table.remove(grades, #grades) + end + end + + else + -- Debug message. + self:T2(self.lid..string.format("AI unit %s crashed!", EventData.IniUnitName)) + + -- Remove unit from flight and queues. + self:_RemoveUnitFromFlight(EventData.IniUnit) + end + +end + +--- Airboss event handler for event Ejection. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +function AIRBOSS:OnEventEjection(EventData) + self:F3({eventland = EventData}) + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + self:T3(self.lid.."EJECT: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.lid.."EJECT: group = "..tostring(EventData.IniGroupName)) + self:T3(self.lid.."EJECT: player = "..tostring(_playername)) + + if _unit and _playername then + self:T(self.lid..string.format("Player %s ejected!",_playername)) + -- Get player flight. + local flight=self.players[_playername] + + -- Remove flight completely from all queues and collapse marshal if necessary. + if flight then + self:_RemoveFlight(flight, true) + end + + -- Remove all grades until a final grade is reached. + local grades=self.playerscores[_playername] + if grades and #grades>0 then + while #grades>0 and grades[#grades].finalscore==nil do + table.remove(grades, #grades) + end + end + + else + -- Debug message. + self:T2(self.lid..string.format("AI unit %s ejected!", EventData.IniUnitName)) + + -- Remove unit from flight and queues. + self:_RemoveUnitFromFlight(EventData.IniUnit) + end + +end + +--- Airboss event handler for event player leave unit. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +--function AIRBOSS:OnEventPlayerLeaveUnit(EventData) +function AIRBOSS:_PlayerLeft(EventData) + self:F3({eventleave=EventData}) + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + self:T3(self.lid.."PLAYERLEAVEUNIT: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.lid.."PLAYERLEAVEUNIT: group = "..tostring(EventData.IniGroupName)) + self:T3(self.lid.."PLAYERLEAVEUNIT: player = "..tostring(_playername)) + + if _unit and _playername then + + -- Debug info. + self:T(self.lid..string.format("Player %s left unit %s!",_playername, _unitName)) + + -- Get player flight. + local flight=self.players[_playername] + + -- Remove flight completely from all queues and collapse marshal if necessary. + if flight then + self:_RemoveFlight(flight, true) + end + + -- Remove all grades until a final grade is reached. + local grades=self.playerscores[_playername] + if grades and #grades>0 then + while #grades>0 and grades[#grades].finalscore==nil do + table.remove(grades, #grades) + end + end + + end + +end + +--[[ +--- Airboss event function handling the mission end event. +-- Handles the case when the mission is ended. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData Event data. +function AIRBOSS:OnEventMissionEnd(EventData) + + -- Auto save player results. + if self.autosave then + self:Save(self.autosavepath, self.autosavefile) + end +end +]] + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- PATTERN functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Waiting outside 10 NM zone for free Marshal stack. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_Waiting(playerData) + + -- Create 10 NM zone around the carrier. + local radius=UTILS.NMToMeters(10) + local zone=ZONE_RADIUS:New("Carrier 10 NM Zone", self.carrier:GetVec2(), radius) + + -- Check if player is inside 10 NM radius of the carrier. + local inzone=playerData.unit:IsInZone(zone) + + -- Time player is waiting. + local Twaiting=timer.getAbsTime()-playerData.time + + -- Warning if player is inside the zone. + if inzone and Twaiting>3*60 and not playerData.warning then + local text=string.format("You are supposed to wait outside the 10 NM zone.") + self:MessageToPlayer(playerData, text, "AIRBOSS") + playerData.warning=true + end + + -- Reset warning. + if inzone==false and playerData.warning==true then + playerData.warning=nil + end + +end + +--- Holding. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_Holding(playerData) + + -- Player unit and flight. + local unit=playerData.unit + + -- Current stack. + local stack=playerData.flag:Get() + + --------------------------- + -- Holding Pattern Check -- + --------------------------- + + -- Pattern altitude. + local patternalt=self:_GetMarshalAltitude(stack, playerData.case) + + -- Player altitude. + local playeralt=unit:GetAltitude() + + -- Get holding zone of player. + local zoneHolding=self:_GetZoneHolding(playerData.case, stack) + + -- Check if player is in holding zone. + local inholdingzone=unit:IsInZone(zoneHolding) + + -- Altitude difference between player and assigned stack. + local altdiff=playeralt-patternalt + + -- Acceptable altitude depending on player skill. + local altgood=UTILS.FeetToMeters(500) + if playerData.difficulty==AIRBOSS.Difficulty.HARD then + -- Pros can be expected to be within +-200 ft. + altgood=UTILS.FeetToMeters(200) + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + -- Normal guys should be within +-350 ft. + altgood=UTILS.FeetToMeters(350) + elseif playerData.difficulty==AIRBOSS.Difficulty.EASY then + -- Students should be within +-500 ft. + altgood=UTILS.FeetToMeters(500) + end + + -- When back to good altitude = 50%. + local altback=altgood*0.5 + + -- Check if stack just collapsed and give the player one minute to change the altitude. + local justcollapsed=false + if self.Tcollapse then + -- Time since last stack change. + local dT=timer.getTime()-self.Tcollapse + + -- TODO: check if this works. + --local dT=timer.getAbsTime()-playerData.time + + -- Check if less then 90 seconds. + if dT<=90 then + justcollapsed=true + end + end + + -- Check if altitude is acceptable. + local goodalt=math.abs(altdiff)altgood then + + -- Issue warning for being too high. + if not playerData.warning then + text=text..string.format("You left your assigned altitude. Descent to angels %d.", angels) + playerData.warning=true + end + + elseif altdiff<-altgood then + + -- Issue warning for being too low. + if not playerData.warning then + text=text..string.format("You left your assigned altitude. Climb to angels %d.", angels) + playerData.warning=true + end + + end + + end + + -- Back to assigned altitude. + if playerData.warning and math.abs(altdiff)<=altback then + text=text..string.format("Altitude is looking good again.") + playerData.warning=nil + end + + elseif playerData.holding==false then + + -- Player left holding zone + if inholdingzone then + -- Player is back in the holding zone. + self:T("Player is back in the holding zone after leaving it.") + text=text..string.format("You are back in the holding zone. Now stay there!") + playerData.holding=true + else + -- Player is still outside the holding zone. + self:T2("Player still outside the holding zone. What are you doing man?!") + end + + elseif playerData.holding==nil then + -- Player did not entered the holding zone yet. + + if inholdingzone then + + -- Player arrived in holding zone. + playerData.holding=true + + -- Debug output. + self:T("Player entered the holding zone for the first time.") + + -- Inform player. + text=text..string.format("You arrived at the holding zone.") + + -- Feedback on altitude. + if goodalt then + text=text..string.format(" Altitude is good.") + else + if altdiff<0 then + text=text..string.format(" But you're too low.") + else + text=text..string.format(" But you're too high.") + end + text=text..string.format("\nCurrently assigned altitude is %d ft.", UTILS.MetersToFeet(patternalt)) + playerData.warning=true + end + + -- No info for the pros. + if playerData.difficulty==AIRBOSS.Difficulty.HARD then + text="" + end + + else + -- Player did not yet arrive in holding zone. + self:T2("Waiting for player to arrive in the holding zone.") + end + + end + + -- Send message. + self:MessageToPlayer(playerData, text, "AIRBOSS", nil, 5) +end + + +--- Commence approach. This step initializes the player data. Section members are also set to commence. Next step depends on recovery case: +-- +-- * Case 1: Initial +-- * Case 2/3: Platform +-- +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #boolean zonecheck If true, zone is checked before player is released. +function AIRBOSS:_Commencing(playerData, zonecheck) + + -- Check for auto commence + if zonecheck then + + -- Get auto commence zone. + local zoneCommence=self:_GetZoneCommence(playerData.case) + + -- Check if unit is in the zone. + local inzone=playerData.unit:IsInZone(zoneCommence) + + -- Skip the rest if not in the zone yet. + if not inzone then + return + end + + end + + -- Remove flight from Marshal queue. If flight was in queue, stack is collapsed and flight added to the pattern queue. + self:_RemoveFlightFromMarshalQueue(playerData) + + -- Initialize player data for new approach. + self:_InitPlayer(playerData) + + -- Commencing message to player only. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Text + local text="" + + -- Positive response. + if playerData.case==1 then + text=text.."Proceed to initial." + else + text=text.."Descent to platform." + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + text=text.." VSI 4000 ft/min until you reach 5000 ft." + end + end + + -- Message to player. + --self:MessageToPlayer(playerData, text, "MARSHAL", nil, 3) + self:MessageToPlayer(playerData, text, "MARSHAL") + end + + -- Next step: depends on case recovery. + local nextstep + if playerData.case==1 then + -- CASE I: Player has to fly to the initial which is 3 NM DME astern of the boat. + nextstep=AIRBOSS.PatternStep.INITIAL + else + -- CASE II/III: Player has to start the descent at 4000 ft/min to the platform at 5k ft. + nextstep=AIRBOSS.PatternStep.PLATFORM + end + + -- Next step hint. + self:_SetPlayerStep(playerData, nextstep) + + -- Commence section members as well but dont check the zone. + for i,_flight in pairs(playerData.section) do + local flight=_flight --#AIRBOSS.PlayerData + self:_Commencing(flight, false) + end + +end + +--- Start pattern when player enters the initial zone in case I/II recoveries. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @return #boolean True if player is in the inital zone. +function AIRBOSS:_Initial(playerData) + + -- Check if player is in initial zone and entering the CASE I pattern. + local inzone=playerData.unit:IsInZone(self:_GetZoneInitial(playerData.case)) + + -- Relative heading to carrier direction. + local relheading=self:_GetRelativeHeading(playerData.unit, false) + + -- Check if player is in zone and flying roughly in the right direction. + if inzone and math.abs(relheading)<60 then + + -- Send message for normal and easy difficulty. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Inform player. + local hint=string.format("Initial") + + -- Hook down for students. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + hint=hint.." - Hook down!" + end + + self:MessageToPlayer(playerData, hint, "MARSHAL") + end + + -- Next step: Break entry. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.BREAKENTRY) + + return true + end + + return false +end + +--- Check if player is in CASE II/III approach corridor. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_CheckCorridor(playerData) + + -- Check if player is in valid zone + local validzone=self:_GetZoneCorridor(playerData.case) + + -- Check if we are inside the moving zone. + local invalid=playerData.unit:IsNotInZone(validzone) + + -- Issue warning. + if invalid and (not playerData.warning) then + self:MessageToPlayer(playerData, "You left the approach corridor!", "AIRBOSS") + playerData.warning=true + end + + -- Back in zone. + if (not invalid) and playerData.warning then + self:MessageToPlayer(playerData, "You're back in the approach corridor.", "AIRBOSS") + playerData.warning=false + end + +end + +--- Platform at 5k ft for case II/III recoveries. Descent at 2000 ft/min. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Platform(playerData) + + -- Check if player left or got back to the approach corridor. + self:_CheckCorridor(playerData) + + -- Check if we are inside the moving zone. + local inzone=playerData.unit:IsInZone(self:_GetZonePlatform(playerData.case)) + + -- Check if we are in zone. + if inzone then + + -- Debug message. + MESSAGE:New("Platform step reached", 5, "DEBUG"):ToAllIf(self.Debug) + + -- Get optimal altitiude. + local altitude, aoa, distance, speed =self:_GetAircraftParameters(playerData) + + -- Get altitude hint. + local hintAlt=self:_AltitudeCheck(playerData, altitude) + + -- Get altitude hint. + local hintSpeed=self:_SpeedCheck(playerData, speed) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Altitude and speed hint. + local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintSpeed) + + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end + + -- Next step: depends. + local nextstep + if math.abs(self.holdingoffset)>0 and playerData.case>1 then + -- Turn to BRC (case II) or FB (case III). + nextstep=AIRBOSS.PatternStep.ARCIN + else + if playerData.case==2 then + -- Case II: Initial zone then Case I recovery. + nextstep=AIRBOSS.PatternStep.INITIAL + elseif playerData.case==3 then + -- CASE III: Dirty up. + nextstep=AIRBOSS.PatternStep.DIRTYUP + end + end + + -- Next step hint. + self:_SetPlayerStep(playerData, nextstep) + + end +end + + +--- Arc in turn for case II/III recoveries. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_ArcInTurn(playerData) + + -- Check if player left or got back to the approach corridor. + self:_CheckCorridor(playerData) + + -- Check if we are inside the moving zone. + local inzone=playerData.unit:IsInZone(self:_GetZoneArcIn(playerData.case)) + + if inzone then + + -- Debug message. + MESSAGE:New("Arc Turn In step reached", 5, "DEBUG"):ToAllIf(self.Debug) + + -- Get optimal altitiude. + local altitude, aoa, distance, speed=self:_GetAircraftParameters(playerData) + + -- Get speed hint. + local hintSpeed=self:_SpeedCheck(playerData, speed) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Hint speed. + local hint=string.format("%s\n%s", playerData.step, hintSpeed) + + -- Hint turn and set TACAN. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + -- Get inverse magnetic radial without offset ==> FB for Case II or BRC for Case III. + local radial=self:GetRadial(playerData.case, true, false, true) + local turn="right" + if self.holdingoffset<0 then + turn="left" + end + hint=hint..string.format("\nTurn %s and select TACAN %03d°.", turn, radial) + end + + -- Message to player. + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end + + -- Next step: Arc Out Turn. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.ARCOUT) + + end +end + +--- Arc out turn for case II/III recoveries. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_ArcOutTurn(playerData) + + -- Check if player left or got back to the approach corridor. + self:_CheckCorridor(playerData) + + -- Check if we are inside the moving zone. + local inzone=playerData.unit:IsInZone(self:_GetZoneArcOut(playerData.case)) + + --if self:_CheckLimits(X, Z, self.DirtyUp) then + if inzone then + + -- Debug message. + MESSAGE:New("Arc Turn Out step reached", 5, "DEBUG"):ToAllIf(self.Debug) + + -- Get optimal altitiude. + local altitude, aoa, distance, speed=self:_GetAircraftParameters(playerData) + + -- Get speed hint. + local hintSpeed=self:_SpeedCheck(playerData, speed) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s\n%s", playerData.step, hintSpeed) + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end + + -- Next step: + local nextstep + if playerData.case==3 then + -- Case III: Dirty up. + nextstep=AIRBOSS.PatternStep.DIRTYUP + else + -- Case II: Initial. + nextstep=AIRBOSS.PatternStep.INITIAL + end + + -- Next step hint. + self:_SetPlayerStep(playerData, nextstep) + end +end + +--- Dirty up and level out at 1200 ft for case III recovery. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_DirtyUp(playerData) + + -- Check if player left or got back to the approach corridor. + self:_CheckCorridor(playerData) + + -- Check if we are inside the moving zone. + local inzone=playerData.unit:IsInZone(self:_GetZoneDirtyUp(playerData.case)) + + if inzone then + + -- Debug message. + MESSAGE:New("Dirty up step reached", 5, "DEBUG"):ToAllIf(self.Debug) + + -- Get optimal altitiude. + local altitude, aoa, distance, speed=self:_GetAircraftParameters(playerData) + + -- Get altitude hint. + local hintAlt, debrief=self:_AltitudeCheck(playerData, altitude) + + -- Get speed hint. + -- TODO: Not sure if we already need to be onspeed AoA at this point? + local hintSpeed=self:_SpeedCheck(playerData, speed) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Hint alt and speed. + local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintSpeed) + + -- Hint turn and set TACAN. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + hint=hint.."\nDirty up! Hook, gear and flaps down." + end + + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end + + -- Radio call "Say/Fly needles". Delayed by 10/15 seconds. + local callsay=self:_NewRadioCall(AIRBOSS.MarshalCall.SAYNEEDLES, nil, nil, 5, playerData.onboard) + local callfly=self:_NewRadioCall(AIRBOSS.MarshalCall.FLYNEEDLES, nil, nil, 5, playerData.onboard) + self:RadioTransmission(self.MarshalRadio, callsay, false, 40) + self:RadioTransmission(self.MarshalRadio, callfly, false, 45) + + -- TODO: Make Fly Bullseye call if no automatic ICLS is active. + + -- Next step: CASE III: Intercept glide slope and follow bullseye (ICLS). + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.BULLSEYE) + + end +end + +--- Intercept glide slop and follow ICLS, aka Bullseye for case III recovery. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @return #boolean If true, player is in bullseye zone. +function AIRBOSS:_Bullseye(playerData) + + -- Check if player left or got back to the approach corridor. + self:_CheckCorridor(playerData) + + -- Check if we are inside the moving zone. + local inzone=playerData.unit:IsInZone(self:_GetZoneBullseye(playerData.case)) + + -- Relative heading to carrier direction of the runway. + local relheading=self:_GetRelativeHeading(playerData.unit, true) + + -- Check if player is in zone and flying roughly in the right direction. + if inzone and math.abs(relheading)<60 then + + -- Debug message. + MESSAGE:New("Bullseye step reached", 5, "DEBUG"):ToAllIf(self.Debug) + + -- Get optimal altitiude. + local altitude, aoa, distance, speed=self:_GetAircraftParameters(playerData) + + -- Get altitude hint. + local hintAlt=self:_AltitudeCheck(playerData, altitude) + + -- Get altitude hint. + local hintAoA=self:_AoACheck(playerData, aoa) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Hint alt and aoa. + local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintAoA) + + -- Hint follow the needles. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + hint=hint..string.format("Intercept glideslope and follow the needles.") + end + + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end + + -- Next step: Groove Call the ball. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_XX) + + end +end + +--- Bolter pattern. Sends player to abeam for Case I/II or Bullseye for Case III ops. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_BolterPattern(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z, rho, phi=self:_GetDistances(playerData.unit) + + -- Bolter Pattern thresholds. + local Bolter={} + Bolter.name="Bolter Pattern" + Bolter.Xmin=-UTILS.NMToMeters(5) -- Not more then 5 NM astern of boat. + Bolter.Xmax= UTILS.NMToMeters(3) -- Not more then 3 NM ahead of boat. + Bolter.Zmin=-UTILS.NMToMeters(5) -- Not more than 2 NM port. + Bolter.Zmax= UTILS.NMToMeters(1) -- Not more than 1 NM starboard. + Bolter.LimitXmin= 100 -- Check that 100 meter ahead and port + Bolter.LimitXmax= nil + Bolter.LimitZmin= nil + Bolter.LimitZmax= nil + + -- Check if we are in front of the boat (diffX > 0). + if self:_CheckLimits(X, Z, Bolter) then + local nextstep + if playerData.case<3 then + nextstep=AIRBOSS.PatternStep.ABEAM + else + nextstep=AIRBOSS.PatternStep.BULLSEYE + end + self:_SetPlayerStep(playerData, nextstep) + + end +end + +--- Break entry for case I/II recoveries. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_BreakEntry(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z, rho, phi=self:_GetDistances(playerData.unit) + + -- Abort condition check. + if self:_CheckAbort(X, Z, self.BreakEntry) then + self:_AbortPattern(playerData, X, Z, self.BreakEntry, true) + return + end + + -- Check if we are in front of the boat (diffX > 0). + if self:_CheckLimits(X, Z, self.BreakEntry) then + + -- Get optimal altitude, distance and speed. + local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) + + -- Get altitude hint. + local hintAlt=self:_AltitudeCheck(playerData, alt) + + -- Get speed hint. + local hintSpeed=self:_SpeedCheck(playerData, speed) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintSpeed) + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end + + -- Next step: Early Break. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.EARLYBREAK) + + end +end + + +--- Break. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #string part Part of the break. +function AIRBOSS:_Break(playerData, part) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z, rho, phi=self:_GetDistances(playerData.unit) + + -- Early or late break. + local breakpoint = self.BreakEarly + if part==AIRBOSS.PatternStep.LATEBREAK then + breakpoint = self.BreakLate + end + + -- Check abort conditions. + if self:_CheckAbort(X, Z, breakpoint) then + self:_AbortPattern(playerData, X, Z, breakpoint, true) + return + end + + -- Check limits. + if self:_CheckLimits(X, Z, breakpoint) then + + -- Get optimal altitude, distance and speed. + local altitude=self:_GetAircraftParameters(playerData) + + -- Grade altitude. + local hint, debrief=self:_AltitudeCheck(playerData, altitude) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Hint alt. + local hint=string.format("%s %s", playerData.step, hint) + + -- Hint dirty up. + if playerData.difficult==AIRBOSS.Difficulty.EASY and part==AIRBOSS.PatternStep.LATEBREAK then + hint=hint.."\nDirty up! Gear down, flaps down. Check hook down." + end + + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end + + -- Debrief + self:_AddToDebrief(playerData, debrief) + + -- Next step: Late Break or Abeam. + local nextstep + if part==AIRBOSS.PatternStep.EARLYBREAK then + nextstep=AIRBOSS.PatternStep.LATEBREAK + else + nextstep=AIRBOSS.PatternStep.ABEAM + end + + self:_SetPlayerStep(playerData, nextstep) + end +end + +--- Long downwind leg check. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_CheckForLongDownwind(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z=self:_GetDistances(playerData.unit) + + -- One NM from carrier is too far. + local limit=UTILS.NMToMeters(-1.5) + + -- Check we are not too far out w.r.t back of the boat. + if X90 and self:_CheckLimits(X, Z, self.Wake) then + -- Message to player. + self:MessageToPlayer(playerData, "You are already at the wake and have not passed the 90. Turn faster next time!", "LSO") + --TODO: pattern WO? + end +end + +--- At the Wake. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Wake(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z = self:_GetDistances(playerData.unit) + + -- Check abort conditions. + if self:_CheckAbort(X, Z, self.Wake) then + self:_AbortPattern(playerData, X, Z, self.Wake, true) + return + end + + -- Right behind the wake of the carrier dZ>0. + if self:_CheckLimits(X, Z, self.Wake) then + + -- Get optimal altitude, distance and speed. + local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) + + -- Grade altitude. + local hintAlt, debriefAlt=self:_AltitudeCheck(playerData, alt) + + -- Grade AoA. + local hintAoA, debriefAoA=self:_AoACheck(playerData, aoa) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintAoA) + self:MessageToPlayer(playerData, hint, "LSO", "") + end + + -- Debrief. + local debrief=string.format("%s\n%s", debriefAlt, debriefAoA) + + -- Add to debrief. + self:_AddToDebrief(playerData, debrief) + + -- Next step: Final. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.FINAL) + + end +end + +--- Turn to final. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Final(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z, rho, phi = self:_GetDistances(playerData.unit) + + -- In front of carrier or more than 4 km behind carrier. + if self:_CheckAbort(X, Z, self.Final) then + self:_AbortPattern(playerData, X, Z, self.Final, true) + return + end + + -- Relative heading 0=fly parallel +-90=fly perpendicular + local relhead=self:_GetRelativeHeading(playerData.unit, true) + + -- Line up wrt runway. + local lineup=self:_Lineup(playerData.unit, true) + + -- Player's angle of bank. + local roll=playerData.unit:GetRoll() + + -- Check if player is in +-5 deg cone and flying towards the runway. + if math.abs(lineup)<5 then --and math.abs(relhead)<5 then + + -- Get optimal altitude, distance and speed. + local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) + + -- Grade altitude. + local hintAlt, debriefAlt=self:_AltitudeCheck(playerData, alt) + + -- AoA feed back + local hintAoA, debriefAoA=self:_AoACheck(playerData, aoa) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintAoA) + self:MessageToPlayer(playerData, hint, "LSO", "") + end + + -- Add to debrief. + local debrief=string.format("%s\n%s", debriefAlt, debriefAoA) + self:_AddToDebrief(playerData, debrief) + + -- Gather pilot data. + local groovedata={} --#AIRBOSS.GrooveData + groovedata.Step=playerData.step + groovedata.Alt=alt + groovedata.AoA=aoa + groovedata.GSE=self:_Glideslope(playerData.unit, 3.5) + groovedata.LUE=self:_Lineup(playerData.unit, true) + groovedata.Roll=roll + groovedata.Rhdg=relhead + groovedata.TGroove=timer.getTime() + groovedata.FlyThrough=nil + + -- TODO: could add angled approach if lineup<5 and relhead>5. This would mean the player has not turned in correctly! + + -- Groove data. + playerData.groove.X0=groovedata + + -- Next step: X start. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_XX) + end + +end + +--- In the groove. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Groove(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z = self:_GetDistances(playerData.unit) + + -- Player altitude + local alt=playerData.unit:GetAltitude() + + -- Player group. + local player=playerData.unit:GetGroup() + + -- Check abort conditions. + if self:_CheckAbort(X, Z, self.Groove) then + self:_AbortPattern(playerData, X, Z, self.Groove, true) + return + end + + -- Stern position at the rundown. + local stern=self:_GetSternCoord() + + -- Distance from rundown to player aircraft. + local rho=stern:Get2DDistance(playerData.unit:GetCoordinate()) + + -- Lineup with runway centerline. + local lineupError=self:_Lineup(playerData.unit, true) + + -- Glideslope. + local glideslopeError=self:_Glideslope(playerData.unit, 3.5) + + -- Get AoA. + local AoA=playerData.unit:GetAoA() + + -- Aircraft is behind the carrier. + local astern=X=RAR and rho<=RIC and not playerData.waveoff then + + -- Check if player should wave off. + local waveoff=self:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) + + -- Let's see.. + if waveoff then + + -- Debug info. + self:T3(self.lid..string.format("Waveoff distance rho=%.1f m", rho)) + + -- LSO Wave off! + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.WAVEOFF) + playerData.Tlso=timer.getTime() + + -- Player was waved off! + playerData.waveoff=true + + -- Nothing else necessary. + return + end + + end + + ----------------- + -- Groove Data -- + ----------------- + + -- Get groove step short hand of the previous step. + local gs=self:_GS(playerData.step, -1) + + -- For debugging. + local text=string.format("Groove %s: LineUp=%.2f GlideSlope=%.2f AoA=%.2f\n", gs, lineupError, glideslopeError, AoA) + text=text..string.format("R=%.1f m, h=%.1f m", rho, alt-self.carrierparam.deckheight-2) + --MESSAGE:New(text, 1, nil, true):ToAllIf(self.Debug) + + -- Check if we are beween 3/4 NM and end of ship. + if rho>=RAR and rhomath.abs(gd.LUE) then + self:T(self.lid..string.format("Got bigger Linue up error at %s: LUE %.3f>%.3f.", gs, lineupError, gd.LUE)) + gd.LUE=lineupError + end + + -- Fly through good window of glideslope. + if gd.GSE>0.4 and glideslopeError<-0.3 then + -- Fly through down ==> "\" + gd.FlyThrough="\\" + self:T(self.lid..string.format("Got Fly through DOWN at %s. Max GSE=%.1f, lower GSE=%.1f", gs, gd.GSE, glideslopeError)) + elseif gd.GSE<-0.3 and glideslopeError>0.4 then + -- Fly through up ==> "/" + gd.FlyThrough="/" + self:T(self.lid..string.format("Got Fly through UP at %s. Min GSE=%.1f, lower GSE=%.1f", gs, gd.GSE, glideslopeError)) + end + + -- Update max deviation of glideslope error. + if math.abs(glideslopeError)>math.abs(gd.GSE) then + self:T(self.lid..string.format("Got bigger glideslope error at %s: GSE |%.3f|>|%.3f|.", gs, glideslopeError, gd.GSE)) + gd.GSE=glideslopeError + end + + -- Get current AoA. + local aoa=playerData.unit:GetAoA() + + -- Get aircraft AoA parameters. + local aircraftaoa=self:_GetAircraftAoA(playerData) + + -- On Speed AoA. + local aoaopt=aircraftaoa.OnSpeed + + -- Compare AoAs wrt on speed AoA and update max deviation. + if math.abs(aoa-aoaopt)>math.abs(gd.AoA-aoaopt) then + self:T(self.lid..string.format("Got bigger AoA error at %s: AoA %.3f>%.3f.", gs, aoa, gd.AoA)) + gd.AoA=aoa + end + + end + + --------------- + -- LSO Calls -- + --------------- + + -- Time since last LSO call. + local deltaT=timer.getTime()-playerData.Tlso + + -- LSO call if necessary. + if deltaT>=self.LSOdT then + self:_LSOadvice(playerData, glideslopeError, lineupError) + end + + end + + ---------------------------------------------------------- + --- Some time here the landing event MIGHT be triggered -- + ---------------------------------------------------------- + + -- Player infront of the carrier X>~77 m. + if X>self.carrierparam.totlength+self.carrierparam.sterndist then + + if playerData.waveoff then + + if playerData.landed then + -- This should not happen because landing event was triggered. + self:_AddToDebrief(playerData, "You were waved off but landed anyway. Airboss wants to talk to you!") + else + self:_AddToDebrief(playerData, "You were waved off.") + end + + elseif playerData.boltered then + + -- This should not happen because landing event was triggered. + self:_AddToDebrief(playerData, "You boltered.") + + else + + -- This should not happen. + self:E("What? Player was not waved off but flew past the carrier without landing. Why did waveoff not kick in?") + + -- TODO: This is more like a pilot wave off then. + self:_AddToDebrief(playerData, "Pilot waveoff.") + + playerData.waveoff=true + + end + + -- Next step: debrief. + playerData.step=AIRBOSS.PatternStep.DEBRIEF + playerData.warning=nil + + end + +end + +--- LSO check if player needs to wave off. +-- Wave off conditions are: +-- +-- * Glideslope error <1.2 or >1.8 degrees. +-- * |Line up error| > 3 degrees. +-- * AoA check but only for TOPGUN graduates. +-- @param #AIRBOSS self +-- @param #number glideslopeError Glideslope error in degrees. +-- @param #number lineupError Line up error in degrees. +-- @param #number AoA Angle of attack of player aircraft. +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @return #boolean If true, player should wave off! +function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) + + -- Assume we're all good. + local waveoff=false + + -- Too high or too low? + if glideslopeError>1.8 then + local text=string.format("Wave off due to glideslope error %.2f > 1.8 degrees!", glideslopeError) + self:T(self.lid..string.format("%s: %s", playerData.name, text)) + self:_AddToDebrief(playerData, text) + waveoff=true + elseif glideslopeError<-1.2 then + local text=string.format("Wave off due to glideslope error %.2f < -1.2 degrees!", glideslopeError) + self:T(self.lid..string.format("%s: %s", playerData.name, text)) + self:_AddToDebrief(playerData, text) + waveoff=true + end + + -- Too far from centerline? + if math.abs(lineupError)>3 then + local text=string.format("Wave off due to line up error |%.1f| > 3 degrees!", lineupError) + self:T(self.lid..string.format("%s: %s", playerData.name, text)) + self:_AddToDebrief(playerData, text) + waveoff=true + end + + -- Too slow or too fast? Only for pros. + if playerData.difficulty==AIRBOSS.Difficulty.HARD then + -- Get aircraft specific AoA values + local aoaac=self:_GetAircraftAoA(playerData) + -- Check too slow or too fast. + if AoAaoaac.SLOW then + local text=string.format("Wave off due to AoA %.1f > %.1f!", AoA, aoaac.SLOW) + self:T(self.lid..string.format("%s: %s", playerData.name, text)) + self:_AddToDebrief(playerData, text) + waveoff=true + end + end + + return waveoff +end + +--- Check if other aircraft are currently on the landing runway. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @return boolean If true, we have a foul deck. +function AIRBOSS:_CheckFoulDeck(playerData) + + -- Check if player was already waved off. Should not be necessary as player step is set to debrief afterwards! + if playerData.fouldeckwo==true then + -- Player was already waved off. + return + end + + -- Landing runway zone. + local runway=self:_GetZoneRunwayBox() + + -- Scan radius. + local R=250 + + -- Debug info. + self:T(self.lid..string.format("Foul deck check: Scanning Carrier Runway Area. Radius=%.1f m.", R)) + + -- Scan units in carrier zone. + local _,_,_,unitscan=self:GetCoordinate():ScanObjects(R, true, false, false) + + -- Loop over all scanned units and check if they are on the runway. + local fouldeck=false + local foulunit=nil --Wrapper.Unit#UNIT + for _,_unit in pairs(unitscan) do + local unit=_unit --Wrapper.Unit#UNIT + + -- Check if unit is in zone. + local inzone=unit:IsInZone(runway) + + -- Check if aircraft and in air. + local isaircraft=unit:IsAir() + local isairborn =unit:InAir() + + if inzone and isaircraft and not isairborn then + local text=string.format("Unit %s on landing runway ==> Foul deck!", unit:GetName()) + self:T(self.lid..text) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + if self.Debug then + runway:FlareZone(FLARECOLOR.Red, 30) + end + fouldeck=true + foulunit=unit + end + end + + + -- Add to debrief and + if playerData and fouldeck then + + -- Debrief text. + local text=string.format("Foul deck waveoff due to aircraft %s!", foulunit:GetName()) + self:T(self.lid..string.format("%s: %s", playerData.name, text)) + self:_AddToDebrief(playerData, text) + + -- Foul deck + wave off radio message. + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.FOULDECK, false, 1) + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.WAVEOFF, false, 1.2) + + -- Player hint for flight students. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local text=string.format("overfly landing area and enter bolter pattern.") + self:MessageToPlayer(playerData, text, "LSO", nil, nil, false, 3) + end + + -- Set player parameters for foul deck + playerData.fouldeckwo=true + playerData.step=AIRBOSS.PatternStep.DEBRIEF + playerData.warning=nil + playerData.valid=false + + -- Send a message to the player that blocks the runway. + if foulunit then + local foulflight=self:_GetFlightFromGroupInQueue(foulunit:GetGroup(), self.flights) + if foulflight and not foulflight.ai then + self:MessageToPlayer(foulflight, "move your ass from my runway. NOW!", "AIRBOSS") + end + end + end + + return fouldeck +end + +--- Get "stern" coordinate. +-- @param #AIRBOSS self +-- @return Core.Point#COORDINATE Coordinate at the rundown of the carrier. +function AIRBOSS:_GetSternCoord() + + -- Heading of carrier (true). + local hdg=self.carrier:GetHeading() + + -- Final bearing (true). + local FB=self:GetFinalBearing() + + -- Stern coordinate (sterndist<0). Also translate 10 meters starboard wrt Final bearing. + local stern=self:GetCoordinate():Translate(self.carrierparam.sterndist, hdg):Translate(7, FB+90) + + -- Set altitude. + stern:SetAltitude(self.carrierparam.deckheight) + + return stern +end + +--- Get wire from landing position. +-- @param #AIRBOSS self +-- @param Core.Point#COORDINATE Lcoord Landing position. +-- @param #number dc Distance correction. Shift the landing coord back if dc>0 and forward if dc<0. +-- @return #number Trapped wire (1-4) or 99 if no wire was trapped. +function AIRBOSS:_GetWire(Lcoord, dc) + + -- Final bearing (true). + local FB=self:GetFinalBearing() + + -- Stern coordinate (sterndist<0). Also translate 10 meters starboard wrt Final bearing. + local Scoord=self:_GetSternCoord() + + -- Distance to landing coord. + local Ldist=Lcoord:Get2DDistance(Scoord) + + -- For human (not AI) the lading event is delayed unfortunately. Therefore, we need another correction factor. + dc= dc or 65 + + -- Corrected landing distance wrt to stern. Landing distance needs to be reduced due to delayed landing event for human players. + local d=Ldist-dc + + -- Shift wires from stern to their correct position. + local w1=self.carrierparam.wire1 + local w2=self.carrierparam.wire2 + local w3=self.carrierparam.wire3 + local w4=self.carrierparam.wire4 + + -- Which wire was caught? + local wire + if d wire=%d (dc=%.1f)", Ldist, Ldist-dc, wire, dc)) + + return wire +end + +--- Trapped? Check if in air or not after landing event. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Trapped(playerData) + + if playerData.unit:InAir()==false then + -- Seems we have successfully landed. + + -- Lets see if we can get a good wire. + local unit=playerData.unit + + -- Coordinate of player aircraft. + local coord=unit:GetCoordinate() + + -- Get velocity in km/h. We need to substrackt the carrier velocity. + local v=unit:GetVelocityKMH()-self.carrier:GetVelocityKMH() + + -- Stern coordinate. + local stern=self:_GetSternCoord() + + -- Distance to stern pos. + local s=stern:Get2DDistance(coord) + + -- Get current wire (estimate). This now based on the position where the player comes to a standstill which should reflect the trapped wire better. + local dcorr=100 + if playerData.actype==AIRBOSS.AircraftCarrier.HORNET then + dcorr=100 + elseif playerData.actype==AIRBOSS.AircraftCarrier.A4EC then + -- A-4E gets slowed down much faster the the F/A-18C! + dcorr=56 + end + local wire=self:_GetWire(coord, dcorr) + + -- Debug. + local text=string.format("Player %s _Trapped: v=%.1f km/h, s-dcorr=%.1f m ==> wire=%d (dcorr=%d)", playerData.name, v, s-dcorr, wire, dcorr) + self:T(self.lid..text) + + -- Call this function again until v < threshold. Player comes to a standstill ==> Get wire! + if v>5 then + SCHEDULER:New(nil, self._Trapped, {self, playerData}, 0.1) + return + end + + ---------------------------------------- + --- Form this point on we have converged + ---------------------------------------- + + -- Put some smoke and a mark. + if self.Debug then + coord:SmokeBlue() + coord:MarkToAll(text) + stern:MarkToAll("Stern") + end + + -- Set player wire. + playerData.wire=wire + + -- Message to player. + local text=string.format("Trapped %d-wire.", wire) + if wire==3 then + text=text.." Well done!" + elseif wire==2 then + text=text.." Not bad, maybe you even get the 3rd next time." + elseif wire==4 then + text=text.." That was scary. You can do better than this!" + elseif wire==1 then + text=text.." Try harder next time!" + end + + -- Message to player. + self:MessageToPlayer(playerData, text, "LSO", "") + + -- Debrief. + local hint = string.format("Trapped %d-wire.", wire) + self:_AddToDebrief(playerData, hint, "Groove: IW") + + else + + --Again in air ==> Boltered! + local text=string.format("Player %s boltered in trapped function.", playerData.name) + self:T(self.lid..text) + MESSAGE:New(text, 5, "DEBUG"):ToAllIf(self.debug) + + -- Bolter switch on. + playerData.boltered=true + + end + + -- Next step: debriefing. + playerData.step=AIRBOSS.PatternStep.DEBRIEF + playerData.warning=nil +end + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ZONE functions +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Get Initial zone for Case I or II. +-- @param #AIRBOSS self +-- @param #number case Recovery Case. +-- @return Core.Zone#ZONE_POLYGON_BASE Initial zone. +function AIRBOSS:_GetZoneInitial(case) + + -- Get radial, i.e. inverse of BRC. + local radial=self:GetRadial(2, false, false) + + -- Carrier coordinate. + local cv=self:GetCoordinate() + + -- Zone and vec2 array. + local zone + local vec2 + + if case==1 then + -- Case I + + local c1=cv:Translate(UTILS.NMToMeters(0.5), radial-90) -- 0.0 0.5 starboard + local c2=cv:Translate(UTILS.NMToMeters(1.3), radial-90):Translate(UTILS.NMToMeters(3), radial) -- -3.0 1.3 starboard, astern + local c3=cv:Translate(UTILS.NMToMeters(0.4), radial+90):Translate(UTILS.NMToMeters(3), radial) -- -3.0 -0.4 port, astern + local c4=cv:Translate(UTILS.NMToMeters(1.0), radial) + local c5=cv + + -- Vec2 array. + vec2={c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2()} + + else + -- Case II + + -- Funnel. + local c1=cv:Translate(UTILS.NMToMeters(0.5), radial-90) -- 0.0, 0.5 + local c2=c1:Translate(UTILS.NMToMeters(0.5), radial) -- 0.5, 0.5 + local c3=cv:Translate(UTILS.NMToMeters(1.2), radial-90):Translate(UTILS.NMToMeters(3), radial) -- 3.0, 1.2 + local c4=cv:Translate(UTILS.NMToMeters(1.2), radial+90):Translate(UTILS.NMToMeters(3), radial) -- 3.0,-1.2 + local c5=cv:Translate(UTILS.NMToMeters(0.5), radial) + local c6=cv + + -- Vec2 array. + vec2={c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2(), c6:GetVec2()} + + end + + -- Polygon zone. + local zone=ZONE_POLYGON_BASE:New("CASE I/II initial.", vec2) + + return zone +end + + +--- Get Bullseye zone with radius 1 NM and DME 3 NM from the carrier. Radial depends on recovery case. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @return Core.Zone#ZONE_RADIUS Arc in zone. +function AIRBOSS:_GetZoneBullseye(case) + + -- Radius = 1 NM. + local radius=UTILS.NMToMeters(1) + + -- Distance = 3 NM + local distance=UTILS.NMToMeters(3) + + -- Zone depends on Case recovery. + local radial=self:GetRadial(case, false, false) + + -- Get coordinate and vec2. + local coord=self:GetCoordinate():Translate(distance, radial) + local vec2=coord:GetVec2() + + -- Create zone. + local zone=ZONE_RADIUS:New("Zone Bullseye", vec2, radius) + + return zone +end + +--- Get dirty up zone with radius 1 NM and DME 9 NM from the carrier. Radial depends on recovery case. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @return Core.Zone#ZONE_RADIUS Dirty up zone. +function AIRBOSS:_GetZoneDirtyUp(case) + + -- Radius = 1 NM. + local radius=UTILS.NMToMeters(1) + + -- Distance = 9 NM + local distance=UTILS.NMToMeters(9) + + -- Zone depends on Case recovery. + local radial=self:GetRadial(case, false, false) + + -- Get coordinate and vec2. + local coord=self:GetCoordinate():Translate(distance, radial) + local vec2=coord:GetVec2() + + -- Create zone. + local zone=ZONE_RADIUS:New("Zone Dirty Up", vec2, radius) + + return zone +end + +--- Get arc out zone with radius 1 NM and DME 12 NM from the carrier. Radial depends on recovery case. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @return Core.Zone#ZONE_RADIUS Arc in zone. +function AIRBOSS:_GetZoneArcOut(case) + + -- Radius = 1 NM. + local radius=UTILS.NMToMeters(1.25) + + -- Distance = 12 NM + local distance=UTILS.NMToMeters(11.75) + + -- Zone depends on Case recovery. + local radial=self:GetRadial(case, false, false) + + -- Get coordinate of carrier and translate. + local coord=self:GetCoordinate():Translate(distance, radial) + + -- Create zone. + local zone=ZONE_RADIUS:New("Zone Arc Out", coord:GetVec2(), radius) + + return zone +end + +--- Get arc in zone with radius 1 NM and DME 14 NM from the carrier. Radial depends on recovery case. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @return Core.Zone#ZONE_RADIUS Arc in zone. +function AIRBOSS:_GetZoneArcIn(case) + + -- Radius = 1 NM. + local radius=UTILS.NMToMeters(1.25) + + -- Zone depends on Case recovery. + local radial=self:GetRadial(case, false, true) + + -- Angle between FB/BRC and holding zone. + local alpha=math.rad(self.holdingoffset) + + -- 14+x NM from carrier + local x=14/math.cos(alpha) + + -- Distance = 14 NM + local distance=UTILS.NMToMeters(x) + + -- Get coordinate. + local coord=self:GetCoordinate():Translate(distance, radial) + + -- Create zone. + local zone=ZONE_RADIUS:New("Zone Arc In", coord:GetVec2(), radius) + + return zone +end + +--- Get platform zone with radius 1 NM and DME 19 NM from the carrier. Radial depends on recovery case. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @return Core.Zone#ZONE_RADIUS Circular platform zone. +function AIRBOSS:_GetZonePlatform(case) + + -- Radius = 1 NM. + local radius=UTILS.NMToMeters(1) + + -- Zone depends on Case recovery. + local radial=self:GetRadial(case, false, true) + + -- Angle between FB/BRC and holding zone. + local alpha=math.rad(self.holdingoffset) + + -- Distance = 19 NM + local distance=UTILS.NMToMeters(19) --/math.cos(alpha) + + -- Get coordinate. + local coord=self:GetCoordinate():Translate(distance, radial) + + -- Create zone. + local zone=ZONE_RADIUS:New("Zone Platform", coord:GetVec2(), radius) + + return zone +end + + +--- Get approach corridor zone. Shape depends on recovery case. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @return Core.Zone#ZONE_POLYGON_BASE Box zone. +function AIRBOSS:_GetZoneCorridor(case) + + -- Radial and offset. + local radial=self:GetRadial(case, false, false) + local offset=self:GetRadial(case, false, true) + + -- Angle between radial and offset in rad. + local alpha=math.rad(self.holdingoffset) + + -- Distance shift ahead of carrier to allow for some space to bolter + local dx=2 + + -- Width of the box in NM. + local w=2 + local w2=w/2 + + -- Distance from carrier to arc out zone. + local d=12 + + -- Length of the box in NM. + local x=(d+w/2)/math.cos(alpha) + local l=28-x + + -- Some math... + local y1=d-w2 + local x1=y1*math.tan(alpha) + local y2=d+w2 + local x2=y2*math.tan(alpha) + local b=w2*(1/math.cos(alpha)-1) + + -- This is what we need. + local P=x1+b + local Q=x2-b + + -- Debug output. + self:T3(string.format("FF case %d radial = %d", case, radial)) + self:T3(string.format("FF case %d offset = %d", case, offset)) + self:T3(string.format("FF w = %.1f NM", w)) + self:T3(string.format("FF l = %.1f NM", l)) + self:T3(string.format("FF d = %.1f NM", d)) + self:T3(string.format("FF y1 = %.1f NM", y1)) + self:T3(string.format("FF x1 = %.1f NM", x1)) + self:T3(string.format("FF y2 = %.1f NM", y2)) + self:T3(string.format("FF x2 = %.1f NM", x2)) + self:T3(string.format("FF b = %.1f NM", b)) + self:T3(string.format("FF P = %.1f NM", P)) + self:T3(string.format("FF Q = %.1f NM", Q)) + + local c={} + c[1]=self:GetCoordinate():Translate(-UTILS.NMToMeters(dx), radial) --Carrier coordinate translated 2 NM in direction of travel to allow for bolter space. + + if math.abs(self.holdingoffset)>1 then + -- Complicated case with an angle. + c[2]=c[1]:Translate( UTILS.NMToMeters(w2), radial-90) -- 1 Right of carrier. + c[3]=c[2]:Translate( UTILS.NMToMeters(d+dx+w2), radial) -- 13 "south" @ 1 right + c[4]=c[3]:Translate( UTILS.NMToMeters(Q), radial+90) -- + c[5]=c[4]:Translate( UTILS.NMToMeters(l), offset) + c[6]=c[5]:Translate( UTILS.NMToMeters(w), offset+90) -- Back wall (angled) + c[9]=c[1]:Translate( UTILS.NMToMeters(w2), radial+90) -- 1 left of carrier. + c[8]=c[9]:Translate( UTILS.NMToMeters(d+dx-w2), radial) -- 1 left and 11 behind of carrier. + c[7]=c[8]:Translate( UTILS.NMToMeters(P), radial+90) + + -- Translate these points a bit for a smoother turn. + c[4]=c[4]:Translate(UTILS.NMToMeters(2), offset) + c[7]=c[7]:Translate(UTILS.NMToMeters(2), offset) + else + -- Easy case of a long box. + c[2]=c[1]:Translate( UTILS.NMToMeters(w2), radial-90) + c[3]=c[2]:Translate( UTILS.NMToMeters(d+dx+w2+l), radial) -- 12+1+10 = 23 NM behind the carrier. Stack 1 starts at 21 and is 7 NM. + c[4]=c[3]:Translate( UTILS.NMToMeters(w), radial+90) + c[5]=c[1]:Translate( UTILS.NMToMeters(w2), radial+90) + end + + + -- Create an array of a square! + local p={} + for _i,_c in ipairs(c) do + if self.Debug then + --_c:SmokeBlue() + end + p[_i]=_c:GetVec2() + end + + -- Square zone length=10NM width=6 NM behind the carrier starting at angels+15 NM behind the carrier. + -- So stay 0-5 NM (+1 NM error margin) port of carrier. + local zone=ZONE_POLYGON_BASE:New("CASE II/III Approach Corridor", p) + + return zone +end + + +--- Get zone of carrier. Carrier is approximated as rectangle. +-- @param #AIRBOSS self +-- @return Core.Zone#ZONE Zone surrounding the carrier. +function AIRBOSS:_GetZoneCarrierBox() + + -- Stern coordinate. + local S=self:_GetSternCoord() + + -- Current carrier heading. + local hdg=self:GetHeading(false) + + -- Coordinate array. + local p={} + + -- Starboard stern point. + p[1]=S:Translate(self.carrierparam.totwidthstarboard, hdg+90) + + -- Starboard bow point. + p[2]=p[1]:Translate(self.carrierparam.totlength, hdg) + + -- Port bow point. + p[3]=p[2]:Translate(self.carrierparam.totwidthstarboard+self.carrierparam.totwidthport, hdg-90) + + -- Port stern point. + p[4]=p[3]:Translate(self.carrierparam.totlength, hdg-180) + + -- Convert to vec2. + local vec2={} + for _,coord in ipairs(p) do + table.insert(vec2, coord:GetVec2()) + end + + -- Create polygon zone. + local zone=ZONE_POLYGON_BASE:New("Carrier Box Zone", vec2) + + return zone +end + +--- Get zone of landing runway +-- @param #AIRBOSS self +-- @return Core.Zone#ZONE_POLYGON Zone surrounding landing runway. +function AIRBOSS:_GetZoneRunwayBox() + + -- Stern coordinate. + local S=self:_GetSternCoord() + + -- Current carrier heading. + local FB=self:GetFinalBearing(false) + + -- Coordinate array. + local p={} + + -- Points. + p[1]=S:Translate(self.carrierparam.rwywidth*0.5, FB+90) + p[2]=p[1]:Translate(self.carrierparam.rwylength, FB) + p[3]=p[2]:Translate(self.carrierparam.rwywidth, FB-90) + p[4]=p[3]:Translate(self.carrierparam.rwylength, FB-180) + + -- Convert to vec2. + local vec2={} + for _,coord in ipairs(p) do + table.insert(vec2, coord:GetVec2()) + end + + -- Create polygon zone. + local zone=ZONE_POLYGON_BASE:New("Landing Runway Zone", vec2) + + return zone +end + +--- Get holding zone of player. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @param #number stack Marshal stack number. +-- @return Core.Zone#ZONE Holding zone. +function AIRBOSS:_GetZoneHolding(case, stack) + + -- Holding zone. + local zoneHolding=nil --Core.Zone#ZONE + + -- Stack is <= 0 ==> no marshal zone. + if stack<=0 then + return nil + end + + -- Pattern alitude. + local patternalt, c1, c2=self:_GetMarshalAltitude(stack, case) + + -- Select case. + if case==1 then + -- CASE I + + -- Get current carrier heading. + local hdg=self:GetHeading() + + -- Distance to the post. + local D=UTILS.NMToMeters(2.5) + + -- Post 2.5 NM port of carrier. + local Post=self:GetCoordinate():Translate(D, hdg+270) + + -- Create holding zone. + zoneHolding=ZONE_RADIUS:New("CASE I Holding Zone", Post:GetVec2(), self.marshalradius) + + else + -- CASE II/II + + -- Get radial. + local radial=self:GetRadial(case, false, true) + + -- Create an array of a rectangle. Length is 7 NM, width is 8 NM. One NM starboard to line up with the approach corridor. + local p={} + p[1]=c2:Translate(UTILS.NMToMeters(1), radial-90):GetVec2() --c2 is at (angels+15) NM directly behind the carrier. We translate it 1 NM starboard. + p[2]=c1:Translate(UTILS.NMToMeters(1), radial-90):GetVec2() --c1 is 7 NM further behind. Also translated 1 NM starboard. + p[3]=c1:Translate(UTILS.NMToMeters(7), radial+90):GetVec2() --p3 7 NM port of carrier. + p[4]=c2:Translate(UTILS.NMToMeters(7), radial+90):GetVec2() --p4 7 NM port of carrier. + + -- Square zone length=7NM width=6 NM behind the carrier starting at angels+15 NM behind the carrier. + -- So stay 0-5 NM (+1 NM error margin) port of carrier. + zoneHolding=ZONE_POLYGON_BASE:New("CASE II/III Holding Zone", p) + end + + return zoneHolding +end + +--- Get zone where player are automatically commence when enter. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @return Core.Zone#ZONE Holding zone. +function AIRBOSS:_GetZoneCommence(case) + + -- Commence zone. + local zone + + if case==1 then + -- Case I + + -- Get current carrier heading. + local hdg=self:GetHeading() + + -- Distance to the zone. + local D=UTILS.NMToMeters(4.75) + + -- Zone radius. + local R=UTILS.NMToMeters(1) + + -- Three position + local Three=self:GetCoordinate():Translate(D, hdg+275) + + -- Create holding zone. + zone=ZONE_RADIUS:New("CASE I Holding Zone", Three:GetVec2(), R) + + else + -- Case II/III + + -- We simply take the corridor for now. + zone=self:_GetZoneCorridor(case) + + end + + return zone +end + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ORIENTATION functions +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Provide info about player status on the fly. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_AttitudeMonitor(playerData) + + -- Player unit. + local unit=playerData.unit + + -- Aircraft attitude. + local aoa=unit:GetAoA() + local yaw=unit:GetYaw() + local roll=unit:GetRoll() + local pitch=unit:GetPitch() + + -- Distance to the boat. + local dist=playerData.unit:GetCoordinate():Get2DDistance(self:GetCoordinate()) + local dx,dz,rho,phi=self:_GetDistances(unit) + + -- Wind vector. + local wind=unit:GetCoordinate():GetWindWithTurbulenceVec3() + + -- Aircraft veloecity vector. + local velo=unit:GetVelocityVec3() + local vabs=UTILS.VecNorm(velo) + + -- Relative heading Aircraft to Carrier. + local relhead=self:_GetRelativeHeading(playerData.unit) + + local step=playerData.step + if playerData.step==AIRBOSS.PatternStep.FINAL or + playerData.step==AIRBOSS.PatternStep.GROOVE_XX or + playerData.step==AIRBOSS.PatternStep.GROOVE_IM or + playerData.step==AIRBOSS.PatternStep.GROOVE_IC or + playerData.step==AIRBOSS.PatternStep.GROOVE_AR or + playerData.step==AIRBOSS.PatternStep.GROOVE_IW then + step=self:_GS(step,-1) + end + + -- Output + local text=string.format("Pattern step: %s", step) + text=text..string.format("\nAoA=%.1f° | |V|=%.1f knots", aoa, UTILS.MpsToKnots(vabs)) + if self.Debug then + -- Velocity vector. + text=text..string.format("\nVx=%.1f Vy=%.1f Vz=%.1f m/s", velo.x, velo.y, velo.z) + --Wind vector. + text=text..string.format("\nWind Vx=%.1f Vy=%.1f Vz=%.1f m/s", wind.x, wind.y, wind.z) + end + text=text..string.format("\nPitch=%.1f° | Roll=%.1f° | Yaw=%.1f°", pitch, roll, yaw) + text=text..string.format("\nClimb Angle=%.1f° | Rate=%d ft/min", unit:GetClimbAngle(), velo.y*196.85) + -- If in the groove, provide line up and glide slope error. + if playerData.step==AIRBOSS.PatternStep.FINAL or + playerData.step==AIRBOSS.PatternStep.GROOVE_XX or + playerData.step==AIRBOSS.PatternStep.GROOVE_IM or + playerData.step==AIRBOSS.PatternStep.GROOVE_IC or + playerData.step==AIRBOSS.PatternStep.GROOVE_AR or + playerData.step==AIRBOSS.PatternStep.GROOVE_IW then + local lineup=self:_Lineup(playerData.unit, true) + local glideslope=self:_Glideslope(playerData.unit, 3.5) + local dist=self:_GetOptLandingCoordinate():Get2DDistance(playerData.unit) + text=text..string.format("\nDist=%.1f m Alt=%.1f m", dist, self:_GetAltCarrier(playerData.unit)) + text=text..string.format("\nLineUp=%.2f° | GlideSlope=%.2f° | AoA=%.1f", lineup, glideslope, self:_AoADeg2Units(playerData, aoa)) + local grade, points, analysis=self:_LSOgrade(playerData) + text=text..string.format("\nGrade: %s %.1f PT - %s", grade, points, analysis) + else + text=text..string.format("\nR=%.2f NM | X=%d Z=%d m", UTILS.MetersToNM(rho), dx, dz) + text=text..string.format("\nGamma=%.1f°", relhead) + end + + MESSAGE:New(text, 1, nil , true):ToClient(playerData.client) +end + +--- Get glide slope of aircraft unit. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. +-- @param #number optangle (Optional) Return glide slope relative to this angle, i.e. the error from the optimal glide slope ~3.5 degrees. +-- @return #number Glide slope angle in degrees measured from the deck of the carrier and third wire. +function AIRBOSS:_Glideslope(unit, optangle) + + -- Default is 0. + optangle=optangle or 0 + + -- Landing coordinate + local landingcoord=self:_GetOptLandingCoordinate() + + -- Distance from stern to aircraft. + local x=unit:GetCoordinate():Get2DDistance(landingcoord) + + -- Altitude of unit corrected by the deck height of the carrier. + local h=self:_GetAltCarrier(unit) + + -- Glide slope. + local glideslope=math.atan(h/x) + + -- Glide slope (error) in degrees. + local gs=math.deg(glideslope)-optangle + + -- Debug. + self:T3(self.lid..string.format("Glide slope error = %.1f, x=%.1f h=%.1f", gs, x, h)) + + return gs +end + +--- Get line up of player wrt to carrier. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. +-- @param #boolean runway If true, include angled runway. +-- @return #number Line up with runway heading in degrees. 0 degrees = perfect line up. +1 too far left. -1 too far right. +function AIRBOSS:_Lineup(unit, runway) + + -- Landing coordinate + local landingcoord=self:_GetOptLandingCoordinate() + + -- Vector to landing coord. + local A=landingcoord:GetVec3() + + -- Vector to player. + local B=unit:GetVec3() + + -- Vector from player to carrier. + local C=UTILS.VecSubstract(A, B) + + -- Only in 2D plane. + C.y=0 + + -- Orientation of carrier. + local X=self.carrier:GetOrientationX() + + -- Rotate orientation to angled runway. + if runway then + X=UTILS.Rotate2D(X, -self.carrierparam.rwyangle) + end + + -- Projection of player pos on x component. + local x=UTILS.VecDot(X, C) + + -- Orientation of carrier. + local Z=self.carrier:GetOrientationZ() + + -- Rotate orientation to angled runway. + if runway then + Z=UTILS.Rotate2D(Z, -self.carrierparam.rwyangle) + end + + -- Projection of player pos on z component. + local z=UTILS.VecDot(Z, C) + + --- + + -- Position of the aircraft in the new coordinate system. + local a={x=x, y=0, z=z} + + -- Stern position in the new coordinate system, which is simply the origin. + local b={x=0, y=0, z=0} + + -- Vector from plane to ref point on the boat. + local c=UTILS.VecSubstract(a, b) + + -- Current line up and error wrt to final heading of the runway. + local lineup=math.deg(math.atan2(c.z, c.x)) + + return lineup +end + +--- Get alitude of aircraft wrt carrier deck. Should give zero when the aircraft touched down. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. +-- @return #number Altitude in meters wrt carrier height. +function AIRBOSS:_GetAltCarrier(unit) + -- Altitude of unit corrected by the deck height of the carrier. + local h=unit:GetAltitude()-self.carrierparam.deckheight-4 + return h +end + +--- Get optimal landing position of the aircraft. Usually between second and third wire. +-- @param #AIRBOSS self +-- @return Core.Point#COORDINATE Optimal landing coordinate. +function AIRBOSS:_GetOptLandingCoordinate() + + -- Stern coordinate. + local stern=self:_GetSternCoord() + + -- Ideally we want to land between 2nd and 3rd wire. + if self.carrierparam.wire3 then + -- We take the position of the 3rd wire to approximately account for the length of the aircraft. + local d23=self.carrierparam.wire3 --+0.5*(self.carrierparam.wire3-self.carrierparam.wire2) + stern=stern:Translate(d23, self:GetFinalBearing(false), true) + end + + return stern +end + +--- Get true (or magnetic) heading of carrier. +-- @param #AIRBOSS self +-- @param #boolean magnetic If true, calculate magnetic heading. By default true heading is returned. +-- @return #number Carrier heading in degrees. +function AIRBOSS:GetHeading(magnetic) + self:F3({magnetic=magnetic}) + + -- Carrier heading + local hdg=self.carrier:GetHeading() + + -- Include magnetic declination. + if magnetic then + hdg=hdg-self.magvar + end + + -- Adjust negative values. + if hdg<0 then + hdg=hdg+360 + end + + return hdg +end + +--- Get base recovery course (BRC) of carrier. +-- The is the magnetic heading of the carrier. +-- @param #AIRBOSS self +-- @return #number BRC in degrees. +function AIRBOSS:GetBRC() + return self:GetHeading(true) +end + +--- Get true (or magnetic) heading of carrier into the wind. This accounts for the angled runway. +-- @param #AIRBOSS self +-- @param #boolean magnetic If true, calculate magnetic heading. By default true heading is returned. +-- @return #number Carrier heading in degrees. +function AIRBOSS:GetHeadingIntoWind(magnetic) + + -- Get direction the wind is blowing from. This is where we want to go. + local windfrom=self:GetCoordinate():GetWind(50) + + -- Actually, we want the runway in the wind. + local intowind=windfrom-self.carrierparam.rwyangle + + -- Magnetic heading. + if magnetic then + intowind=intowind-self.magvar + end + + -- Adjust negative values. + if intowind<0 then + intowind=intowind+360 + end + + return intowind +end + +--- Get base recovery course (BRC) when the carrier would head into the wind. +-- This includes the current wind direction and accounts for the angled runway. +-- @param #AIRBOSS self +-- @return #number BRC into the wind in degrees. +function AIRBOSS:GetBRCintoWind() + -- BRC is the magnetic heading. + return self:GetHeadingIntoWind(true) +end + + +--- Get final bearing (FB) of carrier. +-- By default, the routine returns the magnetic FB depending on the current map (Caucasus, NTTR, Normandy, Persion Gulf etc). +-- The true bearing can be obtained by setting the *TrueNorth* parameter to true. +-- @param #AIRBOSS self +-- @param #boolean magnetic If true, magnetic FB is returned. +-- @return #number FB in degrees. +function AIRBOSS:GetFinalBearing(magnetic) + + -- First get the heading. + local fb=self:GetHeading(magnetic) + + -- Final baring = BRC including angled deck. + fb=fb+self.carrierparam.rwyangle + + -- Adjust negative values. + if fb<0 then + fb=fb+360 + end + + return fb +end + +--- Get radial with respect to carrier BRC or FB and (optionally) holding offset. +-- +-- * case=1: radial=FB-180 +-- * case=2: radial=HDG-180 (+offset) +-- * case=3: radial=FB-180 (+offset) +-- +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @param #boolean magnetic If true, magnetic radial is returned. Default is true radial. +-- @param #boolean offset If true, inlcude holding offset. +-- @param #boolean inverse Return inverse, i.e. radial-180 degrees. +-- @return #number Radial in degrees. +function AIRBOSS:GetRadial(case, magnetic, offset, inverse) + + -- Case or current case. + case=case or self.case + + -- Radial. + local radial + + -- Select case. + if case==1 then + + -- Get radial. + radial=self:GetFinalBearing(magnetic)-180 + + elseif case==2 then + + -- Radial wrt to heading of carrier. + radial=self:GetHeading(magnetic)-180 + + -- Holding offset angle (+-15 or 30 degrees usually) + if offset then + radial=radial+self.holdingoffset + end + + elseif case==3 then + + -- Radial wrt angled runway. + radial=self:GetFinalBearing(magnetic)-180 + + -- Holding offset angle (+-15 or 30 degrees usually) + if offset then + radial=radial+self.holdingoffset + end + + end + + -- Adjust for negative values. + if radial<0 then + radial=radial+360 + end + + -- Inverse? + if inverse then + + -- Inverse radial + radial=radial-180 + + -- Adjust for negative values. + if radial<0 then + radial=radial+360 + end + + end + + return radial +end + +--- Get relative heading of player wrt carrier. +-- This is the angle between the direction/orientation vector of the carrier and the direction/orientation vector of the provided unit. +-- Note that this is calculated in the X-Z plane, i.e. the altitude Y is not taken into account. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Player unit. +-- @param #boolean runway (Optional) If true, return relative heading of unit wrt to angled runway of the carrier. +-- @return #number Relative heading in degrees. An angle of 0 means, unit fly parallel to carrier. An angle of + or - 90 degrees means, unit flies perpendicular to carrier. +function AIRBOSS:_GetRelativeHeading(unit, runway) + + -- Direction vector of the carrier. + local vC=self.carrier:GetOrientationX() + + -- Direction vector of the unit. + local vP=unit:GetOrientationX() + + -- We only want the X-Z plane. Aircraft could fly parallel but ballistic and we dont want the "pitch" angle. + vC.y=0 ; vP.y=0 + + -- Get angle between the two orientation vectors in rad. + local rhdg=math.deg(math.acos(UTILS.VecDot(vC,vP)/UTILS.VecNorm(vC)/UTILS.VecNorm(vP))) + + -- Include runway angle. + if runway then + rhdg=rhdg-self.carrierparam.rwyangle + end + + -- Return heading in degrees. + return rhdg +end + +--- Calculate distances between carrier and aircraft unit. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. +-- @return #number Distance [m] in the direction of the orientation of the carrier. +-- @return #number Distance [m] perpendicular to the orientation of the carrier. +-- @return #number Distance [m] to the carrier. +-- @return #number Angle [Deg] from carrier to plane. Phi=0 if the plane is directly behind the carrier, phi=90 if the plane is starboard, phi=180 if the plane is in front of the carrier. +function AIRBOSS:_GetDistances(unit) + + -- Vector to carrier + local a=self.carrier:GetVec3() + + -- Vector to player + local b=unit:GetVec3() + + -- Vector from carrier to player. + local c={x=b.x-a.x, y=0, z=b.z-a.z} + + -- Orientation of carrier. + local x=self.carrier:GetOrientationX() + + -- Projection of player pos on x component. + local dx=UTILS.VecDot(x,c) + + -- Orientation of carrier. + local z=self.carrier:GetOrientationZ() + + -- Projection of player pos on z component. + local dz=UTILS.VecDot(z,c) + + -- Polar coordinates + local rho=math.sqrt(dx*dx+dz*dz) + + + -- Not exactly sure any more what I wanted to calculate here. + local phi=math.deg(math.atan2(dz,dx)) + if phi<0 then + phi=phi+360 + end + + -- phi=0 if the plane is directly behind the carrier, phi=180 if the plane is in front of the carrier + phi=phi-180 + + if phi<0 then + phi=phi+360 + end + + return dx,dz,rho,phi +end + +--- Check limits for reaching next step. +-- @param #AIRBOSS self +-- @param #number X X position of player unit. +-- @param #number Z Z position of player unit. +-- @param #AIRBOSS.Checkpoint check Checkpoint. +-- @return #boolean If true, checkpoint condition for next step was reached. +function AIRBOSS:_CheckLimits(X, Z, check) + + -- Limits + local nextXmin=check.LimitXmin==nil or (check.LimitXmin and (check.LimitXmin<0 and X<=check.LimitXmin or check.LimitXmin>=0 and X>=check.LimitXmin)) + local nextXmax=check.LimitXmax==nil or (check.LimitXmax and (check.LimitXmax<0 and X>=check.LimitXmax or check.LimitXmax>=0 and X<=check.LimitXmax)) + local nextZmin=check.LimitZmin==nil or (check.LimitZmin and (check.LimitZmin<0 and Z<=check.LimitZmin or check.LimitZmin>=0 and Z>=check.LimitZmin)) + local nextZmax=check.LimitZmax==nil or (check.LimitZmax and (check.LimitZmax<0 and Z>=check.LimitZmax or check.LimitZmax>=0 and Z<=check.LimitZmax)) + + -- Proceed to next step if all conditions are fullfilled. + local next=nextXmin and nextXmax and nextZmin and nextZmax + + -- Debug info. + local text=string.format("step=%s: next=%s: X=%d Xmin=%s Xmax=%s | Z=%d Zmin=%s Zmax=%s", + check.name, tostring(next), X, tostring(check.LimitXmin), tostring(check.LimitXmax), Z, tostring(check.LimitZmin), tostring(check.LimitZmax)) + self:T3(self.lid..text) + + return next +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- LSO functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- LSO advice radio call. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #number glideslopeError Error in degrees. +-- @param #number lineupError Error in degrees. +function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) + + -- Advice time. + local advice=0 + local text="" + + -- Glideslope high/low calls. + --TODO: introduce GSE enumerator values. + if glideslopeError>1.5 then + -- "You're high!" + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.HIGH, true) + advice=advice+AIRBOSS.LSOCall.HIGH.duration + elseif glideslopeError>0.8 then + -- "You're high." + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.HIGH, false) + advice=advice+AIRBOSS.LSOCall.HIGH.duration + elseif glideslopeError<-0.9 then + -- "Power!" + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.POWER, true) + advice=advice+AIRBOSS.LSOCall.POWER.duration + elseif glideslopeError<-0.6 then + -- "Power." + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.POWER, false) + advice=advice+AIRBOSS.LSOCall.POWER.duration + else + text="Good altitude." + end + + text=text..string.format(" Glideslope Error = %.2f°", glideslopeError) + text=text.."\n" + + -- Lineup left/right calls. + -- TODO: introduce LUE enumerator values. + if lineupError<-3 then + -- "Come left!" + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.COMELEFT, true) + advice=advice+AIRBOSS.LSOCall.COMELEFT.duration + elseif lineupError<-1 then + -- "Come left." + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.COMELEFT, false) + advice=advice+AIRBOSS.LSOCall.COMELEFT.duration + elseif lineupError>3 then + -- "Right for lineup!" + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.RIGHTFORLINEUP, true) + advice=advice+AIRBOSS.LSOCall.RIGHTFORLINEUP.duration + elseif lineupError>1 then + -- "Right for lineup." + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.RIGHTFORLINEUP, false) + advice=advice+AIRBOSS.LSOCall.RIGHTFORLINEUP.duration + else + text=text.."Good lineup." + end + + text=text..string.format(" Lineup Error = %.1f°\n", lineupError) + + -- Get current AoA. + local AOA=playerData.unit:GetAoA() + + -- Get aircraft AoA parameters. + local acaoa=self:_GetAircraftAoA(playerData) + + -- Speed via AoA. + if AOA>acaoa.SLOW then + -- "Your're slow!" + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.SLOW, true) + advice=advice+AIRBOSS.LSOCall.SLOW.duration + --S=underline("SLO") + elseif AOA>acaoa.Slow then + -- "Your're slow." + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.SLOW, false) + advice=advice+AIRBOSS.LSOCall.SLOW.duration + --S="SLO" + elseif AOA>acaoa.OnSpeedMax then + -- No call. + --S=little("SLO") + elseif AOA 24 seconds: No Grade "--" +-- +-- If you manage to be between 16.4 and and 16.6 seconds, you will even get and okay underline "\_OK\_". +-- +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @return #string LSO grade for time in groove, i.e. \_OK\_, OK, (OK), --. +function AIRBOSS:_EvalGrooveTime(playerData) + + -- Time in groove. + local t=playerData.Tgroove + + local grade="" + if t<9 then + grade="--" + elseif t<12 then + grade="(OK)" + elseif t<22 then + grade="OK" + elseif t<=24 then + grade="(OK)" + else + grade="--" + end + + -- The unicorn! + if t>=16.4 and t<=16.6 then + grade="_OK_" + end + + return grade +end + +--- Grade approach. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @return #string LSO grade, i.g. _OK_, OK, (OK), --, etc. +-- @return #number Points. +-- @return #string LSO analysis of flight path. +function AIRBOSS:_LSOgrade(playerData) + + --- Count deviations. + local function count(base, pattern) + return select(2, string.gsub(base, pattern, "")) + end + + -- Analyse flight data and conver to LSO text. + local GXX,nXX=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.XX) + local GIM,nIM=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.IM) + local GIC,nIC=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.IC) + local GAR,nAR=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.AR) + + -- Put everything together. + local G=GXX.." "..GIM.." ".." "..GIC.." "..GAR + + -- Count number of minor, normal and major deviations. + local N=nXX+nIM+nIC+nAR + local nL=count(G, '_')/2 + local nS=count(G, '%(') + local nN=N-nS-nL + + local grade + local points + if N==0 then + -- No deviations, should be REALLY RARE! + grade="_OK_" + points=5.0 + G="Unicorn" + else + if nL>0 then + -- Larger deviations ==> "No grade" 2.0 points. + grade="--" + points=2.0 + elseif nN>0 then + -- No larger but average deviations ==> "Fair Pass" Pass with average deviations and corrections. + grade="(OK)" + points=3.0 + else + -- Only minor corrections + grade="OK" + points=4.0 + end + end + + -- Replace" )"( and "__" + G=G:gsub("%)%(", "") + G=G:gsub("__","") + + -- Debug info + local text="LSO grade:\n" + text=text..G.."\n" + text=text.."Grade = "..grade.." points = "..points.."\n" + text=text.."# of total deviations = "..N.."\n" + text=text.."# of large deviations _ = "..nL.."\n" + text=text.."# of normal deviations = "..nN.."\n" + text=text.."# of small deviations ( = "..nS.."\n" + self:T2(self.lid..text) + + -- Special cases. + if playerData.patternwo then + -- Pattern Wave Off + grade="PWO" + if playerData.lig then + G="LIG" + elseif playerData.patternwo then + G="n/a" + end + points=1.0 + elseif playerData.fouldeckwo then + if playerData.landed then + --AIRBOSS wants to talk to you! + grade="CUT" + points=0.0 + else + grade="FDWO" + points=-1.0 + end + elseif playerData.waveoff then + -- Wave Off + if playerData.landed then + --AIRBOSS wants to talk to you! + grade="CUT" + points=0.0 + else + grade="WO" + points=1.0 + end + elseif playerData.boltered then + -- Bolter + grade="-- (BOLTER)" + points=2.5 + end + + return grade, points, G +end + +--- Grade flight data. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string groovestep Step in the groove. +-- @param #AIRBOSS.GrooveData fdata Flight data in the groove. +-- @return #string LSO grade or empty string if flight data table is nil. +-- @return #number Number of deviations from perfect flight path. +function AIRBOSS:_Flightdata2Text(playerData, groovestep) + + local function little(text) + return string.format("(%s)",text) + end + local function underline(text) + return string.format("_%s_", text) + end + + -- Groove Data. + local fdata=playerData.groove[groovestep] --#AIRBOSS.GrooveData + + -- No flight data ==> return empty string. + if fdata==nil then + self:T3(self.lid.."Flight data is nil.") + return "", 0 + end + + -- Flight data. + local step=fdata.Step + local AOA=fdata.AoA + local GSE=fdata.GSE + local LUE=fdata.LUE + local ROL=fdata.Roll + + -- Aircraft specific AoA values. + local acaoa=self:_GetAircraftAoA(playerData) + + -- Speed via AoA. Depends on aircraft type. + local S=nil + if AOA>acaoa.SLOW then + S=underline("SLO") + elseif AOA>acaoa.Slow then + S="SLO" + elseif AOA>acaoa.OnSpeedMax then + S=little("SLO") + elseif AOA1.5 then + A=underline("H") + elseif GSE>0.8 then + A="H" + elseif GSE>0.4 then + A=little("H") + elseif GSE<-0.9 then + A=underline("LO") + elseif GSE<-0.6 then + A="LO" + elseif GSE<-0.3 then + A=little("LO") + end + + -- Line up. Good [-0.5, 0.5] + -- TODO: introduce enumerator with LUE values. + local D=nil + if LUE>3 then + D=underline("LUL") + elseif LUE>1 then + D="LUL" + elseif LUE>0.5 then + D=little("LUL") + elseif LUE<-3 then + D=underline("LUR") + elseif LUE<-1 then + D="LUR" + elseif LUE<-0.5 then + D=little("LUR") + end + + -- Compile. + local G="" + local n=0 + -- Fly trough. + if fdata.FlyThrough then + G=G..fdata.FlyThrough + end + -- Speed. + if S then + G=G..S + n=n+1 + end + -- Glide slope. + if A then + G=G..A + n=n+1 + end + -- Line up. + if D then + G=G..D + n=n+1 + end + + -- Add current step. + local step=self:_GS(step) + step=step:gsub("XX","X") + if G~="" then + G=G..step + end + + -- Debug info. + local text=string.format("LSO Grade at %s:\n", step) + text=text..string.format("AOA=%.1f\n",AOA) + text=text..string.format("GSE=%.1f\n",GSE) + text=text..string.format("LUE=%.1f\n",LUE) + text=text..string.format("ROL=%.1f\n",ROL) + text=text..G + self:T3(self.lid..text) + + return G,n +end + +--- Get short name of the grove step. +-- @param #AIRBOSS self +-- @param #string step Player step. +-- @param #number n Use -1 for previous or +1 for next. Default 0. +-- @return #string Shortcut name "X", "RB", "IM", "AR", "IW". +function AIRBOSS:_GS(step, n) + local gp + n=n or 0 + + if step==AIRBOSS.PatternStep.FINAL then + gp=AIRBOSS.GroovePos.X0 --"X0" -- Entering the groove. + if n==-1 then + gp=AIRBOSS.GroovePos.X0 -- There is no previous step. + elseif n==1 then + gp=AIRBOSS.GroovePos.XX + end + elseif step==AIRBOSS.PatternStep.GROOVE_XX then + gp=AIRBOSS.GroovePos.XX --"XX" -- Starting the groove. + if n==-1 then + gp=AIRBOSS.GroovePos.X0 + elseif n==1 then + gp=AIRBOSS.GroovePos.IM + end + elseif step==AIRBOSS.PatternStep.GROOVE_IM then + gp=AIRBOSS.GroovePos.IM --"IM" -- In the middle. + if n==-1 then + gp=AIRBOSS.GroovePos.XX + elseif n==1 then + gp=AIRBOSS.GroovePos.IC + end + elseif step==AIRBOSS.PatternStep.GROOVE_IC then + gp=AIRBOSS.GroovePos.IC --"IC" -- In close. + if n==-1 then + gp=AIRBOSS.GroovePos.IM + elseif n==1 then + gp=AIRBOSS.GroovePos.AR + end + elseif step==AIRBOSS.PatternStep.GROOVE_AR then + gp=AIRBOSS.GroovePos.AR --"AR" -- At the ramp. + if n==-1 then + gp=AIRBOSS.GroovePos.IC + elseif n==1 then + gp=AIRBOSS.GroovePos.IW + end + elseif step==AIRBOSS.PatternStep.GROOVE_IW then + gp=AIRBOSS.GroovePos.IW --"IW" -- In the wires. + if n==-1 then + gp=AIRBOSS.GroovePos.AR + elseif n==1 then + gp=AIRBOSS.GroovePos.IW -- There is no next step. + end + end + return gp +end + +--- Check if a player is within the right area. +-- @param #AIRBOSS self +-- @param #number X X distance player to carrier. +-- @param #number Z Z distance player to carrier. +-- @param #AIRBOSS.Checkpoint pos Position data limits. +-- @return #boolean If true, approach should be aborted. +function AIRBOSS:_CheckAbort(X, Z, pos) + + local abort=false + if pos.Xmin and Xpos.Xmax then + self:T(string.format("Xmax: X=%d > %d=Xmax", X, pos.Xmax)) + abort=true + elseif pos.Zmin and Zpos.Zmax then + self:T(string.format("Zmax: Z=%d > %d=Zmax", Z, pos.Zmax)) + abort=true + end + + return abort +end + +--- Generate a text if a player is too far from where he should be. +-- @param #AIRBOSS self +-- @param #number X X distance player to carrier. +-- @param #number Z Z distance player to carrier. +-- @param #AIRBOSS.Checkpoint posData Checkpoint data. +function AIRBOSS:_TooFarOutText(X, Z, posData) + + -- Intro. + local text="you are too " + + -- X text. + local xtext=nil + if posData.Xmin and XposData.Xmax then + if posData.Xmax>=0 then + xtext="far ahead of " + else + xtext="close to " + end + end + + -- Z text. + local ztext=nil + if posData.Zmin and ZposData.Zmax then + if posData.Zmax>=0 then + ztext="far starboard of " + else + ztext="too close to " + end + end + + -- Combine X-Z text. + if xtext and ztext then + text=text..xtext.." and "..ztext + elseif xtext then + text=text..xtext + elseif ztext then + text=text..ztext + end + + -- Complete the sentence + text=text.."the carrier." + + -- If no case could be identified. + if xtext==nil and ztext==nil then + text="you are too far from where you should be!" + end + + return text +end + +--- Pattern aborted. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #number X X distance player to carrier. +-- @param #number Z Z distance player to carrier. +-- @param #AIRBOSS.Checkpoint posData Checkpoint data. +-- @param #boolean patternwo (Optional) Pattern wave off. +function AIRBOSS:_AbortPattern(playerData, X, Z, posData, patternwo) + + -- Text where we are wrong. + local text=self:_TooFarOutText(X, Z, posData) + + -- Debug. + local dtext=string.format("Abort: X=%d Xmin=%s, Xmax=%s | Z=%d Zmin=%s Zmax=%s", X, tostring(posData.Xmin), tostring(posData.Xmax), Z, tostring(posData.Zmin), tostring(posData.Zmax)) + self:T(self.lid..dtext) + + -- Message to player. + self:MessageToPlayer(playerData, text, "LSO") + + if patternwo then + + -- Pattern wave off! + playerData.patternwo=true + + -- Add to debrief. + self:_AddToDebrief(playerData, string.format("Pattern wave off: %s", text)) + + -- Depart and re-enter radio message. + -- TODO: Radio should depend on player step. + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.DEPARTANDREENTER, false, 3) + + -- Next step debrief. + playerData.step=AIRBOSS.PatternStep.DEBRIEF + playerData.warning=nil + end + +end + + +--- Get error margin depending on player skill. +-- +-- * Flight students: 10% and 20% +-- * Naval Aviators: 5% and 10% +-- * TOPGUN Graduates: 2.5% and 5% +-- +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @return #number Error margin for still being okay. +-- @return #number Error margin for really sucking. +function AIRBOSS:_GetGoodBadScore(playerData) + + local lowscore + local badscore + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + lowscore=10 + badscore=20 + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + lowscore=5 + badscore=10 + elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then + lowscore=2.5 + badscore=5 + end + + return lowscore, badscore +end + + +--- Evaluate player's altitude at checkpoint. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #number altopt Optimal altitude in meters. +-- @return #string Feedback text. +-- @return #string Debriefing text. +function AIRBOSS:_AltitudeCheck(playerData, altopt) + + if altopt==nil then + return nil, nil + end + + -- Player altitude. + local altitude=playerData.unit:GetAltitude() + + -- Get relative score. + local lowscore, badscore=self:_GetGoodBadScore(playerData) + + -- Altitude error +-X% + local _error=(altitude-altopt)/altopt*100 + + -- Radio call for flight students. + local radiocall=nil --#AIRBOSS.RadioCall + + local hint="" + local loud=false + if _error>badscore then + --hint=string.format("You're high.") + radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.HIGH, nil, nil, 5) + loud=true + elseif _error>lowscore then + --hint= string.format("You're slightly high.") + radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.HIGH, nil, nil, 5) + elseif _error<-badscore then + --hint=string.format("You're low. ") + radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.LOW, nil, nil, 5) + loud=true + elseif _error<-lowscore then + --hint=string.format("You're slightly low.") + radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.LOW, nil, nil, 5) + else + hint=string.format("Good altitude.") + end + + -- Extend or decrease depending on skill. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + -- Also inform students about the optimal altitude. + hint=hint..string.format(" Optimal altitude is %d ft.", UTILS.MetersToFeet(altopt)) + self:Sound2Player(playerData, self.LSORadio, radiocall, loud) + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + -- We keep it short normally. + hint="" + self:Sound2Player(playerData, self.LSORadio, radiocall, loud) + elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then + -- No hint at all for the pros. + hint="" + end + + -- Debrief text. + local debrief=string.format("Altitude %d ft = %d%% deviation from %d ft.", UTILS.MetersToFeet(altitude), _error, UTILS.MetersToFeet(altopt)) + + return hint, debrief +end + +--- Evaluate player's distance to the boat at checkpoint. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #number optdist Optimal distance in meters. +-- @return #string Feedback message text. +-- @return #string Debriefing text. +function AIRBOSS:_DistanceCheck(playerData, optdist) + + if optdist==nil then + return nil, nil + end + + -- Distance to carrier. + local distance=playerData.unit:GetCoordinate():Get2DDistance(self:GetCoordinate()) + + -- Get relative score. + local lowscore, badscore = self:_GetGoodBadScore(playerData) + + -- Altitude error +-X% + local _error=(distance-optdist)/optdist*100 + + local hint + if _error>badscore then + hint=string.format("You're too far from the boat!") + elseif _error>lowscore then + hint=string.format("You're slightly too far from the boat.") + elseif _error<-badscore then + hint=string.format( "You're too close to the boat!") + elseif _error<-lowscore then + hint=string.format("You're slightly too far from the boat.") + else + hint=string.format("Good distance to the boat.") + end + + -- Extend or decrease depending on skill. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + -- Also inform students about optimal value. + hint=hint..string.format(" Optimal distance is %.1f NM.", UTILS.MetersToNM(optdist)) + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + -- We keep it short normally. + elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then + -- No hint at all for the pros. + hint="" + end + + -- Debriefing text. + local debrief=string.format("Distance %.1f NM = %d%% deviation from %.1f NM.",UTILS.MetersToNM(distance), _error, UTILS.MetersToNM(optdist)) + + return hint, debrief +end + +--- Score for correct AoA. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #number optaoa Optimal AoA. +-- @return #string Feedback message text or easy and normal difficulty level or nil for hard. +-- @return #string Debriefing text. +function AIRBOSS:_AoACheck(playerData, optaoa) + + if optaoa==nil then + return nil, nil + end + + -- Get relative score. + local lowscore, badscore = self:_GetGoodBadScore(playerData) + + -- Player AoA + local aoa=playerData.unit:GetAoA() + + -- Altitude error +-X% + local _error=(aoa-optaoa)/optaoa*100 + + -- Get aircraft AoA parameters. + local aircraftaoa=self:_GetAircraftAoA(playerData) + + -- Radio call for flight students. + local radiocall=nil --#AIRBOSS.RadioCall + + -- Rate aoa. + local hint="" + local loud=false + if aoa>=aircraftaoa.SLOW then + --hint="Your're slow!" + radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.SLOW, nil, nil, 5) + loud=true + elseif aoa>=aircraftaoa.Slow then + --hint="Your're slow." + radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.SLOW, nil, nil, 5) + elseif aoa>=aircraftaoa.OnSpeedMax then + hint="Your're a little slow." + elseif aoa>=aircraftaoa.OnSpeedMin then + hint="You're on speed." + elseif aoa>=aircraftaoa.Fast then + hint="You're a little fast." + elseif aoa>=aircraftaoa.FAST then + --hint="Your're fast." + radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.FAST, nil, nil, 5) + else + --hint="You're fast!" + radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.FAST, nil, nil, 5) + loud=true + end + + -- Extend or decrease depending on skill. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + -- Also inform students about optimal value. + hint=hint..string.format(" Optimal AoA is %.1f.", self:_AoADeg2Units(playerData, optaoa)) + self:Sound2Player(playerData, self.LSORadio, radiocall, loud) + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + -- We keep is short normally. + self:Sound2Player(playerData, self.LSORadio, radiocall, loud) + elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then + -- No hint at all for the pros. + hint="" + end + + -- Debriefing text. + local debrief=string.format("AoA %.1f = %d%% deviation from %.1f.", self:_AoADeg2Units(playerData, aoa), _error, self:_AoADeg2Units(playerData, optaoa)) + + return hint, debrief +end + +--- Evaluate player's speed. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #number speedopt Optimal speed in m/s. +-- @return #string Feedback text. +-- @return #string Debriefing text. +function AIRBOSS:_SpeedCheck(playerData, speedopt) + + if speedopt==nil then + return nil, nil + end + + -- Player altitude. + local speed=playerData.unit:GetVelocityMPS() + + -- Get relative score. + local lowscore, badscore=self:_GetGoodBadScore(playerData) + + -- Altitude error +-X% + local _error=(speed-speedopt)/speedopt*100 + + -- Radio call for flight students. + local radiocall=nil --#AIRBOSS.RadioCall + + local hint="" + local loud=false + if _error>badscore then + --hint=string.format("You're fast.") + radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.FAST, "AIRBOSS", nil, 5) + loud=true + elseif _error>lowscore then + --hint= string.format("You're slightly fast.") + radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.FAST, "AIRBOSS", nil, 5) + elseif _error<-badscore then + --hint=string.format("You're slow.") + radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.SLOW, "AIRBOSS", nil, 5) + elseif _error<-lowscore then + --hint=string.format("You're slightly slow.") + radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.SLOW, "AIRBOSS", nil, 5) + loud=true + else + hint=string.format("Good speed.") + end + + -- Extend or decrease depending on skill. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + hint=hint..string.format(" Optimal speed is %d knots.", UTILS.MpsToKnots(speedopt)) + self:Sound2Player(playerData, self.LSORadio, radiocall, loud) + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + -- We keep is short normally. + self:Sound2Player(playerData, self.LSORadio, radiocall, loud) + elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then + -- No hint at all for pros. + hint="" + end + + -- Debrief text. + local debrief=string.format("Speed %d knots = %d%% deviation from %d knots.", UTILS.MpsToKnots(speed), _error, UTILS.MpsToKnots(speedopt)) + + return hint, debrief +end + +--- Append text to debriefing. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string hint Debrief text of this step. +-- @param #string step (Optional) Current step in the pattern. Default from playerData. +function AIRBOSS:_AddToDebrief(playerData, hint, step) + step=step or playerData.step + table.insert(playerData.debrief, {step=step, hint=hint}) +end + +--- Debrief player and set next step. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_Debrief(playerData) + self:F(self.lid..string.format("Debriefing of player %s.", playerData.name)) + + -- Switch attitude monitor off if on. + playerData.attitudemonitor=false + + -- LSO grade, points, and flight data analyis. + local grade, points, analysis=self:_LSOgrade(playerData) + + -- Insert points to table of all points until player landed. + if points and points>=0 then + table.insert(playerData.points, points) + end + + -- Player has landed and is not airborne any more. + local Points=0 + if playerData.landed and not playerData.unit:InAir() then + + -- Average over all points received so far. + for _,_points in pairs(playerData.points) do + Points=Points+_points + end + + -- This is the final points. + Points=Points/#playerData.points + + -- Reset points array. + playerData.points={} + else + -- Player boltered or was waved off ==> We display the normal points. + Points=points + end + + -- My LSO grade. + local mygrade={} --#AIRBOSS.LSOgrade + mygrade.grade=grade + mygrade.points=points + mygrade.details=analysis + mygrade.wire=playerData.wire + mygrade.Tgroove=playerData.Tgroove + if playerData.landed and not playerData.unit:InAir() then + mygrade.finalscore=Points + end + mygrade.case=playerData.case + + -- Add LSO grade to player grades table. + table.insert(self.playerscores[playerData.name], mygrade) + + -- LSO grade: (OK) 3.0 PT - LURIM + local text=string.format("%s %.1f PT - %s", grade, Points, analysis) + if Points==-1 then + text=string.format("%s n/a PT - Foul deck", grade, Points, analysis) + end + + -- Wire and Groove time only if not pattern WO. + if not (playerData.patternwo or playerData.fouldeckwo) then + + -- Wire trapped. Not if pattern WI. + if playerData.wire and playerData.wire<=4 then + text=text..string.format(" %d-wire", playerData.wire) + end + + -- Time in the groove. Only Case I/II and not pattern WO. + if playerData.Tgroove and playerData.Tgroove<=60 and playerData.case<3 then + text=text..string.format("\nTime in the groove %d seconds: %s", playerData.Tgroove, self:_EvalGrooveTime(playerData)) + end + + end + + -- Copy debriefing text. + playerData.lastdebrief=UTILS.DeepCopy(playerData.debrief) + + -- Info text. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + text=text..string.format("\nYour detailed debriefing can be found via the F10 radio menu.") + end + + -- Message. + self:MessageToPlayer(playerData, text, "LSO", "", 30, true) + + + -- Set step to undefined and check if other cases apply. + playerData.step=AIRBOSS.PatternStep.UNDEFINED + + + -- Check what happened? + if playerData.patternwo then + + ---------------------- + -- Pattern Wave Off -- + ---------------------- + + -- Next step? + -- TODO: CASE I: After bolter/wo turn left and climb to 600 ft and re-enter the pattern. But do not go to initial but reenter earlier? + -- TODO: CASE I: After pattern wo? go back to initial, I guess? + -- TODO: CASE III: After bolter/wo turn left and climb to 1200 ft and re-enter pattern? + -- TODO: CASE III: After pattern wo? No idea... + + -- Can become nil when I crashed and changed to observer. Which events are captured? Nil check for unit? + + if playerData.unit:IsAlive() then + + -- Heading and distance tip. + local heading, distance + + if playerData.case==1 or playerData.case==2 then + + -- Next step: Initial again. + playerData.step=AIRBOSS.PatternStep.INITIAL + + -- Create a point 3.0 NM astern for re-entry. + local initial=self:GetCoordinate():Translate(UTILS.NMToMeters(3.5), self:GetRadial(2, false, false, false)) + + -- Get heading and distance to initial zone ~3 NM astern. + heading=playerData.unit:GetCoordinate():HeadingTo(initial) + distance=playerData.unit:GetCoordinate():Get2DDistance(initial) + + elseif playerData.case==3 then + + -- Next step? Bullseye for now. + -- TODO: Could be DIRTY UP or PLATFORM or even back to MARSHAL STACK? + playerData.step=AIRBOSS.PatternStep.BULLSEYE + + -- Get heading and distance to bullseye zone ~3 NM astern. + local zone=self:_GetZoneBullseye(playerData.case) + + heading=playerData.unit:GetCoordinate():HeadingTo(zone:GetCoordinate()) + distance=playerData.unit:GetCoordinate():Get2DDistance(zone:GetCoordinate()) + + end + + -- Re-enter message. + local text=string.format("fly heading %03d° for %d NM to re-enter the pattern.", heading, UTILS.MetersToNM(distance)) + self:MessageToPlayer(playerData, text, "LSO", nil, nil, false, 5) + + else + + -- Unit does not seem to be alive! + -- TODO: What now? + self:T2(self.lid..string.format("Player unit not alive!")) + + end + + elseif playerData.fouldeckwo then + + --------------- + -- Foul Deck -- + --------------- + + if playerData.unit:InAir() then + + -- Bolter pattern. Then Abeam or bullseye. + playerData.step=AIRBOSS.PatternStep.BOLTER + + else + + -- Welcome aboard! + self:Sound2Player(playerData, self.LSORadio, AIRBOSS.LSOCall.WELCOMEABOARD) + + -- Airboss talkto! + local text=string.format("the deck was fouled but landed anyway. Airboss wants to talk to you!") + self:MessageToPlayer(playerData, text, "LSO", nil, nil, false, 3) + + end + + + elseif playerData.waveoff then + + -------------- + -- Wave Off -- + -------------- + + if playerData.unit:InAir() then + + -- Bolter pattern. Then Abeam or bullseye. + playerData.step=AIRBOSS.PatternStep.BOLTER + + else + + -- Welcome aboard! + self:Sound2Player(playerData, self.LSORadio, AIRBOSS.LSOCall.WELCOMEABOARD) + + -- Airboss talkto! + local text=string.format("you were waved off but landed anyway. Airboss wants to talk to you!") + self:MessageToPlayer(playerData, text, "LSO", nil, nil, false, 3) + + end + + elseif playerData.boltered then + + -------------- + -- Boltered -- + -------------- + + if playerData.unit:InAir() then + + -- Bolter pattern. Then Abeam or bullseye. + playerData.step=AIRBOSS.PatternStep.BOLTER + + end + + elseif playerData.landed then + + ------------ + -- Landed -- + ------------ + + if not playerData.unit:InAir() then + + -- Welcome aboard! + self:Sound2Player(playerData, self.LSORadio, AIRBOSS.LSOCall.WELCOMEABOARD) + + end + + else + + -- Message to player. + self:MessageToPlayer(playerData, "Undefined state after landing! Please report.", "ERROR", nil, 20) + + -- Next step. + playerData.step=AIRBOSS.PatternStep.UNDEFINED + + end + + -- Player landed and is not in air anymore. + if playerData.landed and not playerData.unit:InAir() then + -- TODO: This is not 100% correct if player group has some AI units. But we do it anyway since otherwise player will + self:_RemoveFlightFromQueue(self.Qpattern, playerData) + end + + -- Increase number of passes. + playerData.passes=playerData.passes+1 + + -- Next step hint for students if any. + self:_StepHint(playerData) + + -- Reinitialize player data for new approach. + self:_InitPlayer(playerData, playerData.step) + + -- Debug message. + MESSAGE:New(string.format("Player step %s.", playerData.step), 5, "DEBUG"):ToAllIf(self.Debug) + + -- Auto save player results. + if self.autosave and mygrade.finalscore then + self:Save(self.autosavepath, self.autosavefile) + end +end + +--- Display hint for flight students about the (next) step. Message is displayed after one second. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string step Step for which hint is given. +function AIRBOSS:_StepHint(playerData, step) + + -- Set step. + step=step or playerData.step + + -- Message is only for "Flight Students". + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + + -- Get optimal parameters at step. + local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData, step) + + -- Hint: + local hint="" + + -- Altitude. + if alt then + hint=hint..string.format("\nAltitude %d ft", UTILS.MetersToFeet(alt)) + end + + -- AoA. + if aoa then + hint=hint..string.format("\nAoA %.1f", aoa) + end + + -- Speed. + if speed then + hint=hint..string.format("\nSpeed %d knots", UTILS.MpsToKnots(speed)) + end + + -- Distance to the boat. + if dist then + hint=hint..string.format("\nDistance to the boat %.1f NM", UTILS.MetersToNM(dist)) + end + + -- Check if there was actually anything to tell. + if hint~="" then + + -- Compile text if any. + local text=string.format("Optimal setup at next step %s:%s", step, hint) + + -- Send hint to player. + self:MessageToPlayer(playerData, text, "AIRBOSS", "", nil, false, 1) + + end + + end +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- CARRIER ROUTING Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check for possible collisions between two coordinates. +-- @param #AIRBOSS self +-- @param Core.Point#COORDINATE coordto Coordinate to which the collision is check. +-- @param Core.Point#COORDINATE coordfrom Coordinate from which the collision is check. +-- @return #boolean If true, surface type ahead is not deep water. +-- @return #number Max free distance in meters. +function AIRBOSS:_CheckCollisionCoord(coordto, coordfrom) + + -- Increment in meters. + local dx=100 + + -- From coordinate. Default 500 in front of the carrier. + local d=0 + if coordfrom then + d=0 + else + d=250 + coordfrom=self:GetCoordinate():Translate(d, self:GetHeading()) + end + + -- Distance between the two coordinates. + local dmax=coordfrom:Get2DDistance(coordto) + + -- Direction. + local direction=coordfrom:HeadingTo(coordto) + + -- Scan path between the two coordinates. + local clear=true + while d<=dmax do + + -- Check point. + local cp=coordfrom:Translate(d, direction) + + -- Check if surface type is water. + if not cp:IsSurfaceTypeWater() then + + -- Debug mark points. + if self.Debug then + local st=cp:GetSurfaceType() + cp:MarkToAll(string.format("Collision check surface type %d", st)) + end + + -- Collision WARNING! + clear=false + break + end + + -- Increase distance. + d=d+dx + end + + local text="" + if clear then + text=string.format("Path into direction %03d° is clear for the next %.1f NM.", direction, UTILS.MetersToNM(d)) + else + text=string.format("Detected obstacle at distance %.1f NM into direction %03d°.", UTILS.MetersToNM(d), direction) + end + self:T2(self.lid..text) + + return not clear, d +end + + +--- Check Collision. +-- @param #AIRBOSS self +-- @param Core.Point#COORDINATE fromcoord Coordinate from which the path to the next WP is calculated. Default current carrier position. +-- @return #boolean If true, surface type ahead is not deep water. +function AIRBOSS:_CheckFreePathToNextWP(fromcoord) + + -- Position. + fromcoord=fromcoord or self:GetCoordinate():Translate(250, self:GetHeading()) + + -- Next wp = current+1 (or last) + local Nnextwp=math.min(self.currentwp+1, #self.waypoints) + + -- Next waypoint. + local nextwp=self.waypoints[Nnextwp] --Core.Point#COORDINATE + + -- Check for collision. + local collision=self:_CheckCollisionCoord(nextwp, fromcoord) + + return collision +end + +--- Find free path to the next waypoint. +-- @param #AIRBOSS self +function AIRBOSS:_Pathfinder() + + -- Heading and current coordiante. + local hdg=self:GetHeading() + local cv=self:GetCoordinate() + + -- Possible directions. + local directions={-20, 20, -30, 30, -40, 40, -50, 50, -60, 60, -70, 70, -80, 80, -90, 90, -100, 100} + + -- Starboard turns up to 90 degrees. + for _,_direction in pairs(directions) do + + -- New direction. + local direction=hdg+_direction + + -- Check for collisions in the next 20 NM of the current direction. + local _, dfree=self:_CheckCollisionCoord(cv:Translate(UTILS.NMToMeters(20), direction), cv) + + -- Loop over distances and find the first one which gives a clear path to the next waypoint. + local distance=500 + while distance<=dfree do + + -- Coordinate from which we calculate the path. + local fromcoord=cv:Translate(distance, direction) + + -- Check for collision between point and next waypoint. + local collision=self:_CheckFreePathToNextWP(fromcoord) + + -- Debug info. + self:T2(self.lid..string.format("Pathfinder d=%.1f m, direction=%03d°, collision=%s", distance, direction, tostring(collision))) + + -- If path is clear, we start a little detour. + if not collision then + self:CarrierDetour(fromcoord) + return + end + + distance=distance+500 + end + end +end + + +--- Carrier resumes the route at its next waypoint. +--@param #AIRBOSS self +--@param Core.Point#COORDINATE gotocoord (Optional) First goto this coordinate before resuming route. +--@return #AIRBOSS self +function AIRBOSS:CarrierResumeRoute(gotocoord) + + -- Make carrier resume its route. + AIRBOSS._ResumeRoute(self.carrier:GetGroup(), self, gotocoord) + + return self +end + + +--- Let the carrier make a detour to a given point. When it reaches the point, it will resume its normal route. +-- @param #AIRBOSS self +-- @param Core.Point#COORDINATE coord Coordinate of the detour. +-- @param #number speed Speed in knots. Default is current carrier velocity. +-- @param #boolean uturn (Optional) If true, carrier will go back to where it came from before it resumes its route to the next waypoint. +-- @return #AIRBOSS self +function AIRBOSS:CarrierDetour(coord, speed, uturn) + + -- Current coordinate of the carrier. + local pos0=self:GetCoordinate() + + -- Speed in km/h. + local speedkmh=UTILS.KnotsToKmph(speed or self.carrier:GetVelocityKNOTS()) + + -- Waypoint table. + local wp={} + + -- Create from/to waypoints. + table.insert(wp, pos0:WaypointGround(speedkmh)) + table.insert(wp, coord:WaypointGround(speedkmh)) + + -- If enabled, go back to where you came from. + if uturn then + table.insert(wp, pos0:WaypointGround(speedkmh)) + end + + -- Get carrier group. + local group=self.carrier:GetGroup() + + -- Passing waypoint taskfunction + local TaskResumeRoute=group:TaskFunction("AIRBOSS._ResumeRoute", self) + + -- Set task to restart route at the last point. + group:SetTaskWaypoint(wp[#wp], TaskResumeRoute) + + -- Debug mark. + if self.Debug then + coord:MarkToAll("Detour Point") + end + + -- Detour switch true. + self.detour=true + + -- Route carrier into the wind. + self.carrier:Route(wp) +end + +--- Let the carrier turn into the wind. +-- @param #AIRBOSS self +-- @param #number distance Distance in NM from current position. +-- @param #number speed Speed in knots. Default 15 knots. +-- @return #AIRBOSS self +function AIRBOSS:CarrierTurnIntoWind(distance, speed) + + -- Time in seconds. + local time=distance/speed*3600 + + -- Debug message. + self:T(self.lid..string.format("Carrier steaming into the wind. Distance=%.1f NM, Speed=%.1f knots, Time=%d sec.", distance, speed, time)) + + -- Get heading into the wind accounting for angled runway. + local intowind=self:GetHeadingIntoWind(false) + + -- Length of path into the wind in meters. + local dist=UTILS.NMToMeters(distance) + + -- Translate current position. + local pos1=self:GetCoordinate():Translate(dist, intowind) + + -- Debug mark. + if self.Debug then + pos1:MarkToAll("Into the wind point") + end + + -- Speed in km/h. + local speedkmh=UTILS.KnotsToKmph(speed) + + -- Return to coordinate if collision is detected. + self.Creturnto=self:GetCoordinate() + + -- Let the carrier make a detour from its route but return to its current position. + self:CarrierDetour(pos1, speed, true) + + -- Set switch that we are currently turning into the wind. + self.turnintowind=true + + return self +end + +--- Patrol carrier. +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:_PatrolRoute() + + -- Get carrier group. + local CarrierGroup=self.carrier:GetGroup() + + -- Waypoints of group. + local Waypoints = CarrierGroup:GetTemplateRoutePoints() + + -- Loop over waypoints. + for n=1,#Waypoints do + + -- Passing waypoint taskfunction + local TaskPassingWP=CarrierGroup:TaskFunction("AIRBOSS._PassingWaypoint", self, n, #Waypoints) + + -- Call task function when carrier arrives at waypoint. + CarrierGroup:SetTaskWaypoint(Waypoints[n], TaskPassingWP) + end + + -- Set waypoint table. + local i=1 + for _,point in ipairs(Waypoints) do + + -- Coordinate of the waypoint + local coord=COORDINATE:New(point.x, point.alt, point.y) + + -- Set velocity of the coordinate. + coord:SetVelocity(point.speed) + + -- Add to table. + table.insert(self.waypoints, coord) + + -- Debug info. + if self.Debug then + coord:MarkToAll(string.format("Carrier Waypoint %d, Speed=%.1f knots", i, UTILS.MpsToKnots(point.speed))) + end + + -- Increase counter. + i=i+1 + end + + -- Current waypoint is 1. + self.currentwp=1 + + -- Route carrier group. + CarrierGroup:Route(Waypoints) + + return self +end + +--- Estimated the carrier position at some point in the future given the current waypoints and speeds. +-- @param #AIRBOSS self +-- @return DCS#time ETA abs. time in seconds. +function AIRBOSS:_GetETAatNextWP() + + -- Current waypoint + local cwp=self.currentwp + + -- Current abs. time. + local tnow=timer.getAbsTime() + + -- Current position. + local p=self:GetCoordinate() + + -- Current velocity [m/s]. + local v=self.carrier:GetVelocityMPS() + + -- Distance to next waypoint. + local s=0 + if #self.waypoints>cwp then + s=p:Get2DDistance(self.waypoints[cwp+1]) + end + + -- v=s/t <==> t=s/v + local t=s/v + + -- ETA + local eta=t+tnow + + return eta +end + + +--- Check if heading or position of carrier have changed significantly. +-- @param #AIRBOSS self +function AIRBOSS:_CheckPatternUpdate() + + -- TODO: Make parameters input values. + + -- Min 10 min between pattern updates. + local dTPupdate=10*60 + + -- Update if carrier moves by more than 2.5 NM. + local Dupdate=UTILS.NMToMeters(2.5) + + -- Update if carrier turned by more than 5 degrees. + local Hupdate=5 + + -- Time since last pattern update + local dt=timer.getTime()-self.Tpupdate + + -- At least 10 min between updates. Not yet... + if dt=1 + + -- No update if carrier is turning! + if turning then + self:T2(self.lid..string.format("Carrier is turning. Delta Heading = %.1f", deltaLast)) + return + end + + -- Check if orientation changed. + local Hchange=false + if math.abs(deltaHeading)>=Hupdate then + self:T(self.lid..string.format("Carrier heading changed by %d degrees. Turning=%s.", deltaHeading, tostring(turning))) + Hchange=true + end + + -- Get distance to saved position. + local dist=pos:Get2DDistance(self.Cposition) + + -- Check if carrier moved more than ~10 km. + local Dchange=false + if dist>=Dupdate then + self:T(self.lid..string.format("Carrier position changed by %.1f NM. Turning=%s.", UTILS.MetersToNM(dist), tostring(turning))) + Dchange=true + end + + -- If heading or distance changed ==> update marshal AI patterns. + if Hchange or Dchange then + + -- Loop over all marshal flights + for _,_flight in pairs(self.Qmarshal) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Update marshal pattern of AI keeping the same stack. + if flight.ai then + self:_MarshalAI(flight, flight.flag:Get()) + end + + end + + -- Inform player about new final bearing. + if Hchange then + -- 99, new final bearing XXX + local FB=self:GetFinalBearing(true) + local text=string.format("new final bearing %03d°.", FB) + self:MessageToMarshal(text, "AIRBOSS", "99") + end + + -- Reset parameters for next update check. + self.Corientation=vNew + self.Cposition=pos + self.Tpupdate=timer.getTime() + end + +end + +--- Function called when a group is passing a waypoint. +--@param Wrapper.Group#GROUP group Group that passed the waypoint +--@param #AIRBOSS airboss Airboss object. +--@param #number i Waypoint number that has been reached. +--@param #number final Final waypoint number. +function AIRBOSS._PassingWaypoint(group, airboss, i, final) + + -- Debug message. + local text=string.format("Group %s passing waypoint %d of %d.", group:GetName(), i, final) + + -- Debug smoke and marker. + if airboss.Debug and false then + local pos=group:GetCoordinate() + pos:SmokeRed() + local MarkerID=pos:MarkToAll(string.format("Group %s reached waypoint %d", group:GetName(), i)) + end + + -- Debug message. + MESSAGE:New(text,10):ToAllIf(airboss.Debug) + airboss:T(airboss.lid..text) + + -- Set current waypoint. + airboss.currentwp=i + + -- If final waypoint reached, do route all over again. + if i==final and final>1 and airboss.adinfinitum then + airboss:_PatrolRoute() + end +end + +--- Carrier Strike Group resumes the route of the waypoints defined in the mission editor. +--@param Wrapper.Group#GROUP group Carrier Strike Group that passed the waypoint. +--@param #AIRBOSS airboss Airboss object. +--@param Core.Point#COORDINATE gotocoord Go to coordinate before route is resumed. +function AIRBOSS._ResumeRoute(group, airboss, gotocoord) + + -- Next wp = current+1 (or last) + local nextwp=math.min(airboss.currentwp+1, #airboss.waypoints) + + -- Debug message. + local text=string.format("Group %s is resuming route. Next waypoint %d.", group:GetName(), nextwp) + + -- Debug message. + MESSAGE:New(text,10):ToAllIf(airboss.Debug) + airboss:T(airboss.lid..text) + + -- Waypoints array. + local waypoints={} + + -- Get current velocity in km/h. + local velocity=group:GetVelocityKMH() + + -- Current positon as first waypoint. + local wp0=group:GetCoordinate():WaypointGround(velocity) + table.insert(waypoints, wp0) + + if gotocoord then + local wp1=gotocoord:WaypointGround(velocity) + table.insert(waypoints, wp1) + end + + -- Loop over all remaining waypoints. + for i=nextwp, #airboss.waypoints do + + -- Coordinate of the next WP. + local coord=airboss.waypoints[i] --Core.Point#COORDINATE + + -- Speed in km/h of that WP. Velocity is in m/s. + local speed=coord.Velocity*3.6 + + -- Create waypoint. + local wp=coord:WaypointGround(speed) + + -- Passing waypoint task function. + local TaskPassingWP=group:TaskFunction("AIRBOSS._PassingWaypoint", airboss, i, #airboss.waypoints) + + -- Call task function when carrier arrives at waypoint. + group:SetTaskWaypoint(wp, TaskPassingWP) + + -- Add waypoints to table. + table.insert(waypoints, wp) + end + + -- Set turn into wind switch false. + airboss.turnintowind=false + airboss.detour=false + + -- Route group. + group:Route(waypoints) +end + +--- Function called when a group has reached the holding zone. +--@param Wrapper.Group#GROUP group Group that reached the holding zone. +--@param #AIRBOSS airboss Airboss object. +--@param #AIRBOSS.FlightGroup flight Flight group that has reached the holding zone. +function AIRBOSS._ReachedHoldingZone(group, airboss, flight) + + -- Debug message. + local text=string.format("Flight %s reached holding zone.", group:GetName()) + MESSAGE:New(text,10):ToAllIf(airboss.Debug) + airboss:T(airboss.lid..text) + + -- Debug mark. + if airboss.Debug then + group:GetCoordinate():MarkToAll(text) + end + + -- Set holding flag true and set timestamp for marshal time check. + if flight then + flight.holding=true + flight.time=timer.getAbsTime() + end +end + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- MISC functions +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get aircraft nickname. +-- @param #AIRBOSS self +-- @param #string actype Aircraft type name. +-- @return #string Aircraft nickname. E.g. "Hornet" for the F/A-18C or "Tomcat" For the F-14A. +function AIRBOSS:_GetACNickname(actype) + + local nickname="unknown" + if actype==AIRBOSS.AircraftCarrier.A4EC then + nickname="Skyhawk" + elseif actype==AIRBOSS.AircraftCarrier.AV8B then + nickname="Harrier" + elseif actype==AIRBOSS.AircraftCarrier.E2D then + nickname="Hawkeye" + elseif actype==AIRBOSS.AircraftCarrier.F14A_AI or actype==AIRBOSS.AircraftCarrier.F14A or actype==AIRBOSS.AircraftCarrier.F14B then + nickname="Tomcat" + elseif actype==AIRBOSS.AircraftCarrier.FA18C or actype==AIRBOSS.AircraftCarrier.HORNET then + nickname="Hornet" + elseif actype==AIRBOSS.AircraftCarrier.S3B or actype==AIRBOSS.AircraftCarrier.S3BTANKER then + nickname="Viking" + end + + return nickname +end + +--- Get onboard number of player or client. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @return #string Onboard number as string. +function AIRBOSS:_GetOnboardNumberPlayer(group) + return self:_GetOnboardNumbers(group, true) +end + +--- Get onboard numbers of all units in a group. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @param #boolean playeronly If true, return the onboard number for player or client skill units. +-- @return #table Table of onboard numbers. +function AIRBOSS:_GetOnboardNumbers(group, playeronly) + --self:F({groupname=group:GetName}) + + -- Get group name. + local groupname=group:GetName() + + -- Debug text. + local text=string.format("Onboard numbers of group %s:", groupname) + + -- Units of template group. + local units=group:GetTemplate().units + + -- Get numbers. + local numbers={} + for _,unit in pairs(units) do + + -- Onboard number and unit name. + local n=tostring(unit.onboard_num) + local name=unit.name + local skill=unit.skill + + -- Debug text. + text=text..string.format("\n- unit %s: onboard #=%s skill=%s", name, n, skill) + + if playeronly and skill=="Client" or skill=="Player" then + -- There can be only one player in the group, so we skip everything else. + return n + end + + -- Table entry. + numbers[name]=n + end + + -- Debug info. + self:T2(self.lid..text) + + return numbers +end + +--- Check if aircraft is capable of landing on an aircraft carrier. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. (Will also work with groups as given parameter.) +-- @return #boolean If true, aircraft can land on a carrier. +function AIRBOSS:_IsCarrierAircraft(unit) + local carrieraircraft=false + local aircrafttype=unit:GetTypeName() + for _,actype in pairs(AIRBOSS.AircraftCarrier) do + if actype==aircrafttype then + return true + end + end + return false +end + +--- Checks if a human player sits in the unit. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. +-- @return #boolean If true, human player inside the unit. +function AIRBOSS:_IsHumanUnit(unit) + + -- Get player unit or nil if no player unit. + local playerunit=self:_GetPlayerUnitAndName(unit:GetName()) + + if playerunit then + return true + else + return false + end +end + +--- Checks if a group has a human player. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @return #boolean If true, human player inside group. +function AIRBOSS:_IsHuman(group) + + -- Get all units of the group. + local units=group:GetUnits() + + -- Loop over all units. + for _,_unit in pairs(units) do + -- Check if unit is human. + local human=self:_IsHumanUnit(_unit) + if human then + return true + end + end + + return false +end + +--- Get fuel state in pounds. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit The unit for which the mass is determined. +-- @return #number Fuel state in pounds. +function AIRBOSS:_GetFuelState(unit) + + -- Get relative fuel [0,1]. + local fuel=unit:GetFuel() + + -- Get max weight of fuel in kg. + local maxfuel=self:_GetUnitMasses(unit) + + -- Fuel state, i.e. what let's + local fuelstate=fuel*maxfuel + + -- Debug info. + self:T2(self.lid..string.format("Unit %s fuel state = %.1f kg = %.1f lbs", unit:GetName(), fuelstate, UTILS.kg2lbs(fuelstate))) + + return UTILS.kg2lbs(fuelstate) +end + +--- Convert altitude from meters to angels (thousands of feet). +-- @param #AIRBOSS self +-- @param alt Alitude in meters. +-- @return #number Altitude in Anglels = thousands of feet using math.floor(). +function AIRBOSS:_GetAngels(alt) + + if alt then + local angels=UTILS.Round(UTILS.MetersToFeet(alt)/1000, 0) + return angels + else + return 0 + end + +end + +--- Get unit masses especially fuel from DCS descriptor values. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit The unit for which the mass is determined. +-- @return #number Mass of fuel in kg. +-- @return #number Empty weight of unit in kg. +-- @return #number Max weight of unit in kg. +-- @return #number Max cargo weight in kg. +function AIRBOSS:_GetUnitMasses(unit) + + -- Get DCS descriptors table. + local Desc=unit:GetDesc() + + -- Mass of fuel in kg. + local massfuel=Desc.fuelMassMax or 0 + + -- Mass of empty unit in km. + local massempty=Desc.massEmpty or 0 + + -- Max weight of unit in kg. + local massmax=Desc.massMax or 0 + + -- Rest is cargo. + local masscargo=massmax-massfuel-massempty + + -- Debug info. + self:T2(self.lid..string.format("Unit %s mass fuel=%.1f kg, empty=%.1f kg, max=%.1f kg, cargo=%.1f kg", unit:GetName(), massfuel, massempty, massmax, masscargo)) + + return massfuel, massempty, massmax, masscargo +end + +--- Get player data from unit object +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Unit in question. +-- @return #AIRBOSS.PlayerData Player data or nil if not player with this name or unit exists. +function AIRBOSS:_GetPlayerDataUnit(unit) + if unit:IsAlive() then + local unitname=unit:GetName() + local playerunit,playername=self:_GetPlayerUnitAndName(unitname) + if playerunit and playername then + return self.players[playername] + end + end + return nil +end + + +--- Get player data from group object. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Group in question. +-- @return #AIRBOSS.PlayerData Player data or nil if not player with this name or unit exists. +function AIRBOSS:_GetPlayerDataGroup(group) + local units=group:GetUnits() + for _,unit in pairs(units) do + local playerdata=self:_GetPlayerDataUnit(unit) + if playerdata then + return playerdata + end + end + return nil +end + +--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +-- @return Wrapper.Unit#UNIT Unit of player or nil. +-- @return #string Name of the player or nil. +function AIRBOSS:_GetPlayerUnitAndName(_unitName) + self:F2(_unitName) + + if _unitName ~= nil then + + -- Get DCS unit from its name. + local DCSunit=Unit.getByName(_unitName) + + if DCSunit then + + local playername=DCSunit:getPlayerName() + local unit=UNIT:Find(DCSunit) + + self:T2({DCSunit=DCSunit, unit=unit, playername=playername}) + if DCSunit and unit and playername then + return unit, playername + end + + end + + end + + -- Return nil if we could not find a player. + return nil,nil +end + +--- Get carrier coalition. +-- @param #AIRBOSS self +-- @return #number Coalition side of carrier. +function AIRBOSS:GetCoalition() + return self.carrier:GetCoalition() +end + +--- Get carrier coordinate. +-- @param #AIRBOSS self +-- @return Core.Point#COORDINATE Carrier coordinate. +function AIRBOSS:GetCoordinate() + return self.carrier:GetCoordinate() +end + + +--- Get static weather of this mission from env.mission.weather. +-- @param #AIRBOSS self +-- @param #table Clouds table which has entries "thickness", "density", "base", "iprecptns". +-- @param #number Visibility distance in meters. +-- @param #table Fog table, which has entries "thickness", "visibility" or nil if fog is disabled in the mission. +-- @param #number Dust density or nil if dust is disabled in the mission. +function AIRBOSS:_GetStaticWeather() + + -- Weather data from mission file. + local weather=env.mission.weather + + -- Clouds + --[[ + ["clouds"] = + { + ["thickness"] = 430, + ["density"] = 7, + ["base"] = 0, + ["iprecptns"] = 1, + }, -- end of ["clouds"] + ]] + local clouds=weather.clouds + + -- Visibilty distance in meters. + local visibility=weather.visibility.distance + + -- Dust + --[[ + ["enable_dust"] = false, + ["dust_density"] = 0, + ]] + local dust=nil + if weather.enable_dust==true then + dust=weather.dust_density + end + + -- Fog + --[[ + ["enable_fog"] = false, + ["fog"] = + { + ["thickness"] = 0, + ["visibility"] = 25, + }, -- end of ["fog"] + ]] + local fog=nil + if weather.enable_fog==true then + fog=weather.fog + end + + + return clouds, visibility, fog, dust +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- RADIO MESSAGE Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Radio queue item. +-- @type AIRBOSS.Radioitem +-- @field #number Tplay Abs time when transmission should be played. +-- @field #number Tstarted Abs time when transmission began to play. +-- @field #number prio Priority 0-100. +-- @field #boolean isplaying Currently playing. +-- @field #AIRBOSS.Radio radio Radio object. +-- @field #AIRBOSS.RadioCall call Radio call. +-- @field #boolean loud If true, play loud version of file. + +--- Check radio queue for transmissions to be broadcasted. +-- @param #AIRBOSS self +-- @param #table radioqueue The radio queue. +-- @param #string name Name of the queue. +function AIRBOSS:_CheckRadioQueue(radioqueue, name) + + -- Check if queue is empty. + if #radioqueue==0 then + return + end + + -- Get current abs time. + local time=timer.getAbsTime() + + -- Sort results table wrt times they have already been engaged. + local function _sort(a, b) + return (a.Tplay < b.Tplay) or (a.Tplay==b.Tplay and a.prio < b.prio) + end + --table.sort(radioqueue, _sort) + + local playing=false + local next=nil --#AIRBOSS.Radioitem + local remove=nil + for i,_transmission in ipairs(radioqueue) do + local transmission=_transmission --#AIRBOSS.Radioitem + + -- Check if transmission time has passed. + if time>transmission.Tplay then + + -- Check if transmission is currently playing. + if transmission.isplaying then + + -- Check if transmission is finished. + if time>=transmission.Tstarted+transmission.call.duration then + + -- Transmission over. + transmission.isplaying=false + remove=i + --table.insert(remove, i) + + else -- still playing + + -- Transmission is still playing. + playing=true + + end + + else -- not playing yet + + -- Not playing ==> this will be next. + if next==nil then + next=transmission + end + + end + + else + + -- Transmission not due yet. + + end + end + + -- Found a new transmission. + if next~=nil and not playing then + self:Broadcast(next.radio, next.call, next.loud) + next.isplaying=true + next.Tstarted=time + end + + -- Remove completed calls from queue. + --for _,idx in pairs(remove) do + if remove then + table.remove(radioqueue, remove) + end + --end + +end + +--- Add Radio transmission to radio queue. +-- @param #AIRBOSS self +-- @param #AIRBOSS.Radio radio Radio sending the transmission. +-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. +-- @param #boolean loud If true, play loud sound file version. +-- @param #number delay Delay in seconds, before the message is broadcasted. +function AIRBOSS:RadioTransmission(radio, call, loud, delay) + self:F2({radio=radio, call=call, loud=loud, delay=delay}) + + -- Create a new radio transmission item. + local transmission={} --#AIRBOSS.Radioitem + + transmission.radio=radio + transmission.call=call + transmission.Tplay=timer.getAbsTime()+(delay or 0) + transmission.prio=50 + transmission.isplaying=false + transmission.Tstarted=nil + transmission.loud=loud and call.loud + + -- Player onboard number if sender has one. + if self:_IsOnboard(call.modexsender) then + self:_Number2Radio(radio, call.modexsender, delay) + end + + -- Play onboard number if receiver has one. + if self:_IsOnboard(call.modexreceiver) then + self:_Number2Radio(radio, call.modexreceiver, delay) + end + + -- Add transmission to the right queue. + local caller="" + if radio.alias=="LSO" then + + table.insert(self.RQLSO, transmission) + + caller="LSOCall" + + elseif radio.alias=="MARSHAL" then + + table.insert(self.RQMarshal, transmission) + + caller="MarshalCall" + + end + + -- Append radio click sound at the end of the transmission. + if call~=AIRBOSS[caller].CLICK and + call~=AIRBOSS[caller].N0 and + call~=AIRBOSS[caller].N1 and + call~=AIRBOSS[caller].N2 and + call~=AIRBOSS[caller].N3 and + call~=AIRBOSS[caller].N4 and + call~=AIRBOSS[caller].N5 and + call~=AIRBOSS[caller].N6 and + call~=AIRBOSS[caller].N7 and + call~=AIRBOSS[caller].N8 and + call~=AIRBOSS[caller].N9 then + self:RadioTransmission(radio, AIRBOSS[caller].CLICK, false, delay) + end +end + + +--- Check if a call needs a subtitle because the complete voice overs are not available. +-- @param #AIRBOSS self +-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. +-- @return #boolean If true, call needs a subtitle. +function AIRBOSS:_NeedsSubtitle(call) + -- Currently we play the noise file. + if call.file==AIRBOSS.MarshalCall.NOISE.file or call.file==AIRBOSS.LSOCall.NOISE.file then + return true + else + return false + end +end + +--- Broadcast radio message. +-- @param #AIRBOSS self +-- @param #AIRBOSS.Radio radio Radio sending transmission. +-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. +-- @param #boolean loud Play loud version of file. +function AIRBOSS:Broadcast(radio, call, loud) + self:F(call) + + -- Check which sound output method to use. + if not self.usersoundradio then + + ---------------------------- + -- Transmission via Radio -- + ---------------------------- + + -- Get unit sending the transmission. + local sender=self:_GetRadioSender() + + -- Construct file name and subtitle. + local filename=self:_RadioFilename(call, loud) + + -- Create subtitle for transmission. + local subtitle=self:_RadioSubtitle(radio, call, loud) + + -- Debug. + self:T({filename=filename, subtitle=subtitle}) + + if sender then + + -- Broadcasting from aircraft. Only players tuned in to the right frequency will see the message. + self:T(self.lid..string.format("Broadcasting from aircraft %s", sender:GetName())) + + -- Command to set the Frequency for the transmission. + local commandFrequency={ + id="SetFrequency", + params={ + frequency=radio.frequency*1000000, -- Frequency in Hz. + modulation=radio.modulation, + }} + + -- Command to tranmit the call. + local commandTransmit={ + id = "TransmitMessage", + params = { + file=filename, + duration=call.subduration or 5, + subtitle=subtitle, + loop=false, + }} + + -- Set commend for frequency + sender:SetCommand(commandFrequency) + + -- Set command for radio transmission. + sender:SetCommand(commandTransmit) + + else + + -- Broadcasting from carrier. No subtitle possible. Need to send messages to players. + self:T(self.lid..string.format("Broadcasting from carrier via trigger.action.radioTransmission().")) + + -- Transmit from carrier position. + local vec3=self.carrier:GetPositionVec3() + + -- Transmit via trigger. + trigger.action.radioTransmission(filename, vec3, radio.modulation, false, radio.frequency*1000000, 100) + + -- Display subtitle of message to players. + for _,_player in pairs(self.players) do + local playerData=_player --#AIRBOSS.PlayerData + + -- Message to all players in CCA that have subtites on. + if playerData.unit:IsInZone(self.zoneCCA) and playerData.actype~=AIRBOSS.AircraftCarrier.A4EC then + + -- Only to players with subtitle on or if noise is played. + if playerData.subtitles or self:_NeedsSubtitle(call) then + + -- Messages to marshal to everyone. Messages on LSO radio only to those in the pattern. + if radio.alias=="MARSHAL" or (radio.alias=="LSO" and self:_InQueue(self.Qpattern, playerData.group)) then + + -- Message to player. + self:MessageToPlayer(playerData, subtitle, nil, "", call.subduration or 5) + + end + + end + + end + end + end + end + + ---------------- + -- Easy Comms -- + ---------------- + + -- Workaround for the community A-4E-C as long as their radios are not functioning properly. + for _,_player in pairs(self.players) do + local playerData=_player --#AIRBOSS.PlayerData + + -- Easy comms if globally activated but definitly for all player in the community A-4E. + if self.usersoundradio or playerData.actype==AIRBOSS.AircraftCarrier.A4EC then + + -- Messages to marshal to everyone. Messages on LSO radio only to those in the pattern. + if radio.alias=="MARSHAL" or (radio.alias=="LSO" and self:_InQueue(self.Qpattern, playerData.group)) then + + -- User sound to players (inside CCA). + self:Sound2Player(playerData, radio, call, loud) + end + + end + end + +end + +--- Player user sound to player if he is inside the CCA. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #AIRBOSS.Radio radio The radio used for transmission. +-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. +-- @param #boolean loud If true, play loud sound file version. +-- @param #number delay Delay in seconds, before the message is broadcasted. +function AIRBOSS:Sound2Player(playerData, radio, call, loud, delay) + + -- Only to players inside the CCA. + if playerData.unit:IsInZone(self.zoneCCA) and call then + + -- Construct file name. + local filename=self:_RadioFilename(call, loud) + + -- Get Subtitle + local subtitle=self:_RadioSubtitle(radio, call, loud) + + -- Play sound file via usersound trigger. + USERSOUND:New(filename):ToGroup(playerData.group, delay) + + -- Only to players with subtitle on or if noise is played. + if playerData.subtitles or self:_NeedsSubtitle(call) then + self:MessageToPlayer(playerData, subtitle, nil, "", call.subduration, false, delay) + end + + end +end + +--- Create radio subtitle from radio call. +-- @param #AIRBOSS self +-- @param #AIRBOSS.Radio radio The radio used for transmission. +-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. +-- @param #boolean loud If true, append "!" else ".". +-- @return #string Subtitle to be displayed. +function AIRBOSS:_RadioSubtitle(radio, call, loud) + + -- No subtitle if call is nil, or subtitle is nil or subtitle is empty. + if call==nil or call.subtitle==nil or call.subtitle=="" then + return "" + end + + -- Sender + local sender=call.sender or radio.alias + if call.modexsender then + sender=call.modexsender + end + + -- Modex of receiver. + local receiver=call.modexreceiver or "" + + -- Init subtitle. + local subtitle=string.format("%s: %s", sender, call.subtitle) + if receiver and receiver~="" then + subtitle=string.format("%s: %s, %s", sender, receiver, call.subtitle) + end + + -- Last character of the string. + local lastchar=string.sub(subtitle, -1) + + -- Append ! or . + if loud then + if lastchar=="." or lastchar=="!" then + subtitle=string.sub(subtitle, 1,-1) + end + subtitle=subtitle.."!" + else + if lastchar=="!" then + -- This also okay. + elseif lastchar=="." then + -- Nothing to do. + else + subtitle=subtitle.."." + end + end + + return subtitle +end + +--- Get full file name for radio call. +-- @param #AIRBOSS self +-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. +-- @param #boolean loud Use loud version of file if available. +-- @return #string The file name of the radio sound. +function AIRBOSS:_RadioFilename(call, loud) + + -- Construct file name and subtitle. + local prefix=call.file or "" + local suffix=call.suffix or "ogg" + + -- Path to sound files. Default is in the ME + local path=self.soundfolder or "l10n/DEFAULT/" + + -- Loud version. + if loud then + prefix=prefix.."_Loud" + end + + -- File name inclusing path in miz file. + local filename=string.format("%s%s.%s", path, prefix, suffix) + + return filename +end + +--- Send text message to player client. +-- Message format will be "SENDER: RECCEIVER, MESSAGE". +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string message The message to send. +-- @param #string sender The person who sends the message or nil. +-- @param #string receiver The person who receives the message. Default player's onboard number. Set to "" for no receiver. +-- @param #number duration Display message duration. Default 10 seconds. +-- @param #boolean clear If true, clear screen from previous messages. +-- @param #number delay Delay in seconds, before the message is displayed. +-- @param #boolean soundoff If true, do not play boad number message. +function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration, clear, delay, soundoff) + + if playerData and message and message~="" then + + -- Default duration. + duration=duration or self.Tmessage + + -- Format message. + local text + if receiver and receiver=="" then + -- No (blank) receiver. + text=string.format("%s", message) + else + -- Default "receiver" is onboard number of player. + receiver=receiver or playerData.onboard + text=string.format("%s, %s", receiver, message) + end + self:T(self.lid..text) + + if delay and delay>0 then + -- Delayed call. + SCHEDULER:New(nil, self.MessageToPlayer, {self, playerData, message, sender, receiver, duration, clear, 0, soundoff}, delay) + else + + if receiver==playerData.onboard and not soundoff then + + -- Sound only to player group. + if sender and (sender=="LSO" or sender=="MARSHAL" or sender=="AIRBOSS") then + + -- User sound of board number. + local wait=self:_Number2Sound(playerData, sender, receiver) + + -- Play click sound to end message. + local filename=self:_RadioFilename(AIRBOSS.MarshalCall.CLICK) + USERSOUND:New(filename):ToGroup(playerData.group, wait) + + end + end + + -- Text message to player client. + if playerData.client then + MESSAGE:New(text, duration, sender, clear):ToClient(playerData.client) + end + + end + + end +end + + +--- Send text message to all players in the pattern queue. +-- Message format will be "SENDER: RECCEIVER, MESSAGE". +-- @param #AIRBOSS self +-- @param #string message The message to send. +-- @param #string sender The person who sends the message or nil. +-- @param #string receiver The person who receives the message. Default player's onboard number. Set to "" for no receiver. +-- @param #number duration Display message duration. Default 10 seconds. +-- @param #boolean clear If true, clear screen from previous messages. +-- @param #number delay Delay in seconds, before the message is displayed. +function AIRBOSS:MessageToPattern(message, sender, receiver, duration, clear, delay) + + -- Create new (fake) radio call to show the subtitile. + local call=self:_NewRadioCall(AIRBOSS.LSOCall.NOISE, sender or "LSO", message, duration, receiver, sender) + + -- Dummy radio transmission to display subtitle only to those who tuned in. + self:RadioTransmission(self.LSORadio, call, false, delay) + +end + +--- Send text message to all players in the marshal queue. +-- Message format will be "SENDER: RECCEIVER, MESSAGE". +-- @param #AIRBOSS self +-- @param #string message The message to send. +-- @param #string sender The person who sends the message or nil. +-- @param #string receiver The person who receives the message. Default player's onboard number. Set to "" for no receiver. +-- @param #number duration Display message duration. Default 10 seconds. +-- @param #boolean clear If true, clear screen from previous messages. +-- @param #number delay Delay in seconds, before the message is displayed. +function AIRBOSS:MessageToMarshal(message, sender, receiver, duration, clear, delay) + + -- Create new (fake) radio call to show the subtitile. + local call=self:_NewRadioCall(AIRBOSS.MarshalCall.NOISE, sender or "MARSHAL", message, duration, receiver, sender) + + -- Dummy radio transmission to display subtitle only to those who tuned in. + self:RadioTransmission(self.MarshalRadio, call, false, delay) + +end + +--- Generate a new radio call (deepcopy) from an existing default call. P +-- @param #AIRBOSS self +-- @param #AIRBOSS.RadioCall call Radio call to be enhanced. +-- @param #string sender Sender of the message. Default is the radio alias. +-- @param #string subtitle Subtitle of the message. Default from original radio call. Use "" for no subtitle. +-- @param #number subduration Time in seconds the subtitle is displayed. Default 10 seconds. +-- @param #string modexreceiver Onboard number of the receiver or nil. +-- @param #string modexsender Onboard number of the sender or nil. +function AIRBOSS:_NewRadioCall(call, sender, subtitle, subduration, modexreceiver, modexsender) + + -- Create a new call + local newcall=UTILS.DeepCopy(call) --#AIRBOSS.RadioCall + + -- Sender for displaying the subtitle. + newcall.sender=sender + + -- Subtitle of the message. + newcall.subtitle=subtitle or call.subtitle + + -- Duration of subtitle display. + newcall.subduration=subduration or self.Tmessage + + -- Tail number of the receiver. + if self:_IsOnboard(modexreceiver) then + newcall.modexreceiver=modexreceiver + end + + -- Tail number of the sender. + if self:_IsOnboard(modexsender) then + newcall.modexsender=modexsender + end + + return newcall +end + +--- Get unit from which we want to transmit a radio message. This has to be an aircraft for subtitles to work. +-- @param #AIRBOSS self +-- @return Wrapper.Unit#UNIT Sending aircraft unit or nil if was not setup, is not an aircraft or is not alive. +function AIRBOSS:_GetRadioSender() + + -- Check if we have a sending aircraft. + local sender=nil --Wrapper.Unit#UNIT + if self.senderac then + sender=UNIT:FindByName(self.senderac) + end + + -- Check that sender is alive and an aircraft. + if sender and sender:IsAlive() and sender:IsAir() then + return sender + end + + return nil +end + +--- Check if text is an onboard number of a flight. +-- @param #AIRBOSS self +-- @param #string text Text to check. +-- @return #boolean If true, text is an onboard number of a flight. +function AIRBOSS:_IsOnboard(text) + + -- Nil check. + if text==nil then + return false + end + + -- Message to all. + if text=="99" then + return true + end + + -- Loop over all flights. + for _,_flight in pairs(self.flights) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Loop over all onboard number of that flight. + for _,onboard in pairs(flight.onboardnumbers) do + if text==onboard then + return true + end + end + + end + + return false +end + +--- Convert a number (as string) into an outsound and play it to a player group. E.g. for board number or headings. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string sender Who is sending the call, either "LSO" or "MARSHAL". +-- @param #string number Number string, e.g. "032" or "183". +-- @param #number delay Delay before transmission in seconds. +-- @return #number Duration of the call in seconds. +function AIRBOSS:_Number2Sound(playerData, sender, number, delay) + + -- Default. + delay=delay or 0 + + --- Split string into characters. + local function _split(str) + local chars={} + for i=1,#str do + local c=str:sub(i,i) + table.insert(chars, c) + end + return chars + end + + -- Sender + local Sender + if sender=="LSO" then + Sender="LSOCall" + elseif sender=="MARSHAL" or sender=="AIRBOSS" then + Sender="MarshalCall" + else + self:E(self.lid..string.format("ERROR: Unknown radio sender %s!", tostring(sender))) + return + end + + -- Split string into characters. + local numbers=_split(number) + + local wait=0 + for i=1,#numbers do + + -- Current number + local n=numbers[i] + + -- Convert to N0, N1, ... + local N=string.format("N%s", n) + + -- Radio call. + local call=AIRBOSS[Sender][N] --#AIRBOSS.RadioCall + + -- Create file name. + --local filename=string.format("%s.%s", call.file, call.suffix) + local filename=self:_RadioFilename(call, false) + + -- Play sound. + USERSOUND:New(filename):ToGroup(playerData.group, delay+wait) + + -- Wait until this call is over before playing the next. + wait=wait+call.duration + end + + return wait +end + +--- Convert a number (as string) into a radio message. +-- E.g. for board number or headings. +-- @param #AIRBOSS self +-- @param #AIRBOSS.Radio radio Radio used for transmission. +-- @param #string number Number string, e.g. "032" or "183". +-- @param #number delay Delay before transmission in seconds. +-- @return #number Duration of the call in seconds. +function AIRBOSS:_Number2Radio(radio, number, delay) + + --- Split string into characters. + local function _split(str) + local chars={} + for i=1,#str do + local c=str:sub(i,i) + table.insert(chars, c) + end + return chars + end + + -- Sender. + local Sender="" + if radio.alias=="LSO" then + Sender="LSOCall" + elseif radio.alias=="MARSHAL" then + Sender="MarshalCall" + else + self:E(self.lid..string.format("ERROR: Unknown radio alias %s!", tostring(radio.alias))) + end + + -- Split string into characters. + local numbers=_split(number) + + local wait=0 + for i=1,#numbers do + + -- Current number + local n=numbers[i] + + -- Convert to N0, N1, ... + local N=string.format("N%s", n) + + -- Radio call. + local call=AIRBOSS[Sender][N] --#AIRBOSS.RadioCall + + -- Transmit. + self:RadioTransmission(radio, call, false, delay) + + -- Add up duration of the number. + wait=wait+call.duration + end + + -- Return the total duration of the call. + return wait +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- RADIO MENU Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add menu commands for player. +-- @param #AIRBOSS self +-- @param #string _unitName Name of player unit. +function AIRBOSS:_AddF10Commands(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check for player unit. + if _unit and playername then + + -- Get group and ID. + local group=_unit:GetGroup() + local gid=group:GetID() + + if group and gid then + + if not self.menuadded[gid] then + + -- Enable switch so we don't do this twice. + self.menuadded[gid]=true + + -- Set menu root path. + local _rootPath=nil + if AIRBOSS.MenuF10Root then + ------------------------ + -- MISSON LEVEL MENUE -- + ------------------------ + + if self.menusingle then + -- F10/Airboss/... + _rootPath=AIRBOSS.MenuF10Root + else + -- F10/Airboss//... + _rootPath=missionCommands.addSubMenuForGroup(gid, self.alias, AIRBOSS.MenuF10Root) + end + + else + ------------------------ + -- GROUP LEVEL MENUES -- + ------------------------ + + -- Main F10 menu: F10/Airboss/ + if AIRBOSS.MenuF10[gid]==nil then + AIRBOSS.MenuF10[gid]=missionCommands.addSubMenuForGroup(gid, "Airboss") + end + + + if self.menusingle then + -- F10/Airboss/... + _rootPath=AIRBOSS.MenuF10[gid] + else + -- F10/Airboss//... + _rootPath=missionCommands.addSubMenuForGroup(gid, self.alias, AIRBOSS.MenuF10[gid]) + end + + end + + + -------------------------------- + -- F10/Airboss//F1 Help + -------------------------------- + local _helpPath=missionCommands.addSubMenuForGroup(gid, "Help", _rootPath) + -- F10/Airboss//F1 Help/F1 Mark Zones + if self.menumarkzones then + local _markPath=missionCommands.addSubMenuForGroup(gid, "Mark Zones", _helpPath) + -- F10/Airboss//F1 Help/F1 Mark Zones/ + if self.menusmokezones then + missionCommands.addCommandForGroup(gid, "Smoke Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, false) -- F1 + end + missionCommands.addCommandForGroup(gid, "Flare Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, true) -- F2 + if self.menusmokezones then + missionCommands.addCommandForGroup(gid, "Smoke Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, false) -- F3 + end + missionCommands.addCommandForGroup(gid, "Flare Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, true) -- F4 + end + -- F10/Airboss//F1 Help/F2 Skill Level + local _skillPath=missionCommands.addSubMenuForGroup(gid, "Skill Level", _helpPath) + -- F10/Airboss//F1 Help/F2 Skill Level/ + missionCommands.addCommandForGroup(gid, "Flight Student", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.EASY) -- F1 + missionCommands.addCommandForGroup(gid, "Naval Aviator", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.NORMAL) -- F2 + missionCommands.addCommandForGroup(gid, "TOPGUN Graduate", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.HARD) -- F3 + -- F10/Airboss//F1 Help/ + missionCommands.addCommandForGroup(gid, "My Status", _helpPath, self._DisplayPlayerStatus, self, _unitName) -- F3 + missionCommands.addCommandForGroup(gid, "Attitude Monitor", _helpPath, self._DisplayAttitude, self, _unitName) -- F4 + missionCommands.addCommandForGroup(gid, "Radio Check LSO", _helpPath, self._LSORadioCheck, self, _unitName) -- F5 + missionCommands.addCommandForGroup(gid, "Radio Check Marshal", _helpPath, self._MarshalRadioCheck, self, _unitName) -- F6 + missionCommands.addCommandForGroup(gid, "Subtitles On/Off", _helpPath, self._SubtitlesOnOff, self, _unitName) -- F7 + + ------------------------------------- + -- F10/Airboss//F2 Kneeboard + ------------------------------------- + local _kneeboardPath=missionCommands.addSubMenuForGroup(gid, "Kneeboard", _rootPath) + -- F10/Airboss//F2 Kneeboard/F1 Results + local _resultsPath=missionCommands.addSubMenuForGroup(gid, "Results", _kneeboardPath) + -- F10/Airboss//F2 Kneeboard/F1 Results/ + missionCommands.addCommandForGroup(gid, "Greenie Board", _resultsPath, self._DisplayScoreBoard, self, _unitName) -- F1 + missionCommands.addCommandForGroup(gid, "My LSO Grades", _resultsPath, self._DisplayPlayerGrades, self, _unitName) -- F2 + missionCommands.addCommandForGroup(gid, "Last Debrief", _resultsPath, self._DisplayDebriefing, self, _unitName) -- F3 + -- F10/Airboss// + ------------------------- + missionCommands.addCommandForGroup(gid, "Request Marshal", _rootPath, self._RequestMarshal, self, _unitName) -- F3 + missionCommands.addCommandForGroup(gid, "Request Commence", _rootPath, self._RequestCommence, self, _unitName) -- F4 + missionCommands.addCommandForGroup(gid, "Request Refueling", _rootPath, self._RequestRefueling, self, _unitName) -- F5 + missionCommands.addCommandForGroup(gid, "[Reset My Status]", _rootPath, self._ResetPlayerStatus, self, _unitName) -- F6 + end + else + self:E(self.lid..string.format("ERROR: Could not find group or group ID in AddF10Menu() function. Unit name: %s.", _unitName)) + end + else + self:E(self.lid..string.format("ERROR: Player unit does not exist in AddF10Menu() function. Unit name: %s.", _unitName)) + end + +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ROOT MENU +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Reset player status. Player is removed from all queues and its status is set to undefined. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_ResetPlayerStatus(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Inform player. + local text="Status reset executed! You have been removed from all queues." + self:MessageToPlayer(playerData, text, "AIRBOSS") + + -- Remove flight from queues. Collapse marshal stack if necessary. + self:_RemoveFlight(playerData) + + -- Initialize player data. + self:_InitPlayer(playerData) + + end + end +end + +--- Request marshal. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_RequestMarshal(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Check if player is in CCA + local inCCA=playerData.unit:IsInZone(self.zoneCCA) + + if inCCA then + + if self:_InQueue(self.Qmarshal, playerData.group) then + + -- Flight group is already in marhal queue. + local text=string.format("you are already in the Marshal queue. New marshal request denied!") + self:MessageToPlayer(playerData, text, "MARSHAL") + + elseif self:_InQueue(self.Qpattern, playerData.group) then + + -- Flight group is already in pattern queue. + local text=string.format("you are already in the Pattern queue. Marshal request denied!") + self:MessageToPlayer(playerData, text, "MARSHAL") + + elseif self:_InQueue(self.Qwaiting, playerData.group) then + + -- Flight group is already in pattern queue. + local text=string.format("you are in the Waiting queue with %d flights ahead of you. Marshal request denied!", #self.Qwaiting) + self:MessageToPlayer(playerData, text, "MARSHAL") + + elseif not _unit:InAir() then + + -- Flight group is already in pattern queue. + local text=string.format("you are not airborne. Marshal request denied!") + self:MessageToPlayer(playerData, text, "MARSHAL") + + elseif playerData.name~=playerData.seclead then + + -- Flight group is already in pattern queue. + local text=string.format("negative, your section lead %s needs to request Marshal.", playerData.seclead) + self:MessageToPlayer(playerData, text, "MARSHAL") + + else + + -- Get next free Marshal stack. + local freestack=self:_GetFreeStack(playerData.ai) + + -- Check if stack is available. For Case I the number is limited. + if freestack then + + -- Add flight to marshal stack. + self:_MarshalPlayer(playerData, freestack) + + else + + -- Add flight to waiting queue. + self:_WaitPlayer(playerData) + + end + + end + + else + + -- Flight group is not in CCA yet. + local text=string.format("you are not inside CCA. Marshal request denied!") + self:MessageToPlayer(playerData, text, "MARSHAL") + + end + end + end +end + +--- Request to commence landing approach. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_RequestCommence(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Check if unit is in CCA. + local text="" + local cleared=false + if _unit:IsInZone(self.zoneCCA) then + + -- Get stack value. + local stack=playerData.flag:Get() + + -- Number of airborne aircraft currently in pattern. + local _,npattern=self:_GetQueueInfo(self.Qpattern) + + -- TODO: Check distance to initial or platform. Only allow commence if < max distance. Otherwise say bearing. + + if self:_InQueue(self.Qpattern, playerData.group) then + + -- Flight group is already in pattern queue. + text=string.format("negative, %s, you are already in the Pattern queue.", playerData.name) + + elseif not _unit:InAir() then + + -- Flight group is already in pattern queue. + text=string.format("negative, %s, you are not airborne.", playerData.name) + + elseif playerData.seclead~=playerData.name then + + -- Flight group is already in pattern queue. + text=string.format("negative, %s, your section leader %s has to request commence!", playerData.name, playerData.seclead) + + elseif stack>1 then + + -- We are in a higher stack. + text=string.format("negative, %s, it's not your turn yet! You are in stack no. %s.", playerData.name, stack) + + elseif npattern>=self.Nmaxpattern then + + -- Patern is full! + text=string.format("negative ghostrider, pattern is full!\nThere are %d aircraft currently in the pattern.", npattern) + + elseif self:IsRecovering()==false and not self.airbossnice then + + -- Carrier is not recovering right now. + if self.recoverywindow then + local clock=UTILS.SecondsToClock(self.recoverywindow.START) + text=string.format("negative, carrier is currently not recovery. Next window will open at %s.", clock) + else + text=string.format("negative, carrier is not recovering. No future windows planned.") + end + + elseif not self:_InQueue(self.Qmarshal, playerData.group) and not self.airbossnice then + + text="negatitive, you have to request Marshal before you can commence." + + else + + ----------------------- + -- Positive Response -- + ----------------------- + + -- Carrier is not recovering but Airboss has a good day. + if not self:IsRecovering() then + text="Carrier is not recovering currently! However, you are cleared anyway as I have a nice day.\n" + end + + -- If player is not in the Marshal queue set player case to current case. + if not self:_InQueue(self.Qmarshal, playerData.group) then + + -- Set current case. + playerData.case=self.case + + -- Hint about TACAN bearing. + if self.TACANon and playerData.difficulty~=AIRBOSS.Difficulty.HARD then + -- Get inverse magnetic radial potential offset. + local radial=self:GetRadial(playerData.case, true, true, true) + if playerData.case==1 then + -- For case 1 we want the BRC but above routine return FB. + radial=self:GetBRC() + end + text=text..string.format("Select TACAN %03d°, channel %d%s (%s)\n", radial, self.TACANchannel,self.TACANmode, self.TACANmorse) + end + + -- TODO: Inform section members. + + -- Set case of section members as well. Not sure if necessary any more since it is set as soon as the recovery case is changed. + for _,flight in pairs(playerData.section) do + flight.case=playerData.case + end + + -- Add player to pattern queue. Usually this is done when the stack is collapsed but this player is not in the Marshal queue. + table.insert(self.Qpattern, playerData) + end + + -- Clear player for commence. + cleared=true + end + + else + -- This flight is not yet registered! + text=string.format("negative, %s, you are not inside the CCA!", playerData.name) + end + + -- Debug + self:T(self.lid..text) + + -- Send message. + self:MessageToPlayer(playerData, text, "MARSHAL") + + -- Check if player was cleard. Need to do this after the message above is displayed. + if cleared then + -- Call commence routine. No zone check. + -- NOTE: Commencing will set step for all section members as well. + self:_Commencing(playerData, false) + end + end + end +end + +--- Player requests refueling. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +function AIRBOSS:_RequestRefueling(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Check if there is a recovery tanker defined. + local text + if self.tanker then + + -- Check if player is in CCA. + if _unit:IsInZone(self.zoneCCA) then + + -- Check if tanker is running or refueling or returning. + if self.tanker:IsRunning() or self.tanker:IsRefueling() then + + -- Get alt of tanker in angels. + --local angels=UTILS.Round(UTILS.MetersToFeet(self.tanker.altitude)/1000, 0) + local angels=self:_GetAngels(self.tanker.altitude) + + -- Tanker is up and running. + text=string.format("Proceed to tanker at angels %d.", angels) + + -- State TACAN channel of tanker if defined. + if self.tanker.TACANon then + text=text..string.format("\nTanker TACAN channel %d%s (%s).", self.tanker.TACANchannel, self.tanker.TACANmode, self.tanker.TACANmorse) + text=text..string.format("\nRadio frequency %.3f MHz AM.", self.tanker.RadioFreq) + end + + -- Tanker is currently refueling. Inform player. + if self.tanker:IsRefueling() then + text=text.."\nTanker is currently refueling. You might have to queue up." + end + + -- Collapse marshal stack if player is in queue. + self:_RemoveFlightFromMarshalQueue(playerData, true) + + -- Set step to refueling. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.REFUELING) + + -- Inform section and set step. + for _,sec in pairs(playerData.section) do + local sectext="Follow you section leader to the tanker." + self:MessageToPlayer(sec, sectext, "MARSHAL") + self:_SetPlayerStep(sec, AIRBOSS.PatternStep.REFUELING) + end + + elseif self.tanker:IsReturning() then + -- Tanker is RTB. + text="Tanker is RTB. Request denied!\nWait for the tanker to be back on station if you can." + end + + else + text="negative, you are not inside the CCA yet." + end + else + text="negative, no refueling tanker available." + end + + -- Send message. + self:MessageToPlayer(playerData, text, "MARSHAL") + end + end +end + + +--- Remove a member from the player's section. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player +-- @param #AIRBOSS.PlayerData sectionmember The section member to be removed. +-- @return #boolean If true, flight was a section member and could be removed. False otherwise. +function AIRBOSS:_RemoveSectionMember(playerData, sectionmember) + -- Loop over all flights in player's section + for i,_flight in pairs(playerData.section) do + local flight=_flight --#AIRBOSS.PlayerData + if flight.name==sectionmember.name then + table.remove(playerData.section, i) + return true + end + end + return false +end + +--- Set all flights within 100 meters to be part of my section. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +function AIRBOSS:_SetSection(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Coordinate of flight lead. + local mycoord=_unit:GetCoordinate() + + -- Max distance up to which section members are allowed. + local dmax=100 + + -- Check if player is in Marshal or pattern queue already. + local text + if self.NmaxSection==0 then + text=string.format("setting sections is disabled in this mission. You stay alone.") + elseif self:_InQueue(self.Qmarshal,playerData.group) then + text=string.format("you are already in the Marshal queue. Setting section not possible any more!") + elseif self:_InQueue(self.Qpattern, playerData.group) then + text=string.format("you are already in the Pattern queue. Setting section not possible any more!") + else + + -- Check if player is member of another section already. If so, remove him from his current section. + if playerData.seclead~=playerData.name then + local lead=self.players[playerData.seclead] --#AIRBOSS.PlayerData + if lead then + + -- Remove player from his old section lead. + local removed=self:_RemoveSectionMember(lead, playerData) + if removed then + self:MessageToPlayer(lead, string.format("Flight %s has been removed from your section.", playerData.name), "AIRBOSS", "", 5) + self:MessageToPlayer(playerData, string.format("You have been removed from %s's section.", lead.name), "AIRBOSS", "", 5) + end + + end + end + + -- Potential section members. + local section={} + + -- Loop over all registered flights. + for _,_flight in pairs(self.flights) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Only human flight groups excluding myself. Also only flights that dont have a section itself (would get messy) or are part of another section (no double membership). + if flight.ai==false and flight.groupname~=playerData.groupname and #flight.section==0 and flight.seclead==flight.name then + + -- Distance (3D) to other flight group. + local distance=flight.group:GetCoordinate():Get3DDistance(mycoord) + + -- Check distance. + if distance remove it. + if not gotit then + self:MessageToPlayer(flight, string.format("you were removed from %s's section and are on your own now.", playerData.name), "AIRBOSS", "", 5) + flight.seclead=flight.name + self:_RemoveSectionMember(playerData, flight) + end + end + + -- Remove all flights that are currently in the player's section already from scanned potential new section members. + for i,_new in pairs(section) do + local newflight=_new.flight --#AIRBOSS.PlayerData + for _,_flight in pairs(playerData.section) do + local currentflight=_flight --#AIRBOSS.PlayerData + if newflight.name==currentflight.name then + table.remove(section, i) + end + end + end + + -- Init section table. Should not be necessary as all members are removed anyhow above. + --playerData.section={} + + -- Output text. + text=string.format("Registered flight section:") + text=text..string.format("\n- %s (lead)", playerData.seclead) + -- Old members that stay (if any). + for _,_flight in pairs(playerData.section) do + local flight=_flight --#AIRBOSS.PlayerData + text=text..string.format("\n- %s", flight.name) + end + -- New members (if any). + for i=1,math.min(self.NmaxSection-#playerData.section, #section) do + local flight=section[i].flight --#AIRBOSS.PlayerData + + -- New flight members. + text=text..string.format("\n- %s", flight.name) + + -- Set section lead of player flight. + flight.seclead=playerData.name + + -- Set case of f + flight.case=playerData.case + + -- Inform player that he is now part of a section. + self:MessageToPlayer(flight, string.format("your section lead is now %s.", playerData.name), "AIRBOSS") + + -- Add flight to section table. + table.insert(playerData.section, flight) + end + + -- Section is empty. + if #playerData.section==0 then + text=text..string.format("\n- No other human flights found within radius of %.1f meters!", dmax) + end + + end + + -- Message to section lead. + self:MessageToPlayer(playerData, text, "MARSHAL") + end + end +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- RESULTS MENU +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Display top 10 player scores. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_DisplayScoreBoard(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + + -- Results table. + local _playerResults={} + + -- Calculate average points for all players. + for playerName,playerGrades in pairs(self.playerscores) do + + if playerGrades then + + -- Loop over all grades + local Paverage=0 + local n=0 + for _,_grade in pairs(playerGrades) do + local grade=_grade --#AIRBOSS.LSOgrade + + -- Add up only final scores for the average. + if grade.finalscore then --grade.points>=0 then + Paverage=Paverage+grade.finalscore + n=n+1 + else + -- Case when the player just leaves after an unfinished pass, e.g bolter, without landing. + -- But this should now be solved by deleteing all unfinished results. + end + end + + -- We dont want to devide by zero. + if n>0 then + _playerResults[playerName]=Paverage/n + end + + end + end + + -- Message text. + local text = string.format("Greenie Board (top ten):") + local i=1 + for _playerName,_points in UTILS.spairs(_playerResults, function(t, a, b) return t[b] < t[a] end) do + + -- Text. + text=text..string.format("\n[%d] %s %.1f||", i,_playerName, _points) + + -- All player grades. + local playerGrades=self.playerscores[_playerName] + + -- Add grades of passes. We use the actual grade of each pass here and not the average after player has landed. + for _,_grade in pairs(playerGrades) do + local grade=_grade --#AIRBOSS.LSOgrade + if grade.finalscore then + text=text..string.format("%.1f|", grade.points) + elseif grade.points>=0 then -- Only points >=0 as foul deck gives -1. + text=text..string.format("(%.1f)", grade.points) + end + end + + -- Display only the top ten. + i=i+1 + if i>10 then + break + end + end + + -- If no results yet. + if i==1 then + text=text.."\nNo results yet." + end + + -- Send message. + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + if playerData.client then + MESSAGE:New(text, 30, nil, true):ToClient(playerData.client) + end + + end +end + +--- Display top 10 player scores. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_DisplayPlayerGrades(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Grades of player: + local text=string.format("Your last 10 grades, %s:", _playername) + + -- All player grades. + local playerGrades=self.playerscores[_playername] or {} + + local p=0 -- Average points. + local n=0 -- Number of final passes. + local m=0 -- Number of total passes. + --for i,_grade in pairs(playerGrades) do + for i=#playerGrades,1,-1 do + --local grade=_grade --#AIRBOSS.LSOgrade + local grade=playerGrades[i] --#AIRBOSS.LSOgrade + + -- Check if points >=0. For foul deck WO we give -1 and pass is not counted. + if grade.points>=0 then + + -- Show final points or points of pass. + local points=grade.finalscore or grade.points + + -- Display max 10 results. + if m<10 then + text=text..string.format("\n[%d] %s %.1f PT - %s", i, grade.grade, points, grade.details) + + -- Wire trapped if any. + if grade.wire and grade.wire<=4 then + text=text..string.format(" %d-wire", grade.wire) + end + + -- Time in the groove if any. + if grade.Tgroove and grade.Tgroove<=60 then + text=text..string.format(" Tgroove=%.1f s", grade.Tgroove) + end + end + + -- Add up final points. + if grade.finalscore then + p=p+grade.finalscore + n=n+1 + end + + -- Total passes + m=m+1 + end + end + + + if n>0 then + text=text..string.format("\nAverage points = %.1f", p/n) + else + text=text..string.format("\nNo data available.") + end + + -- Send message. + if playerData.client then + MESSAGE:New(text, 30, nil, true):ToClient(playerData.client) + end + end + end +end + +--- Display last debriefing. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_DisplayDebriefing(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Debriefing text. + local text=string.format("Debriefing:") + + -- Check if data is present. + if #playerData.lastdebrief>0 then + text=text..string.format("\n================================\n") + for _,_data in pairs(playerData.lastdebrief) do + local step=_data.step + local comment=_data.hint + text=text..string.format("* %s:\n",step) + text=text..string.format("%s\n", comment) + end + else + text=text.." Nothing to show yet." + end + + -- Send debrief message to player + self:MessageToPlayer(playerData, text, nil , "", 30, true) + + end + end +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- KNEEBOARD MENU +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Display marshal or pattern queue. +-- @param #AIRBOSS self +-- @param #string _unitname Name of the player unit. +-- @param #table queue The queue to display. +-- @param #string qname Name of the queue. +function AIRBOSS:_DisplayQueue(_unitname, queue, qname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Number of group and units in queue + local Nqueue,nqueue=self:_GetQueueInfo(queue, playerData.case) + + local text=string.format("%s Queue:", qname) + if #queue==0 then + text=text.." empty" + else + local N=0 + if qname=="Marshal" then + for i,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.FlightGroup + local charlie=self:_GetCharlieTime(flight) + local Charlie=UTILS.SecondsToClock(charlie) + local stack=flight.flag:Get() + local angels=self:_GetAngels(self:_GetMarshalAltitude(stack, flight.case)) + local _,nunit,nsec=self:_GetFlightUnits(flight, true) + local nick=self:_GetACNickname(flight.actype) + N=N+nunit + text=text..string.format("\n[Stack %d] %s (%s*%d+%d): Case %d, Angels %d, Charlie %s", stack, flight.onboard, nick, nunit, nsec, flight.case, angels, tostring(Charlie)) + end + elseif qname=="Pattern" or qname=="Waiting" then + for i,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.FlightGroup + local _,nunit,nsec=self:_GetFlightUnits(flight, true) + local nick=self:_GetACNickname(flight.actype) + local ptime=UTILS.SecondsToClock(timer.getAbsTime()-flight.time) + N=N+nunit + text=text..string.format("\n[%d] %s (%s*%d+%d): Case %d, T=%s", i, flight.onboard, nick, nunit, nsec, flight.case, ptime) + end + end + text=text..string.format("\nTotal AC: %d (airborne %d)", N, nqueue) + end + + -- Send message. + self:MessageToPlayer(playerData, text, nil, "", nil, true) + end + end +end + + +--- Report information about carrier. +-- @param #AIRBOSS self +-- @param #string _unitname Name of the player unit. +function AIRBOSS:_DisplayCarrierInfo(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Current coordinates. + local coord=self:GetCoordinate() + + -- Carrier speed and heading. + local carrierheading=self.carrier:GetHeading() + local carrierspeed=UTILS.MpsToKnots(self.carrier:GetVelocityMPS()) + + -- Tacan/ICLS. + local tacan="unknown" + local icls="unknown" + if self.TACANon and self.TACANchannel~=nil then + tacan=string.format("%d%s (%s)", self.TACANchannel, self.TACANmode, self.TACANmorse) + end + if self.ICLSon and self.ICLSchannel~=nil then + icls=string.format("%d (%s)", self.ICLSchannel, self.ICLSmorse) + end + + -- Get groups, units in queues. + local Nmarshal,nmarshal=self:_GetQueueInfo(self.Qmarshal, playerData.case) + local Npattern,npattern=self:_GetQueueInfo(self.Qpattern) + local Nwaiting,nwaiting=self:_GetQueueInfo(self.Qwaiting) + local Ntotal,ntotal=self:_GetQueueInfo(self.flights) + + -- Current abs time. + local Tabs=timer.getAbsTime() + + -- Get recovery times of carrier. + local recoverytext="Recovery time windows (max 5):" + if #self.recoverytimes==0 then + recoverytext=recoverytext.." none." + else + -- Loop over recovery windows. + local rw=0 + for _,_recovery in pairs(self.recoverytimes) do + local recovery=_recovery --#AIRBOSS.Recovery + -- Only include current and future recovery windows. + if Tabs=5 then + -- Break the loop after 5 recovery times. + break + end + end + end + end + + -- Recovery tanker TACAN text. + local tankertext=nil + if self.tanker then + tankertext=string.format("Recovery tanker frequency %.3f MHz\n", self.tanker.RadioFreq) + if self.tanker.TACANon then + tankertext=tankertext..string.format("Recovery tanker TACAN %d%s (%s)",self.tanker.TACANchannel, self.tanker.TACANmode, self.tanker.TACANmorse) + else + tankertext=tankertext.."Recovery tanker TACAN n/a" + end + end + + -- Message text. + local text=string.format("%s info:\n", self.alias) + text=text..string.format("================================\n") + text=text..string.format("Carrier state %s\n", self:GetState()) + if self.case==1 then + text=text..string.format("Case %d recovery ops\n", self.case) + else + local radial=self:GetRadial(self.case, true, true, true) + text=text..string.format("Case %d recovery ops\nMarshal radial %03d°\n", self.case, radial) + end + text=text..string.format("BRC %03d°\n", self:GetBRC()) + text=text..string.format("FB %03d°\n", self:GetFinalBearing(true)) + text=text..string.format("Speed %d kts\n", carrierspeed) + text=text..string.format("Marshal radio %.3f MHz\n", self.MarshalFreq) + text=text..string.format("LSO radio %.3f MHz\n", self.LSOFreq) + text=text..string.format("TACAN Channel %s\n", tacan) + text=text..string.format("ICLS Channel %s\n", icls) + if tankertext then + text=text..tankertext.."\n" + end + --text=text..string.format("# A/C total %d\n", #self.flights) + text=text..string.format("# A/C total %d (%d)\n", Ntotal, ntotal) + text=text..string.format("# A/C marshal %d (%d)\n", Nmarshal, nmarshal) + text=text..string.format("# A/C pattern %d (%d)\n", Npattern, npattern) + text=text..string.format("# A/C waiting %d (%d)\n", Nwaiting, nwaiting) + text=text..string.format(recoverytext) + self:T2(self.lid..text) + + -- Send message. + self:MessageToPlayer(playerData, text, nil, "", 30, true) + + else + self:E(self.lid..string.format("ERROR: Could not get player data for player %s.", playername)) + end + end + +end + + +--- Report weather conditions at the carrier location. Temperature, QFE pressure and wind data. +-- @param #AIRBOSS self +-- @param #string _unitname Name of the player unit. +function AIRBOSS:_DisplayCarrierWeather(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Message text. + local text="" + + -- Current coordinates. + local coord=self:GetCoordinate() + + -- Get atmospheric data at carrier location. + local T=coord:GetTemperature() + local P=coord:GetPressure() + local Wd,Ws=coord:GetWind() + + -- Get Beaufort wind scale. + local Bn,Bd=UTILS.BeaufortScale(Ws) + + local WD=string.format('%03d°', Wd) + local Ts=string.format("%d°C",T) + + local tT=string.format("%d°C",T) + local tW=string.format("%.1f knots", UTILS.MpsToKnots(Ws)) + local tP=string.format("%.2f inHg", UTILS.hPa2inHg(P)) + + -- Report text. + text=text..string.format("Weather Report at Carrier %s:\n", self.alias) + text=text..string.format("================================\n") + text=text..string.format("Temperature %s\n", tT) + text=text..string.format("Wind from %s at %s (%s)\n", WD, tW, Bd) + text=text..string.format("QFE %.1f hPa = %s", P, tP) + + if self.staticweather then + local clouds, visibility, fog, dust=self:_GetStaticWeather() + text=text..string.format("\nVisibility %.1f NM", UTILS.MetersToNM(visibility)) + text=text..string.format("\nCloud base %d ft", UTILS.MetersToFeet(clouds.base)) + text=text..string.format("\nCloud thickness %d ft", UTILS.MetersToFeet(clouds.thickness)) + text=text..string.format("\nCloud density %d", clouds.density) + text=text..string.format("\nPrecipitation %d", clouds.iprecptns) + if fog then + text=text..string.format("\nFog thickness %d ft", UTILS.MetersToFeet(fog.thickness)) + text=text..string.format("\nFog visibility %d ft", UTILS.MetersToFeet(fog.visibility)) + else + text=text..string.format("\nNo fog") + end + if dust then + text=text..string.format("\nDust density %d", dust) + else + text=text..string.format("\nNo dust") + end + end + + -- Debug output. + self:T2(self.lid..text) + + -- Send message to player group. + self:MessageToPlayer(self.players[playername], text, nil, "", 30, true) + + else + self:E(self.lid..string.format("ERROR! Could not find player unit in CarrierWeather! Unit name = %s", _unitname)) + end +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- HELP MENU +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set difficulty level. +-- @param #AIRBOSS self +-- @param #string playername Player name. +-- @param #AIRBOSS.Difficulty difficulty Difficulty level. +function AIRBOSS:_SetDifficulty(playername, difficulty) + self:T2({difficulty=difficulty, playername=playername}) + + local playerData=self.players[playername] --#AIRBOSS.PlayerData + + if playerData then + playerData.difficulty=difficulty + local text=string.format("your skill level is now: %s.", difficulty) + self:MessageToPlayer(playerData, text, nil, playerData.name, 5) + else + self:E(self.lid..string.format("ERROR: Could not get player data for player %s.", playername)) + end +end + +--- Turn player's aircraft attitude display on or off. +-- @param #AIRBOSS self +-- @param #string _unitname Name of the player unit. +function AIRBOSS:_DisplayAttitude(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#AIRBOSS.PlayerData + + if playerData then + playerData.attitudemonitor=not playerData.attitudemonitor + end + end + +end + +--- Turn radio subtitles of player on or off +-- @param #AIRBOSS self +-- @param #string _unitname Name of the player unit. +function AIRBOSS:_SubtitlesOnOff(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#AIRBOSS.PlayerData + + if playerData then + playerData.subtitles=not playerData.subtitles + -- Inform player. + local text="" + if playerData.subtitles==true then + text=string.format("subtitiles are now ON.") + elseif playerData.subtitles==false then + text=string.format("subtitiles are now OFF.") + end + self:MessageToPlayer(playerData, text, nil, playerData.name, 5) + end + end + +end + + +--- Display player status. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +function AIRBOSS:_DisplayPlayerStatus(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Pattern step text. + local steptext=playerData.step + if playerData.step==AIRBOSS.PatternStep.HOLDING then + if playerData.holding==nil then + steptext="Transit to Marshal" + elseif playerData.holding==false then + steptext="Marshal (outside zone)" + elseif playerData.holding==true then + steptext="Marshal Stack Holding" + end + end + + -- Stack. + local stack=playerData.flag:Get() + + -- Stack text. + local stacktext=nil + if stack>0 then + local stackalt=self:_GetMarshalAltitude(stack) + local angels=self:_GetAngels(stackalt) + stacktext=string.format("Marshal Stack %d, Angels %d\n", stack, angels) + + + -- Hint about TACAN bearing. + if playerData.step==AIRBOSS.PatternStep.HOLDING and playerData.case>1 then + -- Get inverse magnetic radial potential offset. + local radial=self:GetRadial(playerData.case, true, true, true) + stacktext=stacktext..string.format("Select TACAN %03d°, %d DME\n", radial, angels+15) + end + end + + -- Fuel and fuel state. + local fuel=playerData.unit:GetFuel()*100 + local fuelstate=self:_GetFuelState(playerData.unit) + + -- Number of units in group. + local _,nunitsGround=self:_GetFlightUnits(playerData, true) + local _,nunitsAirborne=self:_GetFlightUnits(playerData, false) + + -- Player data. + local text=string.format("Status of player %s (%s)\n", playerData.name, playerData.callsign) + text=text..string.format("================================\n") + text=text..string.format("Step: %s\n", steptext) + if stacktext then + text=text..stacktext + end + text=text..string.format("Recovery Case: %d\n", playerData.case) + text=text..string.format("Skill Level: %s\n", playerData.difficulty) + text=text..string.format("Tail # %s (%s)\n", playerData.onboard, self:_GetACNickname(playerData.actype)) + text=text..string.format("Fuel State: %.1f lbs/1000 (%.1f %%)\n", fuelstate/1000, fuel) + text=text..string.format("# units: %d (%d airborne)\n", nunitsGround, nunitsAirborne) + text=text..string.format("Section Lead: %s (%d/%d)", tostring(playerData.seclead), #playerData.section+1, self.NmaxSection+1) + for _,_sec in pairs(playerData.section) do + local sec=_sec --#AIRBOSS.PlayerData + text=text..string.format("\n- %s", sec.name) + end + + if playerData.step==AIRBOSS.PatternStep.INITIAL then + + -- Create a point 3.0 NM astern for re-entry. + local zoneinitial=self:GetCoordinate():Translate(UTILS.NMToMeters(3.5), self:GetRadial(2, false, false, false)) + + -- Heading and distance to initial zone. + local flyhdg=playerData.unit:GetCoordinate():HeadingTo(zoneinitial) + local flydist=UTILS.MetersToNM(playerData.unit:GetCoordinate():Get2DDistance(zoneinitial)) + local brc=self:GetBRC() + + -- Help player to find its way to the initial zone. + text=text..string.format("\nTo Initial: Fly heading %03d° for %.1f NM and turn to BRC %03d°", flyhdg, flydist, brc) + + elseif playerData.step==AIRBOSS.PatternStep.PLATFORM then + + -- Coordinate of the platform zone. + local zoneplatform=self:_GetZonePlatform(playerData.case):GetCoordinate() + + -- Heading and distance to platform zone. + local flyhdg=playerData.unit:GetCoordinate():HeadingTo(zoneplatform) + local flydist=UTILS.MetersToNM(playerData.unit:GetCoordinate():Get2DDistance(zoneplatform)) + + -- Get heading. + local hdg=self:GetRadial(playerData.case, true, true, true) + + -- Help player to find its way to the initial zone. + text=text..string.format("\nTo Platform: Fly heading %03d° for %.1f NM and turn to %03d°", flyhdg, flydist, hdg) + + end + + -- Send message. + self:MessageToPlayer(playerData, text, nil, "", 30, true) + end + end + +end + +--- Mark current marshal zone of player by either smoke or flares. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +-- @param #boolean flare If true, flare the zone. If false, smoke the zone. +function AIRBOSS:_MarkMarshalZone(_unitName, flare) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Get player stack and recovery case. + local stack=playerData.flag:Get() + local case=playerData.case + + local text="" + if stack>0 then + + -- Get current holding zone. + local zoneHolding=self:_GetZoneHolding(case, stack) + + -- Get Case I commence zone at three position. + local zoneThree=self:_GetZoneCommence(case) + + -- Pattern alitude. + local patternalt=self:_GetMarshalAltitude(stack, case) + + -- Flare and smoke at the ground. + patternalt=5 + + if flare then + text=text.."Marking Marshal zone with WHITE flares." + zoneHolding:FlareZone(FLARECOLOR.White, 45, nil, patternalt) + + if playerData.case==1 then + text=text.."\nMarking Commence zone with RED flares." + zoneThree:FlareZone(FLARECOLOR.Red, 45, nil, patternalt) + end + else + text="Marking Marshal zone with WHITE smoke." + zoneHolding:SmokeZone(SMOKECOLOR.White, 45, patternalt) + + if playerData.case==1 then + text=text.."\nMarking Commence zone with RED smoke." + zoneThree:SmokeZone(SMOKECOLOR.Red, 45, patternalt) + end + end + + else + text="You are currently not in a marshal stack. No zone to mark!" + end + + -- Send message to player. + self:MessageToPlayer(playerData, text, "MARSHAL", "") + end + end + +end + + +--- Mark CASE I or II/II zones by either smoke or flares. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +-- @param #boolean flare If true, flare the zone. If false, smoke the zone. +function AIRBOSS:_MarkCaseZones(_unitName, flare) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Player's recovery case. + local case=playerData.case + + -- Initial + local text=string.format("Marking CASE %d zones\n", case) + + -- Flare or smoke? + if flare then + + ----------- + -- Flare -- + ----------- + + -- Case I/II: Initial + if case==1 or case==2 then + text=text.."* initial with GREEN flares\n" + self:_GetZoneInitial(case):FlareZone(FLARECOLOR.Green, 45) + end + + -- Case II/III: approach corridor + if case==2 or case==3 then + text=text.."* approach corridor with GREEN flares\n" + self:_GetZoneCorridor(case):FlareZone(FLARECOLOR.Green, 45) + end + + -- Case II/III: platform + if case==2 or case==3 then + text=text.."* platform with RED flares\n" + self:_GetZonePlatform(case):FlareZone(FLARECOLOR.Red, 45) + end + + -- Case III: dirty up + if case==3 then + text=text.."* dirty up with YELLOW flares\n" + self:_GetZoneDirtyUp(case):FlareZone(FLARECOLOR.Yellow, 45) + end + + -- Case II/III: arc in/out + if case==2 or case==3 then + if math.abs(self.holdingoffset)>0 then + self:_GetZoneArcIn(case):FlareZone(FLARECOLOR.White, 45) + text=text.."* arc turn in with WHITE flares\n" + self:_GetZoneArcOut(case):FlareZone(FLARECOLOR.White, 45) + text=text.."* arc trun out with WHITE flares\n" + end + end + + -- Case III: bullseye + if case==3 then + text=text.."* bullseye with GREEN flares\n" + self:_GetZoneBullseye(case):FlareZone(FLARECOLOR.Green, 45) + end + + else + + ----------- + -- Smoke -- + ----------- + + -- Case I/II: Initial + if case==1 or case==2 then + text=text.."* initial with GREEN smoke\n" + self:_GetZoneInitial(case):SmokeZone(SMOKECOLOR.Green, 45) + end + + -- Case II/III: Approach Corridor + if case==2 or case==3 then + text=text.."* approach corridor with GREEN smoke\n" + self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) + end + + -- Case II/III: platform + if case==2 or case==3 then + text=text.."* platform with RED smoke\n" + self:_GetZonePlatform(case):SmokeZone(SMOKECOLOR.Red, 45) + end + + -- Case II/III: arc in/out if offset>0. + if case==2 or case==3 then + if math.abs(self.holdingoffset)>0 then + self:_GetZoneArcIn(case):SmokeZone(SMOKECOLOR.Blue, 45) + text=text.."* arc turn in with BLUE smoke\n" + self:_GetZoneArcOut(case):SmokeZone(SMOKECOLOR.Blue, 45) + text=text.."* arc trun out with BLUE smoke\n" + end + end + + -- Case III: dirty up + if case==3 then + text=text.."* dirty up with ORANGE smoke\n" + self:_GetZoneDirtyUp(case):SmokeZone(SMOKECOLOR.Orange, 45) + end + + -- Case III: bullseye + if case==3 then + text=text.."* bullseye with GREEN smoke\n" + self:_GetZoneBullseye(case):SmokeZone(SMOKECOLOR.Green, 45) + end + + end + + -- Send message to player. + self:MessageToPlayer(playerData, text, "MARSHAL", "") + end + end + +end + +--- LSO radio check. Will broadcase LSO message at given LSO frequency. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_LSORadioCheck(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + if playerData then + -- Broadcase LSO radio check message on LSO radio. + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.RADIOCHECK) + end + end +end + +--- Marshal radio check. Will broadcase Marshal message at given Marshal frequency. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_MarshalRadioCheck(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + if playerData then + -- Broadcase Marshal radio check message on Marshal radio. + self:RadioTransmission(self.MarshalRadio, AIRBOSS.MarshalCall.RADIOCHECK) + end + end +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +-- Persistence Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + +--- On before "Save" event. Checks if io and lfs are available. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string path (Optional) Path where the file is saved. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized. +-- @param #string filename (Optional) File name for saving the player grades. Default is "AIRBOSS-_LSOgrades.csv". +function AIRBOSS:onbeforeSave(From, Event, To, path, filename) + + -- Check io module is available. + if not io then + self:E(self.lid.."ERROR: io not desanitized. Can't save player grades.") + return false + end + + -- Check default path. + if path==nil and not lfs then + self:E(self.lid.."WARNING: lfs not desanitized. Results will be saved in DCS installation root directory rather than your \"Saved Games\DCS\" folder.") + end + + return true +end + +--- On after "Save" event. Player data is saved to file. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string path Path where the file is saved. If nil, file is saved in the DCS root installtion directory or your "Saved Games" folder if lfs was desanitized. +-- @param #string filename (Optional) File name for saving the player grades. Default is "AIRBOSS-_LSOgrades.csv". +function AIRBOSS:onafterSave(From, Event, To, path, filename) + + --- Function that saves data to file + local function _savefile(filename, data) + local f = assert(io.open(filename, "wb")) + f:write(data) + f:close() + end + + -- Set path or default. + if lfs then + path=path or lfs.writedir() + end + + -- Set file name. + filename=filename or string.format("AIRBOSS-%s_LSOgrades.csv", self.alias) + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Info + local text=string.format("Saving player LSO grades to file %s", filename) + MESSAGE:New(text,30):ToAllIf(self.Debug) + self:I(self.lid..text) + + -- Header line + local scores="Name,Pass,Points Final,Points Pass,Grade,Details,Wire,Tgroove,Case\n" + + -- Loop over all players. + for playername,grades in pairs(self.playerscores) do + + -- Loop over player grades table. + for i,_grade in pairs(grades) do + local grade=_grade --#AIRBOSS.LSOgrade + + -- Check some stuff that could be nil. + local wire="n/a" + if grade.wire and grade.wire<=4 then + wire=tostring(grade.wire) + end + + local Tgroove="n/a" + if grade.Tgroove and grade.Tgroove<=60 and grade.case<3 then + Tgroove=tostring(UTILS.Round(grade.Tgroove, 1)) + end + + local finalscore="n/a" + if grade.finalscore then + finalscore=tostring(UTILS.Round(grade.finalscore, 1)) + end + + -- Compile grade line. + scores=scores..string.format("%s,%d,%s,%.1f,%s,%s,%s,%s,%d\n", playername, i, finalscore, grade.points, grade.grade, grade.details, wire, Tgroove, grade.case) + end + end + + -- Save file. + _savefile(filename, scores) +end + + +--- On before "Load" event. Checks if the file that the player grades from exists. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string path (Optional) Path where the file is loaded from. Default is the DCS installation root directory or your "Saved Games\\DCS" folder if lfs was desanizized. +-- @param #string filename (Optional) File name for saving the player grades. Default is "AIRBOSS-_LSOgrades.csv". +function AIRBOSS:onbeforeLoad(From, Event, To, path, filename) + + --- Function that check if a file exists. + local function _fileexists(name) + local f=io.open(name,"r") + if f~=nil then + io.close(f) + return true + else + return false + end + end + + -- Check io module is available. + if not io then + self:E(self.lid.."WARNING: io not desanitized. Can't load player grades.") + return false + end + + -- Check default path. + if path==nil and not lfs then + self:E(self.lid.."WARNING: lfs not desanitized. Results will be saved in DCS installation root directory rather than your \"Saved Games\DCS\" folder.") + end + + -- Set path or default. + if lfs then + path=path or lfs.writedir() + end + + -- Set file name. + filename=filename or string.format("AIRBOSS-%s_LSOgrades.csv", self.alias) + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Check if file exists. + local exists=_fileexists(filename) + + if exists then + return true + else + self:E(self.lid..string.format("WARNING: Player LSO grades file %s does not exist.", filename)) + return false + end + +end + + +--- On after "Load" event. Loads grades of all players from file. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string path Path where the file is loaded from. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if lfs was desanizied. +-- @param #string filename (Optional) File name for saving the player grades. Default is "AIRBOSS-_LSOgrades.csv". +function AIRBOSS:onafterLoad(From, Event, To, path, filename) + + --- Function that load data from a file. + local function _loadfile(filename) + local f=assert(io.open(filename, "rb")) + local data=f:read("*all") + f:close() + return data + end + + -- Set path or default. + if lfs then + path=path or lfs.writedir() + end + + -- Set file name. + filename=filename or string.format("AIRBOSS-%s_LSOgrades.csv", self.alias) + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Info message. + local text=string.format("Loading player LSO grades from file %s", filename) + MESSAGE:New(text,10):ToAllIf(self.Debug) + self:I(self.lid..text) + + -- Load asset data from file. + local data=_loadfile(filename) + + -- Split by line break. + local playergrades=UTILS.Split(data,"\n") + + -- Remove first header line. + table.remove(playergrades, 1) + + -- Init player scores table. + self.playerscores={} + + -- Loop over all lines. + for _,gradeline in pairs(playergrades) do + + -- Parameters are separated by commata. + local gradedata=UTILS.Split(gradeline, ",") + + -- Debug info. + self:T2(gradedata) + + -- Grade table + local grade={} --#AIRBOSS.LSOgrade + + -- Line format: playername, i, grade.finalscore, grade.points, grade.grade, grade.details, wire, Tgroove, case + local playername=gradedata[1] + if gradedata[3]~=nil and gradedata[3]~="n/a" then + grade.finalscore=tonumber(gradedata[3]) + end + grade.points=tonumber(gradedata[4]) + grade.grade=tostring(gradedata[5]) + grade.details=tostring(gradedata[6]) + if gradedata[7]~=nil and gradedata[7]~="n/a" then + grade.wire=tonumber(gradedata[7]) + end + if gradedata[8]~=nil and gradedata[8]~="n/a" then + grade.Tgroove=tonumber(gradedata[8]) + end + grade.case=tonumber(gradedata[9]) + + -- Init player table if necessary. + self.playerscores[playername]=self.playerscores[playername] or {} + + -- Add grade to table. + table.insert(self.playerscores[playername], grade) + + -- Debug info. + self:T2({playername, self.playerscores[playername]}) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua new file mode 100644 index 000000000..d0fa243f9 --- /dev/null +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -0,0 +1,1521 @@ +--- **Ops** - (R2.5) - Recovery tanker for carrier operations. +-- +-- Tanker aircraft flying a racetrack pattern overhead an aircraft carrier. +-- +-- **Main Features:** +-- +-- * Regular pattern update with respect to carrier position. +-- * No restrictions regarding carrier waypoints and heading. +-- * Automatic respawning when tanker runs out of fuel for 24/7 operations. +-- * Tanker can be spawned cold or hot on the carrier or at any other airbase or directly in air. +-- * Automatic AA TACAN beacon setting. +-- * Multiple tankers at the same carrier. +-- * Multiple carriers due to object oriented approach. +-- * Finite State Machine (FSM) implementation, which allows the mission designer to hook into certain events. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- ### Special thanks to **HighwaymanEd** for testing and suggesting improvements! +-- +-- @module Ops.RecoveryTanker +-- @image Ops_RecoveryTanker.png + +--- RECOVERYTANKER class. +-- @type RECOVERYTANKER +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. +-- @field #string lid Log debug id text. +-- @field Wrapper.Unit#UNIT carrier The carrier the tanker is attached to. +-- @field #string carriertype Carrier type. +-- @field #string tankergroupname Name of the late activated tanker template group. +-- @field Wrapper.Group#GROUP tanker Tanker group. +-- @field Wrapper.Airbase#AIRBASE airbase The home airbase object of the tanker. Normally the aircraft carrier. +-- @field Core.Radio#BEACON beacon Tanker TACAN beacon. +-- @field #number TACANchannel TACAN channel. Default 1. +-- @field #string TACANmode TACAN mode, i.e. "X" or "Y". Default "Y". Use only "Y" for AA TACAN stations! +-- @field #string TACANmorse TACAN morse code. Three letters identifying the TACAN station. Default "TKR". +-- @field #boolean TACANon If true, TACAN is automatically activated. If false, TACAN is disabled. +-- @field #number RadioFreq Radio frequency in MHz of the tanker. Default 251 MHz. +-- @field #string RadioModu Radio modulation "AM" or "FM". Default "AM". +-- @field #number speed Tanker speed when flying pattern. +-- @field #number altitude Tanker orbit pattern altitude. +-- @field #number distStern Race-track distance astern. distStern is <0. +-- @field #number distBow Race-track distance bow. distBow is >0. +-- @field #number Dupdate Pattern update when carrier changes its position by more than this distance (meters). +-- @field #number Hupdate Pattern update when carrier changes its heading by more than this number (degrees). +-- @field #number dTupdate Minimum time interval in seconds before the next pattern update can happen. +-- @field #number Tupdate Last time the pattern was updated. +-- @field #number takeoff Takeoff type (cold, hot, air). +-- @field #number lowfuel Low fuel threshold in percent. +-- @field #boolean respawn If true, tanker be respawned (default). If false, no respawning will happen. +-- @field #boolean respawninair If true, tanker will always be respawned in air. This has no impact on the initial spawn setting. +-- @field #boolean uncontrolledac If true, use and uncontrolled tanker group already present in the mission. +-- @field DCS#Vec3 orientation Orientation of the carrier. Used to monitor changes and update the pattern if heading changes significantly. +-- @field DCS#Vec3 orientlast Orientation of the carrier for checking if carrier is currently turning. +-- @field Core.Point#COORDINATE position Position of carrier. Used to monitor if carrier significantly changed its position and then update the tanker pattern. +-- @field #string alias Alias of the spawn group. +-- @field #number uid Unique ID of this tanker. +-- @field #boolean awacs If true, the groups gets the enroute task AWACS instead of tanker. +-- @field #number callsignname Number for the callsign name. +-- @field #number callsignnumber Number of the callsign name. +-- @field #string modex Tail number of the tanker. +-- @extends Core.Fsm#FSM + +--- Recovery Tanker. +-- +-- === +-- +-- ![Banner Image](..\Presentations\RECOVERYTANKER\RecoveryTanker_Main.png) +-- +-- # Recovery Tanker +-- +-- A recovery tanker acts as refueling unit flying overhead an aircraft carrier in order to supply incoming flights with gas if they go "*Bingo on the Ball*". +-- +-- # Simple Script +-- +-- In the mission editor you have to set up a carrier unit, which will act as "mother". In the following, this unit will be named **"USS Stennis"**. +-- +-- Secondly, you need to define a recovery tanker group in the mission editor and set it to **"LATE ACTIVATED"**. The name of the group we'll use is **"Texaco"**. +-- +-- The basic script is very simple and consists of only two lines: +-- +-- TexacoStennis=RECOVERYTANKER:New(UNIT:FindByName("USS Stennis"), "Texaco") +-- TexacoStennis:Start() +-- +-- The first line will create a new RECOVERYTANKER object and the second line starts the process. +-- +-- With this setup, the tanker will be spawned on the USS Stennis with running engines. After it takes off, it will fly a position ~10 NM astern of the boat and from there start its +-- pattern. This is a counter clockwise racetrack pattern at angels 6. +-- +-- A TACAN beacon will be automatically activated at channel 1Y with morse code "TKR". See below how to change this setting. +-- +-- Note that the Tanker entry in the F10 radio menu will appear once the tanker is on station and not before. If you spawn the tanker cold or hot on the carrier, this will take ~10 minutes. +-- +-- Also note, that currently the only carrier capable aircraft in DCS is the S-3B Viking (tanker version). If you want to use another refueling aircraft, you need to activate air spawn +-- or set a different land based airport of the map. This will be explained below. +-- +-- ![Banner Image](..\Presentations\RECOVERYTANKER\RecoveryTanker_Pattern.jpg) +-- +-- The "downwind" leg of the pattern is normally used for refueling. +-- +-- Once the tanker runs out of fuel itself, it will return to the carrier, respawn with full fuel and take up its pattern again. +-- +-- # Options and Fine Tuning +-- +-- Several parameters can be customized by the mission designer via user API functions. +-- +-- ## Takeoff Type +-- +-- By default, the tanker is spawned with running engines on the carrier. The mission designer has set option to set the take off type via the @{#RECOVERYTANKER.SetTakeoff} function. +-- Or via shortcuts +-- +-- * @{#RECOVERYTANKER.SetTakeoffHot}(): Will set the takeoff to hot, which is also the default. +-- * @{#RECOVERYTANKER.SetTakeoffCold}(): Will set the takeoff type to cold, i.e. with engines off. +-- * @{#RECOVERYTANKER.SetTakeoffAir}(): Will set the takeoff type to air, i.e. the tanker will be spawned in air ~10 NM astern the carrier. +-- +-- For example, +-- TexacoStennis=RECOVERYTANKER:New(UNIT:FindByName("USS Stennis"), "Texaco") +-- TexacoStennis:SetTakeoffAir() +-- TexacoStennis:Start() +-- will spawn the tanker several nautical miles astern the carrier. From there it will start its pattern. +-- +-- Spawning in air is not as realistic but can be useful do avoid DCS bugs and shortcomings like aircraft crashing into each other on the flight deck. +-- +-- **Note** that when spawning in air is set, the tanker will also not return to the boat, once it is out of fuel. Instead it will be respawned directly in air. +-- +-- If only the first spawning should happen on the carrier, one use the @{#RECOVERYTANKER.SetRespawnInAir}() function to command that all subsequent spawning +-- will happen in air. +-- +-- If the tanker should not be respawned at all, one can set @{#RECOVERYTANKER.SetRespawnOff}(). +-- +-- ## Pattern Parameters +-- +-- The racetrack pattern parameters can be fine tuned via the following functions: +-- +-- * @{#RECOVERYTANKER.SetAltitude}(*altitude*), where *altitude* is the pattern altitude in feet. Default 6000 ft. +-- * @{#RECOVERYTANKER.SetSpeed}(*speed*), where *speed* is the pattern speed in knots. Default is 274 knots TAS which results in ~250 KIAS. +-- * @{#RECOVERYTANKER.SetRacetrackDistances}(*distbow*, *diststern*), where *distbow* and *diststern* are the distances ahead and astern the boat (default 10 and 4 NM), respectively. +-- In principle, these number should be more like 8 and 6 NM but since the carrier is moving, we give translate the pattern points a bit forward. +-- +-- ## Home Base +-- +-- The home base is the airbase where the tanker is spawned (if not in air) and where it will go once it is running out of fuel. The default home base is the carrier itself. +-- The home base can be changed via the @{#RECOVERYTANKER.SetHomeBase}(*airbase*) function, where *airbase* can be a MOOSE @{Wrapper.Airbase#AIRBASE} object or simply the +-- name of the airbase passed as string. +-- +-- Note that only the S3B Viking is a refueling aircraft that is carrier capable. You can use other tanker aircraft types, e.g. the KC-130, but in this case you must either +-- set an airport of the map as home base or activate spawning in air via @{#RECOVERYTANKER.SetTakeoffAir}. +-- +-- ## TACAN +-- +-- A TACAN beacon for the tanker can be activated via scripting, i.e. no need to do this within the mission editor. +-- +-- The beacon is create with the @{#RECOVERYTANKER.SetTACAN}(*channel*, *morse*) function, where *channel* is the TACAN channel (a number), +-- and *morse* a three letter string that is send as morse code to identify the tanker: +-- +-- TexacoStennis:SetTACAN(10, "TKR") +-- +-- will activate a TACAN beacon 10Y with more code "TKR". +-- +-- If you do not set a TACAN beacon explicitly, it is automatically create on channel 1Y and morse code "TKR". +-- The mode is *always* "Y" for AA TACAN stations since mode "X" does not work! +-- +-- In order to completely disable the TACAN beacon, you can use the @{#RECOVERYTANKER.SetTACANoff}() function in your script. +-- +-- ## Radio +-- +-- The radio frequency on optionally modulation can be set via the @{#RECOVERYTANKER.SetRadio}(*frequency*, *modulation*) function. The first parameter denotes the radio frequency the tanker uses in MHz. +-- The second parameter is *optional* and sets the modulation to either AM (default) or FM. +-- +-- For example, +-- +-- TexacoStennis:SetRadio(260) +-- +-- will set the frequency of the tanker to 260 MHz AM. +-- +-- **Note** that if this is not set, the tanker frequency will be automatically set to **251 MHz AM**. +-- +-- ## Pattern Update +-- +-- The pattern of the tanker is updated if at least one of the two following conditions apply: +-- +-- * The aircraft carrier changes its position by more than 5 NM (see @{#RECOVERYTANKER.SetPatternUpdateDistance}) and/or +-- * The aircraft carrier changes its heading by more than 5 degrees (see @{#RECOVERYTANKER.SetPatternUpdateHeading}) +-- +-- **Note** that updating the pattern often leads to a more or less small disruption of the perfect racetrack pattern of the tanker. This is because a new waypoint and new racetrack points +-- need to be set as DCS task. This is the reason why the pattern is not constantly updated but rather when the position or heading of the carrier changes significantly. +-- +-- The maximum update frequency is set to 10 minutes. You can adjust this by @{#RECOVERYTANKER.SetPatternUpdateInterval}. +-- Also the pattern will not be updated whilst the carrier is turning or the tanker is currently refueling another unit. +-- +-- ## Callsign +-- +-- The callsign of the tanker can be set via the @{#RECOVERYTANKER.SetCallsign}(*callsignname*, *callsignnumber*) function. Both parameters are *numbers*. +-- The first parameter *callsignname* defines the name (1=Texaco, 2=Arco, 3=Shell). The second (optional) parameter specifies the first number and has to be between 1-9. +-- Also see [DCS_enum_callsigns](https://wiki.hoggitworld.com/view/DCS_enum_callsigns) and [DCS_command_setCallsign](https://wiki.hoggitworld.com/view/DCS_command_setCallsign). +-- +-- TexacoStennis:SetCAllsign(CALLSIGN.Tanker.Arco) +-- +-- For convenience, MOOSE has a CALLSIGN enumerator introduced. +-- +-- ## AWACS +-- +-- You can use the class also to have an AWACS orbiting overhead the carrier. This requires to add the @{#RECOVERYTANKER.SetAWACS}() function to the script, which sets the enroute tasks AWACS +-- as soon as the aircraft enters its pattern. +-- +-- A simple script could look like this: +-- +-- -- E-2D at USS Stennis spawning in air. +-- local awacsStennis=RECOVERYTANKER:New("USS Stennis", "E2D Group") +-- +-- -- Custom settings: +-- awacsStennis:SetAWACS() +-- awacsStennis:SetCallsign(CALLSIGN.AWACS.Wizard, 1) +-- awacsStennis:SetTakeoffAir() +-- awacsStennis:SetAltitude(20000) +-- awacsStennis:SetRadio(262) +-- awacsStennis:SetTACAN(2, "WIZ") +-- +-- -- Start AWACS. +-- awacsStennis:Start() +-- +-- # Finite State Machine +-- +-- The implementation uses a Finite State Machine (FSM). This allows the mission designer to hook in to certain events. +-- +-- * @{#RECOVERYTANKER.Start}: This event starts the FMS process and initialized parameters and spawns the tanker. DCS event handling is started. +-- * @{#RECOVERYTANKER.Status}: This event is called in regular intervals (~60 seconds) and checks the status of the tanker and carrier. It triggers other events if necessary. +-- * @{#RECOVERYTANKER.PatternUpdate}: This event commands the tanker to update its pattern +-- * @{#RECOVERYTANKER.RTB}: This events sends the tanker to its home base (usually the carrier). This is called once the tanker runs low on gas. +-- * @{#RECOVERYTANKER.RefuelStart}: This event is called when a tanker starts to refuel another unit. +-- * @{#RECOVERYTANKER.RefuelStop}: This event is called when a tanker stopped to refuel another unit. +-- * @{#RECOVERYTANKER.Run}: This event is called when the tanker resumes normal operations, e.g. after refueling stopped or tanker finished refueling. +-- * @{#RECOVERYTANKER.Stop}: This event stops the FSM by unhandling DCS events. +-- +-- The mission designer can capture these events by RECOVERYTANKER.OnAfter*Eventname* functions, e.g. @{#RECOVERYTANKER.OnAfterPatternUpdate}. +-- +-- # Debugging +-- +-- In case you have problems, it is always a good idea to have a look at your DCS log file. You find it in your "Saved Games" folder, so for example in +-- C:\Users\\Saved Games\DCS\Logs\dcs.log +-- All output concerning the @{#RECOVERYTANKER} class should have the string "RECOVERYTANKER" in the corresponding line. +-- Searching for lines that contain the string "error" or "nil" can also give you a hint what's wrong. +-- +-- The verbosity of the output can be increased by adding the following lines to your script: +-- +-- BASE:TraceOnOff(true) +-- BASE:TraceLevel(1) +-- BASE:TraceClass("RECOVERYTANKER") +-- +-- To get even more output you can increase the trace level to 2 or even 3, c.f. @{Core.Base#BASE} for more details. +-- +-- ## Debug Mode +-- +-- You have the option to enable the debug mode for this class via the @{#RECOVERYTANKER.SetDebugModeON} function. +-- If enabled, text messages about the tanker status will be displayed on screen and marks of the pattern created on the F10 map. +-- +-- @field #RECOVERYTANKER +RECOVERYTANKER = { + ClassName = "RECOVERYTANKER", + Debug = false, + lid = nil, + carrier = nil, + carriertype = nil, + tankergroupname = nil, + tanker = nil, + airbase = nil, + beacon = nil, + TACANchannel = nil, + TACANmode = nil, + TACANmorse = nil, + TACANon = nil, + RadioFreq = nil, + RadioModu = nil, + altitude = nil, + speed = nil, + distStern = nil, + distBow = nil, + dTupdate = nil, + Dupdate = nil, + Hupdate = nil, + Tupdate = nil, + takeoff = nil, + lowfuel = nil, + respawn = nil, + respawninair = nil, + uncontrolledac = nil, + orientation = nil, + orientlast = nil, + position = nil, + alias = nil, + uid = 0, + awacs = nil, + callsignname = nil, + callsignnumber = nil, + modex = nil, +} + +--- Unique ID (global). +-- @field #number UID Unique ID (global). +RECOVERYTANKER.UID=0 + +--- Class version. +-- @field #string version +RECOVERYTANKER.version="1.0.6" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- DONE: Is alive check for tanker necessary? +-- DONE: Seamless change of position update. Get good updated waypoint and update position if tanker position is right. Not really possiple atm. +-- DONE: Check if TACAN mode "X" is allowed for AA TACAN stations. Nope +-- DONE: Check if tanker is going back to "Running" state after RTB and respawn. +-- DONE: Write documentation. +-- DONE: Trace functions self:T instead of self:I for less output. +-- DONE: Make pattern update parameters (distance, orientation) input parameters. +-- DONE: Add FSM event for pattern update. +-- DONE: Smarter pattern update function. E.g. (small) zone around carrier. Only update position when carrier leaves zone or changes heading? +-- DONE: Set AA TACAN. +-- DONE: Add refueling event/state. +-- DONE: Possibility to add already present/spawned aircraft, e.g. for warehouse. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create new RECOVERYTANKER object. +-- @param #RECOVERYTANKER self +-- @param Wrapper.Unit#UNIT carrierunit Carrier unit. +-- @param #string tankergroupname Name of the late activated tanker aircraft template group. +-- @return #RECOVERYTANKER RECOVERYTANKER object. +function RECOVERYTANKER:New(carrierunit, tankergroupname) + + -- Inherit everthing from FSM class. + local self = BASE:Inherit(self, FSM:New()) -- #RECOVERYTANKER + + if type(carrierunit)=="string" then + self.carrier=UNIT:FindByName(carrierunit) + else + self.carrier=carrierunit + end + + -- Carrier type. + self.carriertype=self.carrier:GetTypeName() + + -- Tanker group name. + self.tankergroupname=tankergroupname + + -- Increase unique ID. + RECOVERYTANKER.UID=RECOVERYTANKER.UID+1 + + -- Unique ID of this tanker. + self.uid=RECOVERYTANKER.UID + + -- Save self in static object. Easier to retrieve later. + self.carrier:SetState(self.carrier, string.format("RECOVERYTANKER_%d", self.uid) , self) + + -- Set unique spawn alias. + self.alias=string.format("%s_%s_%02d", self.carrier:GetName(), self.tankergroupname, RECOVERYTANKER.UID) + + -- Log ID. + self.lid=string.format("RECOVERYTANKER %s | ", self.alias) + + -- Init default parameters. + self:SetAltitude() + self:SetSpeed() + self:SetRacetrackDistances() + self:SetHomeBase(AIRBASE:FindByName(self.carrier:GetName())) + self:SetTakeoffHot() + self:SetLowFuelThreshold() + self:SetRespawnOnOff() + self:SetTACAN() + self:SetRadio() + self:SetPatternUpdateDistance() + self:SetPatternUpdateHeading() + self:SetPatternUpdateInterval() + self:SetAWACS(false) + + -- Debug trace. + if false then + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(1) + end + + ----------------------- + --- FSM Transitions --- + ----------------------- + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start the FSM. + self:AddTransition("*", "RefuelStart", "Refueling") -- Tanker has started to refuel another unit. + self:AddTransition("*", "RefuelStop", "Running") -- Tanker starts to refuel. + self:AddTransition("*", "Run", "Running") -- Tanker starts normal operation again. + self:AddTransition("Running", "RTB", "Returning") -- Tanker is returning to base (for fuel). + self:AddTransition("*", "Status", "*") -- Status update. + self:AddTransition("Running", "PatternUpdate", "*") -- Update pattern wrt to carrier. + self:AddTransition("*", "Stop", "Stopped") -- Stop the FSM. + + + --- Triggers the FSM event "Start" that starts the recovery tanker. Initializes parameters and starts event handlers. + -- @function [parent=#RECOVERYTANKER] Start + -- @param #RECOVERYTANKER self + + --- Triggers the FSM event "Start" that starts the recovery tanker after a delay. Initializes parameters and starts event handlers. + -- @function [parent=#RECOVERYTANKER] __Start + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "RefuelStart" when the tanker starts refueling another aircraft. + -- @function [parent=#RECOVERYTANKER] RefuelStart + -- @param #RECOVERYTANKER self + -- @param Wrapper.Unit#UNIT receiver Unit receiving fuel from the tanker. + + --- On after "RefuelStart" event user function. Called when a the the tanker started to refuel another unit. + -- @function [parent=#RECOVERYTANKER] OnAfterRefuelStart + -- @param #RECOVERYTANKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Unit#UNIT receiver Unit receiving fuel from the tanker. + + + --- Triggers the FSM event "RefuelStop" when the tanker stops refueling another aircraft. + -- @function [parent=#RECOVERYTANKER] RefuelStop + -- @param #RECOVERYTANKER self + -- @param Wrapper.Unit#UNIT receiver Unit stoped receiving fuel from the tanker. + + --- On after "RefuelStop" event user function. Called when a the the tanker stopped to refuel another unit. + -- @function [parent=#RECOVERYTANKER] OnAfterRefuelStop + -- @param #RECOVERYTANKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Unit#UNIT receiver Unit that received fuel from the tanker. + + + --- Triggers the FSM event "Run". Simply puts the group into "Running" state. + -- @function [parent=#RECOVERYTANKER] Run + -- @param #RECOVERYTANKER self + + --- Triggers delayed the FSM event "Run". Simply puts the group into "Running" state. + -- @function [parent=#RECOVERYTANKER] __Run + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "RTB" that sends the tanker home. + -- @function [parent=#RECOVERYTANKER] RTB + -- @param #RECOVERYTANKER self + -- @param Wrapper.Airbase#AIRBASE airbase The airbase where the tanker should return to. + + --- Triggers the FSM event "RTB" that sends the tanker home after a delay. + -- @function [parent=#RECOVERYTANKER] __RTB + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase where the tanker should return to. + + --- On after "RTB" event user function. Called when a the the tanker returns to its home base. + -- @function [parent=#RECOVERYTANKER] OnAfterRTB + -- @param #RECOVERYTANKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase where the tanker should return to. + + + --- Triggers the FSM event "Status" that updates the tanker status. + -- @function [parent=#RECOVERYTANKER] Status + -- @param #RECOVERYTANKER self + + --- Triggers the delayed FSM event "Status" that updates the tanker status. + -- @function [parent=#RECOVERYTANKER] __Status + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "PatternUpdate" that updates the pattern of the tanker wrt to the carrier position. + -- @function [parent=#RECOVERYTANKER] PatternUpdate + -- @param #RECOVERYTANKER self + + --- Triggers the delayed FSM event "PatternUpdate" that updates the pattern of the tanker wrt to the carrier position. + -- @function [parent=#RECOVERYTANKER] __PatternUpdate + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + + --- On after "PatternEvent" event user function. Called when a the pattern of the tanker is updated. + -- @function [parent=#RECOVERYTANKER] OnAfterPatternUpdate + -- @param #RECOVERYTANKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Stop" that stops the recovery tanker. Event handlers are stopped. + -- @function [parent=#RECOVERYTANKER] Stop + -- @param #RECOVERYTANKER self + + --- Triggers the FSM event "Stop" that stops the recovery tanker after a delay. Event handlers are stopped. + -- @function [parent=#RECOVERYTANKER] __Stop + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set the speed the tanker flys in its orbit pattern. +-- @param #RECOVERYTANKER self +-- @param #number speed True air speed (TAS) in knots. Default 274 knots, which results in ~250 KIAS. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetSpeed(speed) + self.speed=UTILS.KnotsToMps(speed or 274) + return self +end + +--- Set orbit pattern altitude of the tanker. +-- @param #RECOVERYTANKER self +-- @param #number altitude Tanker altitude in feet. Default 6000 ft. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetAltitude(altitude) + self.altitude=UTILS.FeetToMeters(altitude or 6000) + return self +end + +--- Set race-track distances. +-- @param #RECOVERYTANKER self +-- @param #number distbow Distance [NM] in front of the carrier. Default 10 NM. +-- @param #number diststern Distance [NM] behind the carrier. Default 4 NM. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRacetrackDistances(distbow, diststern) + self.distBow=UTILS.NMToMeters(distbow or 10) + self.distStern=-UTILS.NMToMeters(diststern or 4) + return self +end + +--- Set minimum pattern update interval. After a pattern update this time interval has to pass before the next update is allowed. +-- @param #RECOVERYTANKER self +-- @param #number interval Min interval in minutes. Default is 10 minutes. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetPatternUpdateInterval(interval) + self.dTupdate=(interval or 10)*60 + return self +end + +--- Set pattern update distance threshold. Tanker will update its pattern when the carrier changes its position by more than this distance. +-- @param #RECOVERYTANKER self +-- @param #number distancechange Distance threshold in NM. Default 5 NM (=9.62 km). +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetPatternUpdateDistance(distancechange) + self.Dupdate=UTILS.NMToMeters(distancechange or 5) + return self +end + +--- Set pattern update heading threshold. Tanker will update its pattern when the carrier changes its heading by more than this value. +-- @param #RECOVERYTANKER self +-- @param #number headingchange Heading threshold in degrees. Default 5 degrees. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetPatternUpdateHeading(headingchange) + self.Hupdate=headingchange or 5 + return self +end + +--- Set low fuel state of tanker. When fuel is below this threshold, the tanker will RTB or be respawned if takeoff type is in air. +-- @param #RECOVERYTANKER self +-- @param #number fuelthreshold Low fuel threshold in percent. Default 10 % of max fuel. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetLowFuelThreshold(fuelthreshold) + self.lowfuel=fuelthreshold or 10 + return self +end + +--- Set home airbase of the tanker. This is the airbase where the tanker will go when it is out of fuel. +-- @param #RECOVERYTANKER self +-- @param Wrapper.Airbase#AIRBASE airbase The home airbase. Can be the airbase name or a Moose AIRBASE object. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetHomeBase(airbase) + if type(airbase)=="string" then + self.airbase=AIRBASE:FindByName(airbase) + else + self.airbase=airbase + end + if not self.airbase then + self:E(self.lid.."ERROR: Airbase is nil!") + end + return self +end + +--- Set that the group takes the roll of an AWACS instead of a refueling tanker. +-- @param #RECOVERYTANKER self +-- @param #boolean switch If true or nil, set roll AWACS. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetAWACS(switch) + if switch==nil or switch==true then + self.awacs=true + else + self.awacs=false + end + return self +end + +--- Set callsign of the tanker group. +-- @param #RECOVERYTANKER self +-- @param #number callsignname Number +-- @param #number callsignnumber Number +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetCallsign(callsignname, callsignnumber) + self.callsignname=callsignname + self.callsignnumber=callsignnumber + return self +end + +--- Set modex (tail number) of the tanker. +-- @param #RECOVERYTANKER self +-- @param #number modex Tail number. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetModex(modex) + self.modex=modex + return self +end + +--- Set takeoff type. +-- @param #RECOVERYTANKER self +-- @param #number takeofftype Takeoff type. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetTakeoff(takeofftype) + self.takeoff=takeofftype + return self +end + +--- Set takeoff with engines running (hot). +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetTakeoffHot() + self:SetTakeoff(SPAWN.Takeoff.Hot) + return self +end + +--- Set takeoff with engines off (cold). +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetTakeoffCold() + self:SetTakeoff(SPAWN.Takeoff.Cold) + return self +end + +--- Set takeoff in air at the defined pattern altitude and ~10 NM astern the carrier. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetTakeoffAir() + self:SetTakeoff(SPAWN.Takeoff.Air) + return self +end + +--- Enable respawning of tanker. Note that this is the default behaviour. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRespawnOn() + self.respawn=true + return self +end + +--- Disable respawning of tanker. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRespawnOff() + self.respawn=false + return self +end + +--- Set whether tanker shall be respawned or not. +-- @param #RECOVERYTANKER self +-- @param #boolean switch If true (or nil), tanker will be respawned. If false, tanker will not be respawned. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRespawnOnOff(switch) + if switch==nil or switch==true then + self.respawn=true + else + self.respawn=false + end + return self +end + +--- Tanker will be respawned in air, even it was initially spawned on the carrier. +-- So only the first spawn will be on the carrier while all subsequent spawns will happen in air. +-- This allows for undisrupted operations and less problems on the carrier deck. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRespawnInAir() + self.respawninair=true + return self +end + +--- Use an uncontrolled aircraft already present in the mission rather than spawning a new tanker as initial recovery thanker. +-- This can be useful when interfaced with, e.g., a MOOSE @{Functional.Warehouse#WAREHOUSE}. +-- The group name is the one specified in the @{#RECOVERYTANKER.New} function. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetUseUncontrolledAircraft() + self.uncontrolledac=true + return self +end + + +--- Disable automatic TACAN activation. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetTACANoff() + self.TACANon=false + return self +end + +--- Set TACAN channel of tanker. Note that mode is automatically set to "Y" for AA TACAN since only that works. +-- @param #RECOVERYTANKER self +-- @param #number channel TACAN channel. Default 1. +-- @param #string morse TACAN morse code identifier. Three letters. Default "TKR". +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetTACAN(channel, morse) + self.TACANchannel=channel or 1 + self.TACANmode="Y" + self.TACANmorse=morse or "TKR" + self.TACANon=true + return self +end + +--- Set radio frequency and optionally modulation of the tanker. +-- @param #RECOVERYTANKER self +-- @param #number frequency Radio frequency in MHz. Default 251 MHz. +-- @param #string modulation Radio modulation, either "AM" or "FM". Default "AM". +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRadio(frequency, modulation) + self.RadioFreq=frequency or 251 + self.RadioModu=modulation or "AM" + return self +end + +--- Activate debug mode. Marks of pattern on F10 map and debug messages displayed on screen. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetDebugModeON() + self.Debug=true + return self +end + +--- Deactivate debug mode. This is also the default setting. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetDebugModeOFF() + self.Debug=false + return self +end + +--- Check if tanker is currently returning to base. +-- @param #RECOVERYTANKER self +-- @return #boolean If true, tanker is returning to base. +function RECOVERYTANKER:IsReturning() + return self:is("Returning") +end + +--- Check if tanker is currently operating. +-- @param #RECOVERYTANKER self +-- @return #boolean If true, tanker is operating. +function RECOVERYTANKER:IsRunning() + return self:is("Running") +end + +--- Check if tanker is currently refueling another aircraft. +-- @param #RECOVERYTANKER self +-- @return #boolean If true, tanker is refueling. +function RECOVERYTANKER:IsRefueling() + return self:is("Refueling") +end + +--- Check if FMS was stopped. +-- @param #RECOVERYTANKER self +-- @return #boolean If true, is stopped. +function RECOVERYTANKER:IsStopped() + return self:is("Stopped") +end + +--- Alias of tanker spawn group. +-- @param #RECOVERYTANKER self +-- @return #string Alias of the tanker. +function RECOVERYTANKER:GetAlias() + return self.alias +end + +--- Get unit name of the spawned tanker. +-- @param #RECOVERYTANKER self +-- @return #string Name of the tanker unit or nil if it does not exist. +function RECOVERYTANKER:GetUnitName() + local unit=self.tanker:GetUnit(1) + if unit then + return unit:GetName() + end + return nil +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM states +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the warehouse. Addes event handlers and schedules status updates of reqests and queue. +-- @param #RECOVERYTANKER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RECOVERYTANKER:onafterStart(From, Event, To) + + -- Info on start. + self:I(string.format("Starting Recovery Tanker v%s for carrier unit %s of type %s for tanker group %s.", RECOVERYTANKER.version, self.carrier:GetName(), self.carriertype, self.tankergroupname)) + + -- Handle events. + self:HandleEvent(EVENTS.EngineShutdown) + self:HandleEvent(EVENTS.Refueling, self._RefuelingStart) --Need explicit functions since OnEventRefueling and OnEventRefuelingStop did not hook! + self:HandleEvent(EVENTS.RefuelingStop, self._RefuelingStop) + self:HandleEvent(EVENTS.Crash, self._OnEventCrashOrDead) + self:HandleEvent(EVENTS.Dead, self._OnEventCrashOrDead) + + -- Spawn tanker. We need to introduce an alias in case this class is used twice. This would confuse the spawn routine. + local Spawn=SPAWN:NewWithAlias(self.tankergroupname, self.alias) + + -- Set radio frequency and modulation. + Spawn:InitRadioCommsOnOff(true) + Spawn:InitRadioFrequency(self.RadioFreq) + Spawn:InitRadioModulation(self.RadioModu) + Spawn:InitModex(self.modex) + + -- Spawn on carrier. + if self.takeoff==SPAWN.Takeoff.Air then + + -- Carrier heading + local hdg=self.carrier:GetHeading() + + -- Spawn distance behind the carrier. + local dist=-self.distStern+UTILS.NMToMeters(4) + + -- Coordinate behind the carrier and slightly port. + local Carrier=self.carrier:GetCoordinate():Translate(dist, hdg+190):SetAltitude(self.altitude) + + -- Orientation of spawned group. + Spawn:InitHeading(hdg+10) + + -- Spawn at coordinate. + self.tanker=Spawn:SpawnFromCoordinate(Carrier) + + else + + -- Check if an uncontrolled tanker group was requested. + if self.useuncontrolled then + + -- Use an uncontrolled aircraft group. + self.tanker=GROUP:FindByName(self.tankergroupname) + + if self.tanker:IsAlive() then + + -- Start uncontrolled group. + self.tanker:StartUncontrolled() + + else + -- No group by that name! + self:E(string.format("ERROR: No uncontrolled (alive) tanker group with name %s could be found!", self.tankergroupname)) + return + end + + else + + -- Spawn tanker at airbase. + self.tanker=Spawn:SpawnAtAirbase(self.airbase, self.takeoff) + + end + + end + + -- Initialize route. self.distStern<0! + SCHEDULER:New(nil, self._InitRoute, {self, -self.distStern+UTILS.NMToMeters(3)}, 1) + + -- Create tanker beacon. + if self.TACANon then + self:_ActivateTACAN(2) + end + + -- Set callsign. + if self.callsignname then + self.tanker:CommandSetCallsign(self.callsignname, self.callsignnumber, 2) + end + + -- Get initial orientation and position of carrier. + self.orientation=self.carrier:GetOrientationX() + self.orientlast=self.carrier:GetOrientationX() + self.position=self.carrier:GetCoordinate() + + -- Init status updates in 10 seconds. + self:__Status(10) +end + + +--- On after Status event. Checks player status. +-- @param #RECOVERYTANKER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RECOVERYTANKER:onafterStatus(From, Event, To) + + -- Get current time. + local time=timer.getTime() + + if self.tanker:IsAlive() then + + --------------------- + -- TANKER is ALIVE -- + --------------------- + + -- Get fuel of tanker. + local fuel=self.tanker:GetFuel()*100 + local text=string.format("Recovery tanker %s: state=%s fuel=%.1f", self.tanker:GetName(), self:GetState(), fuel) + self:T(self.lid..text) + + -- Check if tanker is running and not RTBing or refueling. + if self:IsRunning() then + + -- Check fuel. + if fuel 100 meters, this should be another tanker. + if dist>100 then + return + end + + -- Info message. + local text=string.format("Recovery tanker %s started refueling unit %s", self.tanker:GetName(), receiver:GetName()) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- FMS state "Refueling". + self:RefuelStart(receiver) + + end + +end + +--- Event handler for refueling stopped. +-- @param #RECOVERYTANKER self +-- @param Core.Event#EVENTDATA EventData Event data. +function RECOVERYTANKER:_RefuelingStop(EventData) + + if EventData and EventData.IniUnit and EventData.IniUnit:IsAlive() then + + -- Unit receiving fuel. + local receiver=EventData.IniUnit + + -- Get distance to tanker to check that unit is receiving fuel from this tanker. + local dist=receiver:GetCoordinate():Get2DDistance(self.tanker:GetCoordinate()) + + -- If distance > 100 meters, this should be another tanker. + if dist>100 then + return + end + + -- Info message. + local text=string.format("Recovery tanker %s stopped refueling unit %s", self.tanker:GetName(), receiver:GetName()) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- FSM state "Running". + self:RefuelStop(receiver) + end + +end + +--- A unit crashed or died. +-- @param #RECOVERYTANKER self +-- @param Core.Event#EVENTDATA EventData Event data. +function RECOVERYTANKER:_OnEventCrashOrDead(EventData) + self:F2({eventdata=EventData}) + + -- Check that there is an initiating unit in the event data. + if EventData and EventData.IniUnit then + + -- Crashed or dead unit. + local unit=EventData.IniUnit + local unitname=tostring(EventData.IniUnitName) + + -- Check that it was the tanker that crashed. + if EventData.IniGroupName==self.tanker:GetName() then + + -- Error message. + self:E(self.lid..string.format("Recovery tanker %s crashed!", unitname)) + + -- Stop FSM. + self:Stop() + + -- Restart. + if self.respawn then + self:__Start(5) + end + + end + + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- MISC functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Task function to +-- @param #RECOVERYTANKER self +function RECOVERYTANKER:_InitPatternTaskFunction() + + -- Name of the warehouse (static) object. + local carriername=self.carrier:GetName() + + -- Task script. + local DCSScript = {} + DCSScript[#DCSScript+1] = string.format('local mycarrier = UNIT:FindByName(\"%s\") ', carriername) -- The carrier unit that holds the self object. + DCSScript[#DCSScript+1] = string.format('local mytanker = mycarrier:GetState(mycarrier, \"RECOVERYTANKER_%d\") ', self.uid) -- Get the RECOVERYTANKER self object. + DCSScript[#DCSScript+1] = string.format('mytanker:PatternUpdate()') -- Call the function, e.g. mytanker.(self) + + -- Create task. + local DCSTask = CONTROLLABLE.TaskWrappedAction(self, CONTROLLABLE.CommandDoScript(self, table.concat(DCSScript))) + + return DCSTask +end + +--- Init waypoint after spawn. Tanker is first guided to a position astern the carrier and starts its racetrack pattern from there. +-- @param #RECOVERYTANKER self +-- @param #number dist Distance [NM] of initial waypoint astern carrier. Default 8 NM. +-- @param #number delay Delay before routing in seconds. Default 1 second. +function RECOVERYTANKER:_InitRoute(dist, delay) + + -- Defaults. + dist=dist or UTILS.NMToMeters(8) + delay=delay or 1 + + -- Debug message. + self:T(self.lid..string.format("Initializing route of recovery tanker %s.", self.tanker:GetName())) + + -- Carrier position. + local Carrier=self.carrier:GetCoordinate() + + -- Carrier heading. + local hdg=self.carrier:GetHeading() + + -- First waypoint is ~10 NM behind and slightly port the boat. + local p=Carrier:Translate(dist, hdg+190):SetAltitude(self.altitude) + + -- Speed for waypoints in km/h. + -- This causes a problem, because the tanker might not be alive yet ==> We schedule the call of _InitRoute + local speed=self.tanker:GetSpeedMax()*0.8 + + -- Set to 280 knots and convert to km/h. + --local speed=280/0.539957 + + -- Debug mark. + if self.Debug then + p:MarkToAll(string.format("Enter Pattern WP: alt=%d ft, speed=%d kts", UTILS.MetersToFeet(self.altitude), speed*0.539957)) + end + + -- Task to update pattern when wp 2 is reached. + local task=self:_InitPatternTaskFunction() + + -- Waypoints. + local wp={} + if self.takeoff==SPAWN.Takeoff.Air then + wp[#wp+1]=self.tanker:GetCoordinate():SetAltitude(self.altitude):WaypointAirTurningPoint(nil, speed, {}, "Spawn Position") + else + wp[#wp+1]=Carrier:WaypointAirTakeOffParking() + end + wp[#wp+1]=p:WaypointAirTurningPoint(nil, speed, {task}, "Enter Pattern") + + -- Set route. + self.tanker:Route(wp, delay) + + -- Set state to Running. Necessary when tanker was RTB and respawned since it is probably in state "Returning". + self:__Run(1) + + -- No update yet, wait until the function is called (avoids checks if pattern update is needed). + self.Tupdate=nil +end + +--- Check if heading or position have changed significantly. +-- @param #RECOVERYTANKER self +-- @param #number dt Time since last update in seconds. +-- @return #boolean If true, heading and/or position have changed more than 5 degrees or 10 km, respectively. +function RECOVERYTANKER:_CheckPatternUpdate(dt) + + -- Get current position and orientation of carrier. + local pos=self.carrier:GetCoordinate() + + -- Current orientation of carrier. + local vNew=self.carrier:GetOrientationX() + + -- Reference orientation of carrier after the last update + local vOld=self.orientation + + -- Last orientation from 30 seconds ago. + local vLast=self.orientlast + + -- We only need the X-Z plane. + vNew.y=0 ; vOld.y=0 ; vLast.y=0 + + -- Get angle between old and new orientation vectors in rad and convert to degrees. + local deltaHeading=math.deg(math.acos(UTILS.VecDot(vNew,vOld)/UTILS.VecNorm(vNew)/UTILS.VecNorm(vOld))) + + -- Angle between current heading and last time we checked ~30 seconds ago. + local deltaLast=math.deg(math.acos(UTILS.VecDot(vNew,vLast)/UTILS.VecNorm(vNew)/UTILS.VecNorm(vLast))) + + -- Last orientation becomes new orientation + self.orientlast=vNew + + -- Carrier is turning when its heading changed by at least one degree since last check. + local turning=deltaLast>=1 + + -- Debug output if turning + if turning then + self:T2(self.lid..string.format("Carrier is turning. Delta Heading = %.1f", deltaLast)) + end + + -- Check if orientation changed. + local Hchange=false + if math.abs(deltaHeading)>=self.Hupdate then + self:T(self.lid..string.format("Carrier heading changed by %d degrees. Turning=%s.", deltaHeading, tostring(turning))) + Hchange=true + end + + -- Get distance to saved position. + local dist=pos:Get2DDistance(self.position) + + -- Check if carrier moved more than ~5 NM. + local Dchange=false + if dist>self.Dupdate then + self:T(self.lid..string.format("Carrier position changed by %.1f NM. Turning=%s.", UTILS.MetersToNM(dist), tostring(turning))) + Dchange=true + end + + -- Assume no update necessary. + local update=false + + -- No update if currently turning! Also must be running (not RTB or refueling) and T>~10 min since last position update. + if self:IsRunning() and dt>self.dTupdate and not turning then + + -- Update if heading or distance changed. + if Hchange or Dchange then + -- Debug message. + local text=string.format("Updating tanker %s pattern due to carrier position=%s or heading=%s change.", self.tanker:GetName(), tostring(Dchange), tostring(Hchange)) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- Update pos and orientation. + self.orientation=vNew + self.position=pos + update=true + end + + end + + return update +end + +--- Activate TACAN of tanker. +-- @param #RECOVERYTANKER self +-- @param #number delay Delay in seconds. +function RECOVERYTANKER:_ActivateTACAN(delay) + + if delay and delay>0 then + + -- Schedule TACAN activation. + SCHEDULER:New(nil, self._ActivateTACAN, {self}, delay) + + else + + -- Get tanker unit. + local unit=self.tanker:GetUnit(1) + + -- Check if unit is alive. + if unit:IsAlive() then + + -- Debug message. + local text=string.format("Activating recovery tanker TACAN beacon: channel=%d mode=%s, morse=%s.", self.TACANchannel, self.TACANmode, self.TACANmorse) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- Create a new beacon and activate TACAN. + self.beacon=BEACON:New(unit) + self.beacon:ActivateTACAN(self.TACANchannel, self.TACANmode, self.TACANmorse, true) + + else + self:E("ERROR: Recovery tanker is not alive!") + end + + end + +end + +--- Self made race track pattern. Not working as desired, since tanker changes course too rapidly after each waypoint. +-- @param #RECOVERYTANKER self +-- @return #table Table of pattern waypoints. +function RECOVERYTANKER:_Pattern() + + -- Carrier heading. + local hdg=self.carrier:GetHeading() + + -- Pattern altitude + local alt=self.altitude + + -- Carrier position. + local Carrier=self.carrier:GetCoordinate() + + local width=UTILS.NMToMeters(8) + + -- Define race-track pattern. + local p={} + p[1]=self.tanker:GetCoordinate() -- Tanker position + p[2]=Carrier:SetAltitude(alt) -- Carrier position + p[3]=p[2]:Translate(self.distBow, hdg) -- In front of carrier + p[4]=p[3]:Translate(width/math.sqrt(2), hdg-45) -- Middle front for smoother curve + -- Probably need one more to make it go -hdg at the waypoint. + p[5]=p[3]:Translate(width, hdg-90) -- In front on port + p[6]=p[5]:Translate(self.distStern-self.distBow, hdg) -- Behind on port (sterndist<0!) + p[7]=p[2]:Translate(self.distStern, hdg) -- Behind carrier + + local wp={} + for i=1,#p do + local coord=p[i] --Core.Point#COORDINATE + coord:MarkToAll(string.format("Waypoint %d", i)) + --table.insert(wp, coord:WaypointAirFlyOverPoint(nil , self.speed)) + table.insert(wp, coord:WaypointAirTurningPoint(nil , UTILS.MpsToKmph(self.speed))) + end + + return wp +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua new file mode 100644 index 000000000..cabe8390f --- /dev/null +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -0,0 +1,1284 @@ +--- **Ops** - (R2.5) - Rescue helicopter for carrier operations. +-- +-- Recue helicopter for carrier operations. +-- +-- **Main Features:** +-- +-- * Close formation with carrier. +-- * No restrictions regarding carrier waypoints and heading. +-- * Automatic respawning on empty fuel for 24/7 operations. +-- * Automatic rescuing of crashed or ejected pilots in the vicinity of the carrier. +-- * Multiple helos at different carriers due to object oriented approach. +-- * Finite State Machine (FSM) implementation. +-- +-- ## Known (DCS) Issues +-- +-- * CH-53E does only report 27.5% fuel even if fuel is set to 100% in the ME. See [bug report](https://forums.eagle.ru/showthread.php?t=223712) +-- * CH-53E does not accept USS Tarawa as landing airbase (even it can be spawned on it). +-- * Helos dont move away from their landing position on carriers. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- ### Contributions: Flightcontrol (@{AI.AI_Formation} class being used here) +-- +-- @module Ops.RescueHelo +-- @image Ops_RescueHelo.png + +--- RESCUEHELO class. +-- @type RESCUEHELO +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode on/off. +-- @field #string lid Log debug id text. +-- @field Wrapper.Unit#UNIT carrier The carrier the helo is attached to. +-- @field #string carriertype Carrier type. +-- @field #string helogroupname Name of the late activated helo template group. +-- @field Wrapper.Group#GROUP helo Helo group. +-- @field #number takeoff Takeoff type. +-- @field Wrapper.Airbase#AIRBASE airbase The airbase object acting as home base of the helo. +-- @field Core.Set#SET_GROUP followset Follow group set. +-- @field AI.AI_Formation#AI_FORMATION formation AI_FORMATION object. +-- @field #number lowfuel Low fuel threshold of helo in percent. +-- @field #number altitude Altitude of helo in meters. +-- @field #number offsetX Offset in meters to carrier in longitudinal direction. +-- @field #number offsetZ Offset in meters to carrier in latitudinal direction. +-- @field Core.Zone#ZONE_RADIUS rescuezone Zone around the carrier in which helo will rescue crashed or ejected units. +-- @field #boolean respawn If true, helo be respawned (default). If false, no respawning will happen. +-- @field #boolean respawninair If true, helo will always be respawned in air. This has no impact on the initial spawn setting. +-- @field #boolean uncontrolledac If true, use and uncontrolled helo group already present in the mission. +-- @field #boolean rescueon If true, helo will rescue crashed pilots. If false, no recuing will happen. +-- @field #number rescueduration Time the rescue helicopter hovers over the crash site in seconds. +-- @field #number rescuespeed Speed in m/s the rescue helicopter hovers at over the crash site. +-- @field #boolean rescuestopboat If true, stop carrier during rescue operations. +-- @field #boolean carrierstop If true, route of carrier was stopped. +-- @field #number HeloFuel0 Initial fuel of helo in percent. Necessary due to DCS bug that helo with full tank does not return fuel via API function. +-- @field #boolean rtb If true, Helo will be return to base on the next status check. +-- @field #number hid Unit ID of the helo group. (Global) Running number. +-- @field #string alias Alias of the spawn group. +-- @field #number uid Unique ID of this helo. +-- @field #number modex Tail number of the helo. +-- @extends Core.Fsm#FSM + +--- Rescue Helo +-- +-- === +-- +-- ![Banner Image](..\Presentations\RESCUEHELO\RescueHelo_Main.png) +-- +-- # Recue Helo +-- +-- The rescue helo will fly in close formation with another unit, which is typically an aircraft carrier. +-- It's mission is to rescue crashed or ejected pilots. Well, and to look cool... +-- +-- # Simple Script +-- +-- In the mission editor you have to set up a carrier unit, which will act as "mother". In the following, this unit will be named "*USS Stennis*". +-- +-- Secondly, you need to define a rescue helicopter group in the mission editor and set it to "**LATE ACTIVATED**". The name of the group we'll use is "*Recue Helo*". +-- +-- The basic script is very simple and consists of only two lines. +-- +-- RescueheloStennis=RESCUEHELO:New(UNIT:FindByName("USS Stennis"), "Rescue Helo") +-- RescueheloStennis:Start() +-- +-- The first line will create a new @{#RESCUEHELO} object via @{#RESCUEHELO.New} and the second line starts the process by calling @{#RESCUEHELO.Start}. +-- +-- **NOTE** that it is *very important* to define the RESCUEHELO object as **global** variable. Otherwise, the lua garbage collector will kill the formation for unknown reasons! +-- +-- By default, the helo will be spawned on the *USS Stennis* with hot engines. Then it will take off and go on station on the starboard side of the boat. +-- +-- Once the helo is out of fuel, it will return to the carrier. When the helo lands, it will be respawned immidiately and go back on station. +-- +-- If a unit crashes or a pilot ejects within a radius of 30 km from the USS Stennis, the helo will automatically fly to the crash side and +-- rescue to pilot. This will take around 5 minutes. After that, the helo will return to the Stennis, land there and bring back the poor guy. +-- When this is done, the helo will go back on station. +-- +-- # Fine Tuning +-- +-- The implementation allows to customize quite a few settings easily via user API functions. +-- +-- ## Takeoff Type +-- +-- By default, the helo is spawned with running engines on the carrier. The mission designer has set option to set the take off type via the @{#RESCUEHELO.SetTakeoff} function. +-- Or via shortcuts +-- +-- * @{#RESCUEHELO.SetTakeoffHot}(): Will set the takeoff to hot, which is also the default. +-- * @{#RESCUEHELO.SetTakeoffCold}(): Will set the takeoff type to cold, i.e. with engines off. +-- * @{#RESCUEHELO.SetTakeoffAir}(): Will set the takeoff type to air, i.e. the helo will be spawned in air near the unit which he follows. +-- +-- For example, +-- RescueheloStennis=RESCUEHELO:New(UNIT:FindByName("USS Stennis"), "Rescue Helo") +-- RescueheloStennis:SetTakeoffAir() +-- RescueheloStennis:Start() +-- will spawn the helo near the USS Stennis in air. +-- +-- Spawning in air is not as realistic but can be useful do avoid DCS bugs and shortcomings like aircraft crashing into each other on the flight deck. +-- +-- **Note** that when spawning in air is set, the helo will also not return to the boat, once it is out of fuel. Instead it will be respawned in air. +-- +-- If only the first spawning should happen on the carrier, one use the @{#RESCUEHELO.SetRespawnInAir}() function to command that all subsequent spawning +-- will happen in air. +-- +-- If the helo should no be respawned at all, one can set @{#RESCUEHELO.SetRespawnOff}(). +-- +-- ## Home Base +-- +-- It is possible to define a "home base" other than the aircraft carrier using the @{#RESCUEHELO.SetHomeBase}(*airbase*) function, where *airbase* is +-- a @{Wrapper.Airbase#AIRBASE} object or simply the name of the airbase. +-- +-- For example, one could imagine a strike group, and the helo will be spawned from another ship which has a helo pad. +-- +-- RescueheloStennis=RESCUEHELO:New(UNIT:FindByName("USS Stennis"), "Rescue Helo") +-- RescueheloStennis:SetHomeBase(AIRBASE:FindByName("USS Normandy")) +-- RescueheloStennis:Start() +-- +-- In this case, the helo will be spawned on the USS Normandy and then make its way to the USS Stennis to establish the formation. +-- Note that the distance to the mother ship should be rather small since the helo will go there very slowly. +-- +-- Once the helo runs out of fuel, it will return to the USS Normandy and not the Stennis for respawning. +-- +-- ## Formation Position +-- +-- The position of the helo relative to the mother ship can be tuned via the functions +-- +-- * @{#RESCUEHELO.SetAltitude}(*altitude*), where *altitude* is the altitude the helo flies at in meters. Default is 70 meters. +-- * @{#RESCUEHELO.SetOffsetX}(*distance*), where *distance is the distance in the direction of movement of the carrier. Default is 200 meters. +-- * @{#RESCUEHELO.SetOffsetZ}(*distance*), where *distance is the distance on the starboard side. Default is 100 meters. +-- +-- ## Rescue Operations +-- +-- By default the rescue helo will start a rescue operation if an aircraft crashes or a pilot ejects in the vicinity of the carrier. +-- This is restricted to aircraft of the same coalition as the rescue helo. Enemy (or neutral) pilots will be left on their own. +-- +-- The standard "rescue zone" has a radius of 15 NM (~28 km) around the carrier. The radius can be adjusted via the @{#RESCUEHELO.SetRescueZone}(*radius*) functions, +-- where *radius* is the radius of the zone in nautical miles. If you use multiple rescue helos in the same mission, you might want to ensure that the radii +-- are not overlapping so that two helos try to rescue the same pilot. But it should not hurt either way. +-- +-- Once the helo reaches the crash site, the rescue operation will last 5 minutes. This time can be changed by @{#RESCUEHELO.SetRescueDuration(*time*), +-- where *time* is the duration in minutes. +-- +-- During the rescue operation, the helo will hover (orbit) over the crash site at a speed of 5 knots. The speed can be set by @{#RESCUEHELO.SetRescueHoverSpeed}(*speed*), +-- where the *speed* is given in knots. +-- +-- If no rescue operations should be carried out by the helo, this option can be completely disabled by using @{#RESCUEHELO.SetRescueOff}(). +-- +-- # Finite State Machine +-- +-- The implementation uses a Finite State Machine (FSM). This allows the mission designer to hook in to certain events. +-- +-- * @{#RESCUEHELO.Start}: This eventfunction starts the FMS process and initialized parameters and spawns the helo. DCS event handling is started. +-- * @{#RESCUEHELO.Status}: This eventfunction is called in regular intervals (~60 seconds) and checks the status of the helo and carrier. It triggers other events if necessary. +-- * @{#RESCUEHELO.Rescue}: This eventfunction commands the helo to go on a rescue operation at a certain coordinate. +-- * @{#RESCUEHELO.RTB}: This eventsfunction sends the helo to its home base (usually the carrier). This is called once the helo runs low on gas. +-- * @{#RESCUEHELO.Run}: This eventfunction is called when the helo resumes normal operations and goes back on station. +-- * @{#RESCUEHELO.Stop}: This eventfunction stops the FSM by unhandling DCS events. +-- +-- The mission designer can capture these events by RESCUEHELO.OnAfter*Eventname* functions, e.g. @{#RESCUEHELO.OnAfterRescue}. +-- +-- # Debugging +-- +-- In case you have problems, it is always a good idea to have a look at your DCS log file. You find it in your "Saved Games" folder, so for example in +-- C:\Users\\Saved Games\DCS\Logs\dcs.log +-- All output concerning the @{#RESCUEHELO} class should have the string "RESCUEHELO" in the corresponding line. +-- Searching for lines that contain the string "error" or "nil" can also give you a hint what's wrong. +-- +-- The verbosity of the output can be increased by adding the following lines to your script: +-- +-- BASE:TraceOnOff(true) +-- BASE:TraceLevel(1) +-- BASE:TraceClass("RESCUEHELO") +-- +-- To get even more output you can increase the trace level to 2 or even 3, c.f. @{Core.Base#BASE} for more details. +-- +-- ## Debug Mode +-- +-- You have the option to enable the debug mode for this class via the @{#RESCUEHELO.SetDebugModeON} function. +-- If enabled, text messages about the helo status will be displayed on screen and marks of the pattern created on the F10 map. +-- +-- +-- @field #RESCUEHELO +RESCUEHELO = { + ClassName = "RESCUEHELO", + Debug = false, + lid = nil, + carrier = nil, + carriertype = nil, + helogroupname = nil, + helo = nil, + airbase = nil, + takeoff = nil, + followset = nil, + formation = nil, + lowfuel = nil, + altitude = nil, + offsetX = nil, + offsetZ = nil, + rescuezone = nil, + respawn = nil, + respawninair = nil, + uncontrolledac = nil, + rescueon = nil, + rescueduration = nil, + rescuespeed = nil, + rescuestopboat = nil, + HeloFuel0 = nil, + rtb = nil, + carrierstop = nil, + alias = nil, + uid = 0, + modex = nil, +} + +--- Unique ID (global). +-- @field #number uid Unique ID (global). +RESCUEHELO.UID=0 + +--- Class version. +-- @field #string version +RESCUEHELO.version="1.0.5" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- NOPE: Add messages for rescue mission. +-- NOPE: Add option to stop carrier while rescue operation is in progress? Done but NOT working. Postponed... +-- DONE: Write documentation. +-- DONE: Add option to deactivate the rescuing. +-- DONE: Possibility to add already present/spawned aircraft, e.g. for warehouse. +-- DONE: Add rescue event when aircraft crashes. +-- DONE: Make offset input parameter. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new RESCUEHELO object. +-- @param #RESCUEHELO self +-- @param Wrapper.Unit#UNIT carrierunit Carrier unit object or simply the unit name. +-- @param #string helogroupname Name of the late activated rescue helo template group. +-- @return #RESCUEHELO RESCUEHELO object. +function RESCUEHELO:New(carrierunit, helogroupname) + + -- Inherit everthing from FSM class. + local self = BASE:Inherit(self, FSM:New()) -- #RESCUEHELO + + -- Catch case when just the unit name is passed. + if type(carrierunit)=="string" then + self.carrier=UNIT:FindByName(carrierunit) + else + self.carrier=carrierunit + end + + -- Carrier type. + self.carriertype=self.carrier:GetTypeName() + + -- Helo group name. + self.helogroupname=helogroupname + + -- Increase ID. + RESCUEHELO.UID=RESCUEHELO.UID+1 + + -- Unique ID of this helo. + self.uid=RESCUEHELO.UID + + -- Save self in static object. Easier to retrieve later. + self.carrier:SetState(self.carrier, string.format("RESCUEHELO_%d", self.uid) , self) + + -- Set unique spawn alias. + self.alias=string.format("%s_%s_%02d", self.carrier:GetName(), self.helogroupname, RESCUEHELO.UID) + + -- Log ID. + self.lid=string.format("RESCUEHELO %s | ", self.alias) + + -- Init defaults. + self:SetHomeBase(AIRBASE:FindByName(self.carrier:GetName())) + self:SetTakeoffHot() + self:SetLowFuelThreshold() + self:SetAltitude() + self:SetOffsetX() + self:SetOffsetZ() + self:SetRespawnOn() + self:SetRescueOn() + self:SetRescueZone() + self:SetRescueHoverSpeed() + self:SetRescueDuration() + self:SetRescueStopBoatOff() + + -- Some more. + self.rtb=false + self.carrierstop=false + + -- Debug trace. + if false then + self.Debug=true + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(1) + end + + ----------------------- + --- FSM Transitions --- + ----------------------- + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") + self:AddTransition("Running", "Rescue", "Rescuing") + self:AddTransition("Running", "RTB", "Returning") + self:AddTransition("Rescuing", "RTB", "Returning") + self:AddTransition("Returning", "Returned", "Returned") + self:AddTransition("Running", "Run", "Running") + self:AddTransition("Returned", "Run", "Running") + self:AddTransition("*", "Status", "*") + self:AddTransition("*", "Stop", "Stopped") + + + --- Triggers the FSM event "Start" that starts the rescue helo. Initializes parameters and starts event handlers. + -- @function [parent=#RESCUEHELO] Start + -- @param #RESCUEHELO self + + --- Triggers the FSM event "Start" that starts the rescue helo after a delay. Initializes parameters and starts event handlers. + -- @function [parent=#RESCUEHELO] __Start + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Rescue" that sends the helo on a rescue mission to a specifc coordinate. + -- @function [parent=#RESCUEHELO] Rescue + -- @param #RESCUEHELO self + -- @param Core.Point#COORDINATE RescueCoord Coordinate where the resue mission takes place. + + --- Triggers the delayed FSM event "Rescue" that sends the helo on a rescue mission to a specifc coordinate. + -- @function [parent=#RESCUEHELO] __Rescue + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + -- @param Core.Point#COORDINATE RescueCoord Coordinate where the resue mission takes place. + + --- On after "Rescue" event user function. Called when a the the helo goes on a rescue mission. + -- @function [parent=#RESCUEHELO] OnAfterRescue + -- @param #RESCUEHELO self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Point#COORDINATE RescueCoord Crash site where the rescue operation takes place. + + + --- Triggers the FSM event "RTB" that sends the helo home. + -- @function [parent=#RESCUEHELO] RTB + -- @param #RESCUEHELO self + -- @param Wrapper.Airbase#AIRBASE airbase The airbase to return to. Default is the home base. + + --- Triggers the FSM event "RTB" that sends the helo home after a delay. + -- @function [parent=#RESCUEHELO] __RTB + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase to return to. Default is the home base. + + --- On after "RTB" event user function. Called when a the the helo returns to its home base. + -- @function [parent=#RESCUEHELO] OnAfterRTB + -- @param #RESCUEHELO self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase to return to. Default is the home base. + + --- Triggers the FSM event "Returned" after the helo has landed. + -- @function [parent=#RESCUEHELO] Returned + -- @param #RESCUEHELO self + -- @param Wrapper.Airbase#AIRBASE airbase The airbase the helo has landed. + + --- Triggers the delayed FSM event "Returned" after the helo has landed. + -- @function [parent=#RESCUEHELO] __Returned + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase the helo has landed. + + --- On after "Returned" event user function. Called when a the the helo has landed at an airbase. + -- @function [parent=#RESCUEHELO] OnAfterReturned + -- @param #RESCUEHELO self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase the helo has landed. + + + --- Triggers the FSM event "Run". + -- @function [parent=#RESCUEHELO] Run + -- @param #RESCUEHELO self + + --- Triggers the delayed FSM event "Run". + -- @function [parent=#RESCUEHELO] __Run + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Status" that updates the helo status. + -- @function [parent=#RESCUEHELO] Status + -- @param #RESCUEHELO self + + --- Triggers the delayed FSM event "Status" that updates the helo status. + -- @function [parent=#RESCUEHELO] __Status + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Stop" that stops the rescue helo. Event handlers are stopped. + -- @function [parent=#RESCUEHELO] Stop + -- @param #RESCUEHELO self + + --- Triggers the FSM event "Stop" that stops the rescue helo after a delay. Event handlers are stopped. + -- @function [parent=#RESCUEHELO] __Stop + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set low fuel state of helo. When fuel is below this threshold, the helo will RTB or be respawned if takeoff type is in air. +-- @param #RESCUEHELO self +-- @param #number threshold Low fuel threshold in percent. Default 5%. +-- @return #RESCUEHELO self +function RESCUEHELO:SetLowFuelThreshold(threshold) + self.lowfuel=threshold or 5 + return self +end + +--- Set home airbase of the helo. This is the airbase where the helo is spawned (if not in air) and will go when it is out of fuel. +-- @param #RESCUEHELO self +-- @param Wrapper.Airbase#AIRBASE airbase The home airbase. Can be the airbase name (passed as a string) or a Moose AIRBASE object. +-- @return #RESCUEHELO self +function RESCUEHELO:SetHomeBase(airbase) + if type(airbase)=="string" then + self.airbase=AIRBASE:FindByName(airbase) + else + self.airbase=airbase + end + if not self.airbase then + self:E(self.lid.."ERROR: Airbase is nil!") + end + return self +end + +--- Set rescue zone radius. Crashed or ejected units inside this radius of the carrier will be rescued if possible. +-- @param #RESCUEHELO self +-- @param #number radius Radius of rescue zone in nautical miles. Default is 15 NM. +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueZone(radius) + radius=UTILS.NMToMeters(radius or 15) + self.rescuezone=ZONE_UNIT:New("Rescue Zone", self.carrier, radius) + return self +end + +--- Set rescue hover speed. +-- @param #RESCUEHELO self +-- @param #number speed Speed in knots. Default 5 kts. +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueHoverSpeed(speed) + self.rescuespeed=UTILS.KnotsToMps(speed or 5) + return self +end + +--- Set rescue duration. This is the time it takes to rescue a pilot at the crash site. +-- @param #RESCUEHELO self +-- @param #number duration Duration in minutes. Default 5 min. +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueDuration(duration) + self.rescueduration=(duration or 5)*60 + return self +end + +--- Activate rescue option. Crashed and ejected pilots will be rescued. This is the default setting. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueOn() + self.rescueon=true + return self +end + +--- Deactivate rescue option. Crashed and ejected pilots will not be rescued. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueOff() + self.rescueon=false + return self +end + +--- Stop carrier during rescue operations. NOT WORKING! +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueStopBoatOn() + self.rescuestopboat=true + return self +end + +--- Do not stop carrier during rescue operations. This is the default setting. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueStopBoatOff() + self.rescuestopboat=false + return self +end + + +--- Set takeoff type. +-- @param #RESCUEHELO self +-- @param #number takeofftype Takeoff type. Default SPAWN.Takeoff.Hot. +-- @return #RESCUEHELO self +function RESCUEHELO:SetTakeoff(takeofftype) + self.takeoff=takeofftype or SPAWN.Takeoff.Hot + return self +end + +--- Set takeoff with engines running (hot). +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetTakeoffHot() + self:SetTakeoff(SPAWN.Takeoff.Hot) + return self +end + +--- Set takeoff with engines off (cold). +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetTakeoffCold() + self:SetTakeoff(SPAWN.Takeoff.Cold) + return self +end + +--- Set takeoff in air near the carrier. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetTakeoffAir() + self:SetTakeoff(SPAWN.Takeoff.Air) + return self +end + +--- Set altitude of helo. +-- @param #RESCUEHELO self +-- @param #number alt Altitude in meters. Default 70 m. +-- @return #RESCUEHELO self +function RESCUEHELO:SetAltitude(alt) + self.altitude=alt or 70 + return self +end + +--- Set offset parallel to orientation of carrier. +-- @param #RESCUEHELO self +-- @param #number distance Offset distance in meters. Default 200 m (~660 ft). +-- @return #RESCUEHELO self +function RESCUEHELO:SetOffsetX(distance) + self.offsetX=distance or 200 + return self +end + +--- Set offset perpendicular to orientation to carrier. +-- @param #RESCUEHELO self +-- @param #number distance Offset distance in meters. Default 240 m (~780 ft). +-- @return #RESCUEHELO self +function RESCUEHELO:SetOffsetZ(distance) + self.offsetZ=distance or 240 + return self +end + + +--- Enable respawning of helo. Note that this is the default behaviour. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRespawnOn() + self.respawn=true + return self +end + +--- Disable respawning of helo. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRespawnOff() + self.respawn=false + return self +end + +--- Set whether helo shall be respawned or not. +-- @param #RESCUEHELO self +-- @param #boolean switch If true (or nil), helo will be respawned. If false, helo will not be respawned. +-- @return #RESCUEHELO self +function RESCUEHELO:SetRespawnOnOff(switch) + if switch==nil or switch==true then + self.respawn=true + else + self.respawn=false + end + return self +end + +--- Helo will be respawned in air, even it was initially spawned on the carrier. +-- So only the first spawn will be on the carrier while all subsequent spawns will happen in air. +-- This allows for undisrupted operations and less problems on the carrier deck. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRespawnInAir() + self.respawninair=true + return self +end + +--- Set modex (tail number) of the helo. +-- @param #RESCUEHELO self +-- @param #number modex Tail number. +-- @return #RESCUEHELO self +function RESCUEHELO:SetModex(modex) + self.modex=modex + return self +end + +--- Use an uncontrolled aircraft already present in the mission rather than spawning a new helo as initial rescue helo. +-- This can be useful when interfaced with, e.g., a warehouse. +-- The group name is the one specified in the @{#RESCUEHELO.New} function. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetUseUncontrolledAircraft() + self.uncontrolledac=true + return self +end + +--- Activate debug mode. Display debug messages on screen. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetDebugModeON() + self.Debug=true + return self +end + +--- Deactivate debug mode. This is also the default setting. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetDebugModeOFF() + self.Debug=false + return self +end + +--- Check if helo is returning to base. +-- @param #RESCUEHELO self +-- @return #boolean If true, helo is returning to base. +function RESCUEHELO:IsReturning() + return self:is("Returning") +end + +--- Check if helo is operating. +-- @param #RESCUEHELO self +-- @return #boolean If true, helo is operating. +function RESCUEHELO:IsRunning() + return self:is("Running") +end + +--- Check if helo is on a rescue mission. +-- @param #RESCUEHELO self +-- @return #boolean If true, helo is rescuing somebody. +function RESCUEHELO:IsRescuing() + return self:is("Rescuing") +end + +--- Check if FMS was stopped. +-- @param #RESCUEHELO self +-- @return #boolean If true, is stopped. +function RESCUEHELO:IsStopped() + return self:is("Stopped") +end + +--- Alias of helo spawn group. +-- @param #RESCUEHELO self +-- @return #string Alias of the helo. +function RESCUEHELO:GetAlias() + return self.alias +end + +--- Get unit name of the spawned helo. +-- @param #RESCUEHELO self +-- @return #string Name of the helo unit or nil if it does not exist. +function RESCUEHELO:GetUnitName() + local unit=self.helo:GetUnit(1) + if unit then + return unit:GetName() + end + return nil +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- EVENT functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Handle landing event of rescue helo. +-- @param #RESCUEHELO self +-- @param Core.Event#EVENTDATA EventData Event data. +function RESCUEHELO:OnEventLand(EventData) + local group=EventData.IniGroup --Wrapper.Group#GROUP + + if group:IsAlive() then + + -- Group name that landed. + local groupname=group:GetName() + + -- Check that it was our helo that landed. + if groupname==self.helo:GetName() then + + -- Respawn the Helo. + local text=string.format("Respawning rescue helo group %s at home base.", groupname) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- Helo has rescued someone. + -- TODO: Add "Rescued" event. + if self:IsRescuing() then + self:T(self.lid..string.format("Rescue helo %s returned from rescue operation.", groupname)) + end + + -- Check if takeoff air or respawn in air is set. Landing event should not happen unless the helo was on a rescue mission. + if self.takeoff==SPAWN.Takeoff.Air or self.respawninair then + + if not self:IsRescuing() then + + self:E(self.lid..string.format("WARNING: Rescue helo %s landed. This should not happen for Takeoff=Air or respawninair=true and no rescue operation in progress.", groupname)) + + end + end + + -- Trigger returned event. Respawn at current airbase. + self:__Returned(3, EventData.Place) + + end + end +end + +--- A unit crashed or a player ejected. +-- @param #RESCUEHELO self +-- @param Core.Event#EVENTDATA EventData Event data. +function RESCUEHELO:_OnEventCrashOrEject(EventData) + self:F2({eventdata=EventData}) + + -- NOTE: Careful here. Eject and crash events will probably happen for the same unit! + + -- Check that there is an initiating unit in the event data. + if EventData and EventData.IniUnit then + + -- Crashed or ejected unit. + local unit=EventData.IniUnit + local unitname=tostring(EventData.IniUnitName) + + -- Check that it was not the rescue helo itself that crashed. + if EventData.IniGroupName~=self.helo:GetName() then + + -- Debug. + local text=string.format("Unit %s crashed or ejected.", unitname) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- Unit "alive" and in our rescue zone. + if unit:IsAlive() and unit:IsInZone(self.rescuezone) then + + -- Get coordinate of crashed unit. + local coord=unit:GetCoordinate() + + -- Debug mark on map. + if self.Debug then + coord:MarkToCoalition(self.lid..string.format("Crash site of unit %s.", unitname), self.helo:GetCoalition()) + end + + -- Check that coalition is the same. + local rightcoalition=EventData.IniGroup:GetCoalition()==self.helo:GetCoalition() + + -- Only rescue if helo is "running" and not, e.g., rescuing already. + if self:IsRunning() and self.rescueon and rightcoalition then + self:Rescue(coord) + end + + end + + else + + -- Error message. + self:E(self.lid..string.format("Rescue helo %s crashed!", unitname)) + + -- Stop FSM. + self:Stop() + + -- Restart. + if self.respawn then + self:__Start(5) + end + + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM states +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the warehouse. Addes event handlers and schedules status updates of reqests and queue. +-- @param #RESCUEHELO self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RESCUEHELO:onafterStart(From, Event, To) + + -- Events are handled my MOOSE. + local text=string.format("Starting Rescue Helo Formation v%s for carrier unit %s of type %s.", RESCUEHELO.version, self.carrier:GetName(), self.carriertype) + self:I(self.lid..text) + + -- Handle events. + --self:HandleEvent(EVENTS.Birth) + self:HandleEvent(EVENTS.Land) + self:HandleEvent(EVENTS.Crash, self._OnEventCrashOrEject) + self:HandleEvent(EVENTS.Ejection, self._OnEventCrashOrEject) + + -- Delay before formation is started. + local delay=120 + + -- Spawn helo. We need to introduce an alias in case this class is used twice. This would confuse the spawn routine. + local Spawn=SPAWN:NewWithAlias(self.helogroupname, self.alias) + + -- Set modex for spawn. + Spawn:InitModex(self.modex) + + -- Spawn in air or at airbase. + if self.takeoff==SPAWN.Takeoff.Air then + + -- Carrier heading + local hdg=self.carrier:GetHeading() + + -- Spawn distance in front of carrier. + local dist=UTILS.NMToMeters(0.2) + + -- Coordinate behind the carrier. Altitude at least 100 meters for spawning because it drops down a bit. + local Carrier=self.carrier:GetCoordinate():Translate(dist, hdg):SetAltitude(math.max(100, self.altitude)) + + -- Orientation of spawned group. + Spawn:InitHeading(hdg) + + -- Spawn at coordinate. + self.helo=Spawn:SpawnFromCoordinate(Carrier) + + -- Start formation in 1 seconds + delay=1 + + else + + -- Check if an uncontrolled helo group was requested. + if self.useuncontrolled then + + -- Use an uncontrolled aircraft group. + self.helo=GROUP:FindByName(self.helogroupname) + + if self.helo:IsAlive() then + + -- Start uncontrolled group. + self.helo:StartUncontrolled() + + -- Delay before formation is started. + delay=60 + + else + -- No group of that name! + self:E(string.format("ERROR: No uncontrolled (alive) rescue helo group with name %s could be found!", self.helogroupname)) + return + end + + else + + -- Spawn at airbase. + self.helo=Spawn:SpawnAtAirbase(self.airbase, self.takeoff) + + -- Delay before formation is started. + if self.takeoff==SPAWN.Takeoff.Runway then + delay=5 + elseif self.takeoff==SPAWN.Takeoff.Hot then + delay=30 + elseif self.takeoff==SPAWN.Takeoff.Cold then + delay=60 + end + + end + + end + + -- Set of group(s) to follow Mother. + self.followset=SET_GROUP:New() + self.followset:AddGroup(self.helo) + + -- Get initial fuel. + self.HeloFuel0=self.helo:GetFuel() + + -- Define AI Formation object. + self.formation=AI_FORMATION:New(self.carrier, self.followset, "Helo Formation with Carrier", "Follow Carrier at given parameters.") + + -- Formation parameters. + self.formation:FormationCenterWing(-self.offsetX, 50, math.abs(self.altitude), 50, self.offsetZ, 50) + + -- Start formation FSM. + self.formation:__Start(delay) + + -- Init status check + self:__Status(1) +end + +--- On after Status event. Checks player status. +-- @param #RESCUEHELO self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RESCUEHELO:onafterStatus(From, Event, To) + + -- Get current time. + local time=timer.getTime() + + -- Check if helo is running and not RTBing already or rescuing. + if self.helo:IsAlive() then + + ------------------- + -- HELO is ALIVE -- + ------------------- + + -- Get (relative) fuel wrt to initial fuel of helo (DCS bug https://forums.eagle.ru/showthread.php?t=223712) + local fuel=self.helo:GetFuel()*100 + local fuelrel=fuel/self.HeloFuel0 + local life=self.helo:GetUnit(1):GetLife() + local life0=self.helo:GetUnit(1):GetLife0() + + -- Report current fuel. + local text=string.format("Rescue Helo %s: state=%s fuel=%.1f, rel.fuel=%.1f, life=%.1f/%.1f", self.helo:GetName(), self:GetState(), fuel, fuelrel, life, life0) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + if self:IsRunning() then + + -- Check if fuel is low. + if fuel1 then @@ -680,3 +778,131 @@ function UTILS.VecCross(a, b) return {x=a.y*b.z - a.z*b.y, y=a.z*b.x - a.x*b.z, z=a.x*b.y - a.y*b.x} end +--- Calculate the difference between two 3D vectors by substracting the x,y,z components from each other. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. +-- @param DCS#Vec3 b Vector in 3D with x, y, z components. +-- @return DCS#Vec3 Vector c=a-b with c(i)=a(i)-b(i), i=x,y,z. +function UTILS.VecSubstract(a, b) + return {x=a.x-b.x, y=a.y-b.y, z=a.z-b.z} +end + +--- Calculate the angle between two 3D vectors. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. +-- @param DCS#Vec3 b Vector in 3D with x, y, z components. +-- @return #number Angle alpha between and b in degrees. alpha=acos(a*b)/(|a||b|), (* denotes the dot product). +function UTILS.VecAngle(a, b) + local alpha=math.acos(UTILS.VecDot(a,b)/(UTILS.VecNorm(a)*UTILS.VecNorm(b))) + return math.deg(alpha) +end + +--- Rotate 3D vector in the 2D (x,z) plane. y-component (usually altitude) unchanged. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. +-- @param #number angle Rotation angle in degrees. +-- @return DCS#Vec3 Vector rotated in the (x,z) plane. +function UTILS.Rotate2D(a, angle) + + local phi=math.rad(angle) + + local x=a.z + local y=a.x + + local Z=x*math.cos(phi)-y*math.sin(phi) + local X=x*math.sin(phi)+y*math.cos(phi) + local Y=a.y + + local A={x=X, y=Y, z=Z} + + return A +end + + + +--- Converts a TACAN Channel/Mode couple into a frequency in Hz. +-- @param #number TACANChannel The TACAN channel, i.e. the 10 in "10X". +-- @param #string TACANMode The TACAN mode, i.e. the "X" in "10X". +-- @return #number Frequency in Hz or #nil if parameters are invalid. +function UTILS.TACANToFrequency(TACANChannel, TACANMode) + + if type(TACANChannel) ~= "number" then + return nil -- error in arguments + end + if TACANMode ~= "X" and TACANMode ~= "Y" then + return nil -- error in arguments + end + +-- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137. +-- I have no idea what it does but it seems to work + local A = 1151 -- 'X', channel >= 64 + local B = 64 -- channel >= 64 + + if TACANChannel < 64 then + B = 1 + end + + if TACANMode == 'Y' then + A = 1025 + if TACANChannel < 64 then + A = 1088 + end + else -- 'X' + if TACANChannel < 64 then + A = 962 + end + end + + return (A + TACANChannel - B) * 1000000 +end + + +--- Returns the DCS map/theatre as optained by env.mission.theatre +-- @return #string DCS map name . +function UTILS.GetDCSMap() + return env.mission.theatre +end + +--- Returns the magnetic declination of the map. +-- Returned values for the current maps are: +-- +-- * Caucasus +6 (East), year ~ 2011 +-- * NTTR +12 (East), year ~ 2011 +-- * Normandy -10 (West), year ~ 1944 +-- * Persian Gulf +2 (East), year ~ 2011 +-- @param #string map (Optional) Map for which the declination is returned. Default is from env.mission.theatre +-- @return #number Declination in degrees. +function UTILS.GetMagneticDeclination(map) + + -- Map. + map=map or UTILS.GetDCSMap() + + local declination=0 + if map==DCSMAP.Caucasus then + declination=6 + elseif map==DCSMAP.NTTR then + declination=12 + elseif map==DCSMAP.Normandy then + declination=-10 + elseif map==DCSMAP.PersianGulf then + declination=2 + else + declination=0 + end + + return declination +end + +--- Checks if a file exists or not. This requires **io** to be desanitized. +-- @param #string file File that should be checked. +-- @return #boolean True if the file exists, false if the file does not exist or nil if the io module is not available and the check could not be performed. +function UTILS.FileExists(file) + if io then + local f=io.open(file, "r") + if f~=nil then + io.close(f) + return true + else + return false + end + else + return nil + end +end diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index fe8a7824c..953ba2457 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -219,55 +219,54 @@ AIRBASE.Normandy = { --- These are all airbases of the Persion Gulf Map: -- --- * AIRBASE.PersianGulf.Fujairah_Intl --- * AIRBASE.PersianGulf.Qeshm_Island --- * AIRBASE.PersianGulf.Sir_Abu_Nuayr -- * AIRBASE.PersianGulf.Abu_Musa_Island_Airport -- * AIRBASE.PersianGulf.Bandar_Abbas_Intl -- * AIRBASE.PersianGulf.Bandar_Lengeh --- * AIRBASE.PersianGulf.Tunb_Island_AFB --- * AIRBASE.PersianGulf.Havadarya --- * AIRBASE.PersianGulf.Lar_Airbase --- * AIRBASE.PersianGulf.Sirri_Island --- * AIRBASE.PersianGulf.Tunb_Kochak -- * AIRBASE.PersianGulf.Al_Dhafra_AB -- * AIRBASE.PersianGulf.Dubai_Intl -- * AIRBASE.PersianGulf.Al_Maktoum_Intl +-- * AIRBASE.PersianGulf.Fujairah_Intl +-- * AIRBASE.PersianGulf.Tunb_Island_AFB +-- * AIRBASE.PersianGulf.Havadarya -- * AIRBASE.PersianGulf.Khasab +-- * AIRBASE.PersianGulf.Lar_Airbase -- * AIRBASE.PersianGulf.Al_Minhad_AB +-- * AIRBASE.PersianGulf.Qeshm_Island -- * AIRBASE.PersianGulf.Sharjah_Intl --- * AIRBASE.PersianGulf.Shiraz_International_Airport +-- * AIRBASE.PersianGulf.Sirri_Island +-- * AIRBASE.PersianGulf.Tunb_Kochak +-- * AIRBASE.PersianGulf.Sir_Abu_Nuayr -- * AIRBASE.PersianGulf.Kerman_Airport +-- * AIRBASE.PersianGulf.Shiraz_International_Airport -- * AIRBASE.PersianGulf.Sas_Al_Nakheel_Airport --- * AIRBASE.PersianGulf.Bandar_e_Jask_airfield +-- * AIRBASE.PersianGulf.Bandar-e-Jask_airfield -- * AIRBASE.PersianGulf.Abu_Dhabi_International_Airport --- * AIRBASE.PersianGulf.Al_Bateen_Airport +-- * AIRBASE.PersianGulf.Al-Bateen_Airport -- * AIRBASE.PersianGulf.Kish_International_Airport -- * AIRBASE.PersianGulf.Al_Ain_International_Airport -- * AIRBASE.PersianGulf.Lavan_Island_Airport -- * AIRBASE.PersianGulf.Jiroft_Airport --- -- @field PersianGulf AIRBASE.PersianGulf = { - ["Fujairah_Intl"] = "Fujairah Intl", - ["Qeshm_Island"] = "Qeshm Island", - ["Sir_Abu_Nuayr"] = "Sir Abu Nuayr", ["Abu_Musa_Island_Airport"] = "Abu Musa Island Airport", ["Bandar_Abbas_Intl"] = "Bandar Abbas Intl", ["Bandar_Lengeh"] = "Bandar Lengeh", + ["Al_Dhafra_AB"] = "Al Dhafra AB", + ["Dubai_Intl"] = "Dubai Intl", + ["Al_Maktoum_Intl"] = "Al Maktoum Intl", + ["Fujairah_Intl"] = "Fujairah Intl", ["Tunb_Island_AFB"] = "Tunb Island AFB", ["Havadarya"] = "Havadarya", + ["Khasab"] = "Khasab", ["Lar_Airbase"] = "Lar Airbase", + ["Al_Minhad_AB"] = "Al Minhad AB", + ["Qeshm_Island"] = "Qeshm Island", + ["Sharjah_Intl"] = "Sharjah Intl", ["Sirri_Island"] = "Sirri Island", ["Tunb_Kochak"] = "Tunb Kochak", - ["Al_Dhafra_AB"] = "Al Dhafra AB", - ["Dubai_Intl"] = "Dubai Intl", - ["Al_Maktoum_Intl"] = "Al Maktoum Intl", - ["Khasab"] = "Khasab", - ["Al_Minhad_AB"] = "Al Minhad AB", - ["Sharjah_Intl"] = "Sharjah Intl", - ["Shiraz_International_Airport"] = "Shiraz International Airport", + ["Sir_Abu_Nuayr"] = "Sir Abu Nuayr", ["Kerman_Airport"] = "Kerman Airport", + ["Shiraz_International_Airport"] = "Shiraz International Airport", ["Sas_Al_Nakheel_Airport"] = "Sas Al Nakheel Airport", ["Bandar_e_Jask_airfield"] = "Bandar-e-Jask airfield", ["Abu_Dhabi_International_Airport"] = "Abu Dhabi International Airport", @@ -649,31 +648,20 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, verysafe=false end - -- Get the size of an object. - local function _GetObjectSize(unit,mooseobject) - if mooseobject then - unit=unit:GetDCSObject() - end - if unit and unit:isExist() then - local DCSdesc=unit:getDesc() - if DCSdesc.box then - local x=DCSdesc.box.max.x+math.abs(DCSdesc.box.min.x) - local y=DCSdesc.box.max.y+math.abs(DCSdesc.box.min.y) --height - local z=DCSdesc.box.max.z+math.abs(DCSdesc.box.min.z) - return math.max(x,z), x , y, z - end - end - return 0,0,0,0 - end - -- Function calculating the overlap of two (square) objects. - local function _overlap(object1, mooseobject1, object2, mooseobject2, dist) - local l1=_GetObjectSize(object1, mooseobject1) - local l2=_GetObjectSize(object2, mooseobject2) - local safedist=(l1/2+l2/2)*1.1 - local safe = (dist > safedist) - self:T3(string.format("l1=%.1f l2=%.1f s=%.1f d=%.1f ==> safe=%s", l1,l2,safedist,dist,tostring(safe))) - return safe + local function _overlap(object1, object2, dist) + local pos1=object1 --Wrapper.Positionable#POSITIONABLE + local pos2=object2 --Wrapper.Positionable#POSITIONABLE + local r1=pos1:GetBoundingRadius() + local r2=pos2:GetBoundingRadius() + if r1 and r2 then + local safedist=(r1+r2)*1.1 + local safe = (dist > safedist) + self:T2(string.format("r1=%.1f r2=%.1f s=%.1f d=%.1f ==> safe=%s", r1, r2, safedist, dist, tostring(safe))) + return safe + else + return true + end end -- Get airport name. @@ -688,7 +676,7 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, -- Get the aircraft size, i.e. it's longest side of x,z. local aircraft=group:GetUnit(1) - local _aircraftsize, ax,ay,az=_GetObjectSize(aircraft, true) + local _aircraftsize, ax,ay,az=aircraft:GetObjectSize() -- Number of spots we are looking for. Note that, e.g. grouping can require a number different from the group size! local _nspots=nspots or group:GetSize() @@ -722,7 +710,7 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, if verysafe and (parkingspot.Free==false or parkingspot.TOAC==true) then -- DCS getParking() routine returned that spot is not free. - self:E(string.format("%s: Parking spot id %d NOT free (or aircraft has not taken off yet). Free=%s, TOAC=%s.", airport, parkingspot.TerminalID, tostring(parkingspot.Free), tostring(parkingspot.TOAC))) + self:T(string.format("%s: Parking spot id %d NOT free (or aircraft has not taken off yet). Free=%s, TOAC=%s.", airport, parkingspot.TerminalID, tostring(parkingspot.Free), tostring(parkingspot.TOAC))) else @@ -734,16 +722,13 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, -- Check all units. for _,unit in pairs(_units) do - -- Unis are now returned as MOOSE units not DCS units! - --local _vec3=unit:getPoint() - --local _coord=COORDINATE:NewFromVec3(_vec3) local _coord=unit:GetCoordinate() local _dist=_coord:Get2DDistance(_spot) - local _safe=_overlap(aircraft, true, unit, true,_dist) + local _safe=_overlap(aircraft, unit, _dist) if markobstacles then - local l,x,y,z=_GetObjectSize(unit) - _coord:MarkToAll(string.format("Unit %s\nx=%.1f y=%.1f z=%.1f\nl=%.1f d=%.1f\nspot %d safe=%s", unit:getName(),x,y,z,l,_dist, _termid, tostring(_safe))) + local l,x,y,z=unit:GetObjectSize() + _coord:MarkToAll(string.format("Unit %s\nx=%.1f y=%.1f z=%.1f\nl=%.1f d=%.1f\nspot %d safe=%s", unit:GetName(),x,y,z,l,_dist, _termid, tostring(_safe))) end if scanunits and not _safe then @@ -753,13 +738,14 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, -- Check all statics. for _,static in pairs(_statics) do + local _static=STATIC:Find(static) local _vec3=static:getPoint() local _coord=COORDINATE:NewFromVec3(_vec3) local _dist=_coord:Get2DDistance(_spot) - local _safe=_overlap(aircraft, true, static, false,_dist) + local _safe=_overlap(aircraft,_static,_dist) if markobstacles then - local l,x,y,z=_GetObjectSize(static) + local l,x,y,z=_static:GetObjectSize() _coord:MarkToAll(string.format("Static %s\nx=%.1f y=%.1f z=%.1f\nl=%.1f d=%.1f\nspot %d safe=%s", static:getName(),x,y,z,l,_dist, _termid, tostring(_safe))) end @@ -770,13 +756,14 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, -- Check all scenery. for _,scenery in pairs(_sceneries) do + local _scenery=SCENERY:Register(scenery:getTypeName(), scenery) local _vec3=scenery:getPoint() local _coord=COORDINATE:NewFromVec3(_vec3) local _dist=_coord:Get2DDistance(_spot) - local _safe=_overlap(aircraft, true, scenery, false,_dist) + local _safe=_overlap(aircraft,_scenery,_dist) if markobstacles then - local l,x,y,z=_GetObjectSize(scenery) + local l,x,y,z=scenery:GetObjectSize(scenery) _coord:MarkToAll(string.format("Scenery %s\nx=%.1f y=%.1f z=%.1f\nl=%.1f d=%.1f\nspot %d safe=%s", scenery:getTypeName(),x,y,z,l,_dist, _termid, tostring(_safe))) end @@ -788,7 +775,7 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, -- Now check the already given spots so that we do not put a large aircraft next to one we already assigned a nearby spot. for _,_takenspot in pairs(validspots) do local _dist=_takenspot.Coordinate:Get2DDistance(_spot) - local _safe=_overlap(aircraft, true, aircraft, true,_dist) + local _safe=_overlap(aircraft, aircraft, _dist) if not _safe then occupied=true end @@ -798,11 +785,12 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, if occupied then self:T(string.format("%s: Parking spot id %d occupied.", airport, _termid)) else - self:E(string.format("%s: Parking spot id %d free.", airport, _termid)) + self:I(string.format("%s: Parking spot id %d free.", airport, _termid)) if nvalid<_nspots then table.insert(validspots, {Coordinate=_spot, TerminalID=_termid}) end nvalid=nvalid+1 + self:I(string.format("%s: Parking spot id %d free. Nfree=%d/%d.", airport, _termid, nvalid,_nspots)) end end -- loop over units diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 1fb50baa5..f50040282 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -148,7 +148,7 @@ -- * @{#CONTROLLABLE.OptionROEReturnFirePossible} -- * @{#CONTROLLABLE.OptionROEEvadeFirePossible} -- --- ## 5.2) Rule on thread: +-- ## 5.2) Reaction On Thread: -- -- * @{#CONTROLLABLE.OptionROTNoReaction} -- * @{#CONTROLLABLE.OptionROTPassiveDefense} @@ -168,6 +168,11 @@ -- * @{#CONTROLLABLE.OptionAlarmStateGreen} -- * @{#CONTROLLABLE.OptionAlarmStateRed} -- +-- ## 5.4) Jettison weapons: +-- +-- * @{#CONTROLLABLE.OptionAllowJettisonWeaponsOnThreat} +-- * @{#CONTROLLABLE.OptionKeepWeaponsOnThreat} +-- -- @field #CONTROLLABLE CONTROLLABLE = { ClassName = "CONTROLLABLE", @@ -302,7 +307,7 @@ end -- @param #CONTROLLABLE self -- @return #CONTROLLABLE function CONTROLLABLE:ClearTasks() - self:F2() + self:E( "ClearTasks" ) local DCSControllable = self:GetDCSObject() @@ -342,16 +347,26 @@ function CONTROLLABLE:PushTask( DCSTask, WaitTime ) local DCSControllable = self:GetDCSObject() if DCSControllable then - local Controller = self:_GetController() + + local DCSControllableName = self:GetName() -- When a controllable SPAWNs, it takes about a second to get the controllable in the simulator. Setting tasks to unspawned controllables provides unexpected results. -- Therefore we schedule the functions to set the mission and options for the Controllable. - -- Controller:pushTask( DCSTask ) + -- Controller:pushTask( DCSTask ) + + local function PushTask( Controller, DCSTask ) + if self and self:IsAlive() then + local Controller = self:_GetController() + Controller:pushTask( DCSTask ) + else + BASE:E( { DCSControllableName .. " is not alive anymore.", DCSTask = DCSTask } ) + end + end - if WaitTime then - self.TaskScheduler:Schedule( Controller, Controller.pushTask, { DCSTask }, WaitTime ) + if not WaitTime or WaitTime == 0 then + PushTask( self, DCSTask ) else - Controller:pushTask( DCSTask ) + self.TaskScheduler:Schedule( self, PushTask, { DCSTask }, WaitTime ) end return self @@ -362,17 +377,19 @@ end --- Clearing the Task Queue and Setting the Task on the queue from the controllable. -- @param #CONTROLLABLE self --- @param #DCS.Task DCSTask DCS Task array. +-- @param DCS#Task DCSTask DCS Task array. -- @param #number WaitTime Time in seconds, before the task is set. -- @return Wrapper.Controllable#CONTROLLABLE self function CONTROLLABLE:SetTask( DCSTask, WaitTime ) - self:F2( { DCSTask = DCSTask } ) + self:F( { "SetTask", WaitTime, DCSTask = DCSTask } ) local DCSControllable = self:GetDCSObject() if DCSControllable then local DCSControllableName = self:GetName() + + self:T2( "Controllable Name = " .. DCSControllableName ) -- When a controllable SPAWNs, it takes about a second to get the controllable in the simulator. Setting tasks to unspawned controllables provides unexpected results. -- Therefore we schedule the functions to set the mission and options for the Controllable. @@ -383,7 +400,8 @@ function CONTROLLABLE:SetTask( DCSTask, WaitTime ) local Controller = self:_GetController() --self:I( "Before SetTask" ) Controller:setTask( DCSTask ) - --self:I( "After SetTask" ) + -- AI_FORMATION class (used by RESCUEHELO) calls SetTask twice per second! hence spamming the DCS log file ==> setting this to trace. + self:T( { ControllableName = self:GetName(), DCSTask = DCSTask } ) else BASE:E( { DCSControllableName .. " is not alive anymore.", DCSTask = DCSTask } ) end @@ -391,6 +409,8 @@ function CONTROLLABLE:SetTask( DCSTask, WaitTime ) if not WaitTime or WaitTime == 0 then SetTask( self, DCSTask ) + -- See above. + self:T( { ControllableName = self:GetName(), DCSTask = DCSTask } ) else self.TaskScheduler:Schedule( self, SetTask, { DCSTask }, WaitTime ) end @@ -540,9 +560,9 @@ end ---- Executes a command action +--- Executes a command action for the CONTROLLABLE. -- @param #CONTROLLABLE self --- @param DCS#Command DCSCommand +-- @param DCS#Command DCSCommand The command to be executed. -- @return #CONTROLLABLE self function CONTROLLABLE:SetCommand( DCSCommand ) self:F2( DCSCommand ) @@ -630,9 +650,143 @@ function CONTROLLABLE:StartUncontrolled(delay) return self end +--- Give the CONTROLLABLE the command to activate a beacon. See [DCS_command_activateBeacon](https://wiki.hoggitworld.com/view/DCS_command_activateBeacon) on Hoggit. +-- For specific beacons like TACAN use the more convenient @{#BEACON} class. +-- Note that a controllable can only have one beacon activated at a time with the execption of ICLS. +-- @param #CONTROLLABLE self +-- @param Core.Radio#BEACON.Type Type Beacon type (VOR, DME, TACAN, RSBN, ILS etc). +-- @param Core.Radio#BEACON.System System Beacon system (VOR, DME, TACAN, RSBN, ILS etc). +-- @param #number Frequency Frequency in Hz the beacon is running on. Use @{#UTILS.TACANToFrequency} to generate a frequency for TACAN beacons. +-- @param #number UnitID The ID of the unit the beacon is attached to. Usefull if more units are in one group. +-- @param #number Channel Channel the beacon is using. For, e.g. TACAN beacons. +-- @param #string ModeChannel The TACAN mode of the beacon, i.e. "X" or "Y". +-- @param #boolean AA If true, create and Air-Air beacon. IF nil, automatically set if CONTROLLABLE depending on whether unit is and aircraft or not. +-- @param #string Callsign Morse code identification callsign. +-- @param #boolean Bearing If true, beacon provides bearing information - if supported by the unit the beacon is attached to. +-- @param #number Delay (Optional) Delay in seconds before the beacon is activated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, ModeChannel, AA, Callsign, Bearing, Delay) + + AA=AA or self:IsAir() + UnitID=UnitID or self:GetID() + + -- Command + local CommandActivateBeacon= { + id = "ActivateBeacon", + params = { + ["type"] = Type, + ["system"] = System, + ["frequency"] = Frequency, + ["unitId"] = UnitID, + ["channel"] = Channel, + ["modeChannel"] = ModeChannel, + ["AA"] = AA, + ["callsign"] = Callsign, + ["bearing"] = Bearing, + } + } + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandActivateBeacon, {self, Type, System, Frequency, UnitID, Channel, ModeChannel, AA, Callsign, Bearing}, Delay) + else + self:SetCommand(CommandActivateBeacon) + end + + return self +end + +--- Activate ICLS system of the CONTROLLABLE. The controllable should be an aircraft carrier! +-- @param #CONTROLLABLE self +-- @param #number Channel ICLS channel. +-- @param #number UnitID The ID of the unit the ICLS system is attached to. Useful if more units are in one group. +-- @param #string Callsign Morse code identification callsign. +-- @param #number Delay (Optional) Delay in seconds before the ICLS is deactivated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandActivateICLS(Channel, UnitID, Callsign, Delay) + self:F() + + -- Command to activate ICLS system. + local CommandActivateICLS= { + id = "ActivateICLS", + params= { + ["type"] = BEACON.Type.ICLS, + ["channel"] = Channel, + ["unitId"] = UnitID, + ["callsign"] = Callsign, + } + } + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandActivateICLS, {self}, Delay) + else + self:SetCommand(CommandActivateICLS) + end + + return self +end + + +--- Deactivate the active beacon of the CONTROLLABLE. +-- @param #CONTROLLABLE self +-- @param #number Delay (Optional) Delay in seconds before the beacon is deactivated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandDeactivateBeacon(Delay) + self:F() + + -- Command to deactivate + local CommandDeactivateBeacon={id='DeactivateBeacon', params={}} + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandActivateBeacon, {self}, Delay) + else + self:SetCommand(CommandDeactivateBeacon) + end + + return self +end + +--- Deactivate the ICLS of the CONTROLLABLE. +-- @param #CONTROLLABLE self +-- @param #number Delay (Optional) Delay in seconds before the ICLS is deactivated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandDeactivateICLS(Delay) + self:F() + + -- Command to deactivate + local CommandDeactivateICLS={id='DeactivateICLS', params={}} + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandDeactivateICLS, {self}, Delay) + else + self:SetCommand(CommandDeactivateICLS) + end + + return self +end + +--- Set callsign of the CONTROLLABLE. See [DCS_command_setCallsign](https://wiki.hoggitworld.com/view/DCS_command_setCallsign) +-- @param #CONTROLLABLE self +-- @param DCS#CALLSIGN CallName Number corresponding the the callsign identifier you wish this group to be called. +-- @param #number CallNumber The number value the group will be referred to as. Only valid numbers are 1-9. For example Uzi **5**-1. Default 1. +-- @param #number Delay (Optional) Delay in seconds before the callsign is set. Default is immediately. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandSetCallsign(CallName, CallNumber, Delay) + self:F() + + -- Command to set the callsign. + local CommandSetCallsign={id='SetCallsign', params={callname=CallName, callnumber=CallNumber or 1}} + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandSetCallsign, {self, CallName, CallNumber}, Delay) + else + self:SetCommand(CommandSetCallsign) + end + + return self +end + + -- TASKS FOR AIR CONTROLLABLES - - --- (AIR) Attack a Controllable. -- @param #CONTROLLABLE self -- @param Wrapper.Controllable#CONTROLLABLE AttackGroup The Controllable to be attacked. @@ -870,11 +1024,43 @@ function CONTROLLABLE:TaskOrbitCircleAtVec2( Point, Altitude, Speed ) return DCSTask end +--- (AIR) Orbit at a position with at a given altitude and speed. Optionally, a race track pattern can be specified. +-- @param #CONTROLLABLE self +-- @param Core.Point#COORDINATE Coord Coordinate at which the CONTROLLABLE orbits. +-- @param #number Altitude Altitude in meters of the orbit pattern. +-- @param #number Speed Speed [m/s] flying the orbit pattern +-- @param Core.Point#COORDINATE CoordRaceTrack (Optional) If this coordinate is specified, the CONTROLLABLE will fly a race-track pattern using this and the initial coordinate. +-- @return #CONTROLLABLE self +function CONTROLLABLE:TaskOrbit(Coord, Altitude, Speed, CoordRaceTrack) + + local Pattern=AI.Task.OrbitPattern.CIRCLE + + local P1=Coord:GetVec2() + local P2=nil + if CoordRaceTrack then + Pattern=AI.Task.OrbitPattern.RACE_TRACK + P2=CoordRaceTrack:GetVec2() + end + + local Task = { + id = 'Orbit', + params = { + pattern = Pattern, + point = P1, + point2 = P2, + speed = Speed, + altitude = Altitude, + } + } + + return Task +end + --- (AIR) Orbit at the current position of the first unit of the controllable at a specified alititude. -- @param #CONTROLLABLE self -- @param #number Altitude The altitude [m] to hold the position. -- @param #number Speed The speed [m/s] flying when holding the position. --- @param Core.Point#COORDINATE Coordinate The coordinate where to orbit. +-- @param Core.Point#COORDINATE Coordinate (optional) The coordinate where to orbit. If the coordinate is not given, then the current position of the controllable is used. -- @return #CONTROLLABLE self function CONTROLLABLE:TaskOrbitCircle( Altitude, Speed, Coordinate ) self:F2( { self.ControllableName, Altitude, Speed } ) @@ -906,17 +1092,27 @@ end ---- (AIR) Delivering weapon on the runway. +--- (AIR) Delivering weapon on the runway. See [hoggit](https://wiki.hoggitworld.com/view/DCS_task_bombingRunway) +-- +-- Make sure the aircraft has the following role: +-- +-- * CAS +-- * Ground Attack +-- * Runway Attack +-- * Anti-Ship Strike +-- * AFAC +-- * Pinpoint Strike +-- -- @param #CONTROLLABLE self -- @param Wrapper.Airbase#AIRBASE Airbase Airbase to attack. --- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. --- @param DCS#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. See [DCS enum weapon flag](https://wiki.hoggitworld.com/view/DCS_enum_weapon_flag). Default 2147485694 = AnyBomb (GuidedBomb + AnyUnguidedBomb). +-- @param DCS#AI.Task.WeaponExpend WeaponExpend Enum AI.Task.WeaponExpend that defines how much munitions the AI will expend per attack run. Default "ALL". +-- @param #number AttackQty Number of times the group will attack if the target. Default 1. -- @param DCS#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- @param #boolean ControllableAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a controllable, not to a single aircraft. +-- @param #boolean GroupAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a group and not to a single aircraft. -- @return DCS#Task The DCS task structure. -function CONTROLLABLE:TaskBombingRunway( Airbase, WeaponType, WeaponExpend, AttackQty, Direction, ControllableAttack ) - self:F2( { self.ControllableName, Airbase, WeaponType, WeaponExpend, AttackQty, Direction, ControllableAttack } ) +function CONTROLLABLE:TaskBombingRunway(Airbase, WeaponType, WeaponExpend, AttackQty, Direction, GroupAttack) + self:F2( { self.ControllableName, Airbase, WeaponType, WeaponExpend, AttackQty, Direction, GroupAttack } ) -- BombingRunway = { -- id = 'BombingRunway', @@ -926,19 +1122,24 @@ function CONTROLLABLE:TaskBombingRunway( Airbase, WeaponType, WeaponExpend, Atta -- expend = enum AI.Task.WeaponExpend, -- attackQty = number, -- direction = Azimuth, --- controllableAttack = boolean, +-- groupAttack = boolean, -- } --- } +-- } + + -- Defaults. + WeaponType=WeaponType or 2147485694 + WeaponExpend=WeaponExpend or AI.Task.WeaponExpend.ALL + AttackQty=AttackQty or 1 local DCSTask DCSTask = { id = 'BombingRunway', params = { - point = Airbase:GetID(), + runwayId = Airbase:GetID(), weaponType = WeaponType, expend = WeaponExpend, attackQty = AttackQty, direction = Direction, - controllableAttack = ControllableAttack, + groupAttack = GroupAttack, }, }, @@ -958,11 +1159,7 @@ function CONTROLLABLE:TaskRefueling() -- params = {} -- } - local DCSTask - DCSTask = { id = 'Refueling', - params = { - }, - }, + local DCSTask={id='Refueling', params={}} self:T3( { DCSTask } ) return DCSTask @@ -1659,6 +1856,7 @@ function CONTROLLABLE:TaskFunction( FunctionString, ... ) local DCSScript = {} DCSScript[#DCSScript+1] = "local MissionControllable = GROUP:Find( ... ) " + DCSScript[#DCSScript+1] = "env.info( 'TaskFunction: ' .. ( MissionControllable and MissionControllable:GetName() ) or 'No Group' )" if arg and arg.n > 0 then local ArgumentKey = '_' .. tostring( arg ):match("table: (.*)") @@ -2101,7 +2299,7 @@ do -- Route methods FromCoordinate = FromCoordinate or self:GetCoordinate() -- Get path and path length on road including the end points (From and To). - local PathOnRoad, LengthOnRoad=FromCoordinate:GetPathOnRoad(ToCoordinate, true) + local PathOnRoad, LengthOnRoad, GotPath =FromCoordinate:GetPathOnRoad(ToCoordinate, true) -- Get the length only(!) on the road. local _,LengthRoad=FromCoordinate:GetPathOnRoad(ToCoordinate, false) @@ -2113,7 +2311,7 @@ do -- Route methods -- Calculate the direct distance between the initial and final points. local LengthDirect=FromCoordinate:Get2DDistance(ToCoordinate) - if PathOnRoad then + if GotPath then -- Off road part of the rout: Total=OffRoad+OnRoad. LengthOffRoad=LengthOnRoad-LengthRoad @@ -2136,7 +2334,7 @@ do -- Route methods local canroad=false -- Check if a valid path on road could be found. - if PathOnRoad and LengthDirect > 2000 then -- if the length of the movement is less than 1 km, drive directly. + if GotPath and LengthDirect > 2000 then -- if the length of the movement is less than 1 km, drive directly. -- Check whether the road is very long compared to direct path. if LongRoad and Shortcut then @@ -2922,7 +3120,7 @@ function CONTROLLABLE:OptionRTBAmmo( WeaponsFlag ) local Controller = self:_GetController() if self:IsAir() then - Controller:setOption( AI.Option.GROUND.id.RTB_ON_OUT_OF_AMMO, WeaponsFlag ) + Controller:setOption( AI.Option.Air.id.RTB_ON_OUT_OF_AMMO, WeaponsFlag ) end return self @@ -2932,6 +3130,47 @@ function CONTROLLABLE:OptionRTBAmmo( WeaponsFlag ) end +--- Allow to Jettison of weapons upon threat. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionAllowJettisonWeaponsOnThreat() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + + if self:IsAir() then + Controller:setOption( AI.Option.Air.id.PROHIBIT_JETT, false ) + end + + return self + end + + return nil +end + + +--- Keep weapons upon threat. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionKeepWeaponsOnThreat() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + + if self:IsAir() then + Controller:setOption( AI.Option.Air.id.PROHIBIT_JETT, true ) + end + + return self + end + + return nil +end + @@ -3024,6 +3263,3 @@ function CONTROLLABLE:IsAirPlane() return nil end - - --- Message APIs \ No newline at end of file diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index e6c6648fd..8525f0e83 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -325,7 +325,7 @@ end -- 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 true if you want to generate a crash or dead event for each unit. +-- @param #boolean GenerateEvent If true, a crash or dead event for each unit is generated. If false, if no event is triggered. If nil, a RemoveUnit event is triggered. -- @usage -- -- Air unit example: destroy the Helicopter and generate a S_EVENT_CRASH for each unit in the Helicopter group. -- Helicopter = GROUP:FindByName( "Helicopter" ) @@ -934,7 +934,7 @@ end -- @return #number The fuel state of the unit with the least amount of fuel -- @return #Unit reference to #Unit object for further processing function GROUP:GetFuelMin() - self:F(self.ControllableName) + self:F3(self.ControllableName) if not self:GetDCSObject() then BASE:E( { "Cannot GetFuel", Group = self, Alive = self:IsAlive() } ) @@ -1455,6 +1455,57 @@ function GROUP:InitRandomizePositionRadius( OuterRadius, InnerRadius ) 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. @@ -1477,29 +1528,61 @@ end -- -- @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 ) - if not Template then - Template = self:GetTemplate() - end + -- 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() ) + self:F(GroupUnit:GetName()) + if GroupUnit:IsAlive() then - self:F( "Alive" ) - local GroupUnitVec3 = GroupUnit:GetVec3() + self:F("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() @@ -1512,17 +1595,38 @@ function GROUP:Respawn( Template, Reset ) end end + -- Altitude Template.units[UnitID].alt = self.InitRespawnHeight and self.InitRespawnHeight or GroupUnitVec3.y - 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. - Template.units[UnitID].heading = self.InitRespawnHeading and self.InitRespawnHeading or GroupUnit:GetHeading() + + -- 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 - else + + else -- 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() @@ -1535,23 +1639,54 @@ function GROUP:Respawn( Template, Reset ) end 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 + end end - self:Destroy() - _DATABASE:Spawn( Template ) + -- 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 @@ -1648,11 +1783,23 @@ function GROUP:RespawnAtCurrentAirbase(SpawnTemplate, Takeoff, Uncontrolled) -- -- 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) - _DATABASE:Spawn( SpawnTemplate ) + -- Spawn new group. + _DATABASE:Spawn(SpawnTemplate) -- Reset events. self:ResetEvents() @@ -1800,8 +1947,8 @@ do -- Route methods -- -- @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, the maximum Speed of the first unit is selected. - -- @return #GROUP + -- @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 } ) @@ -1811,42 +1958,42 @@ do -- Route methods if RTBAirbase then - local GroupPoint = self:GetVec2() - local GroupVelocity = self:GetUnit(1):GetDesc().speedMax - - local PointFrom = {} - PointFrom.x = GroupPoint.x - PointFrom.y = GroupPoint.y - PointFrom.type = "Turning Point" - PointFrom.action = "Turning Point" - PointFrom.speed = GroupVelocity - - - local PointTo = {} - local AirbasePointVec2 = RTBAirbase:GetPointVec2() - local AirbaseAirPoint = AirbasePointVec2:WaypointAir( - POINT_VEC3.RoutePointAltType.BARO, - "Land", - "Landing", - Speed or self:GetUnit(1):GetDesc().speedMax - ) + -- If speed is not given take 80% of max speed. + local Speed=Speed or self:GetSpeedMax()*0.8 - AirbaseAirPoint["airdromeId"] = RTBAirbase:GetID() - AirbaseAirPoint["speed_locked"] = true, + -- 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} - self:F(AirbaseAirPoint ) - - local Points = { PointFrom, AirbaseAirPoint } - - self:T3( Points ) + -- Debug info. + self:T3(Points) - local Template = self:GetTemplate() - Template.route.points = Points - self:Respawn( Template ) - - --self:Route( 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 diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index fef546f38..70d7a3c15 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -4,7 +4,7 @@ -- -- ### Author: **FlightControl** -- --- ### Contributions: +-- ### Contributions: **Hardcard**, **funkyfranky** -- -- === -- @@ -310,6 +310,44 @@ function POSITIONABLE:GetCoordinate() return nil end +--- Returns a COORDINATE object, which is offset with respect to the orientation of the POSITIONABLE. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @param #number x Offset in the direction "the nose" of the unit is pointing in meters. Default 0 m. +-- @param #number y Offset "above" the unit in meters. Default 0 m. +-- @param #number z Offset in the direction "the wing" of the unit is pointing in meters. z>0 starboard, z<0 port. Default 0 m. +-- @return Core.Point#COORDINATE The COORDINATE of the offset with respect to the orientation of the POSITIONABLE. +function POSITIONABLE:GetOffsetCoordinate(x,y,z) + + -- Default if nil. + x=x or 0 + y=y or 0 + z=z or 0 + + -- Vectors making up the coordinate system. + local X=self:GetOrientationX() + local Y=self:GetOrientationY() + local Z=self:GetOrientationZ() + + -- Offset vector: x meters ahead, z meters starboard, y meters above. + local A={x=x, y=y, z=z} + + -- Scale components of orthonormal coordinate vectors. + local x={x=X.x*A.x, y=X.y*A.x, z=X.z*A.x} + local y={x=Y.x*A.y, y=Y.y*A.y, z=Y.z*A.y} + local z={x=Z.x*A.z, y=Z.y*A.z, z=Z.z*A.z} + + -- Add up vectors in the unit coordinate system ==> this gives the offset vector relative the the origin of the map. + local a={x=x.x+y.x+z.x, y=x.y+y.y+z.y, z=x.z+y.z+z.z} + + -- Vector from the origin of the map to the unit. + local u=self:GetVec3() + + -- Translate offset vector from map origin to the unit: v=u+a. + local v={x=a.x+u.x, y=a.y+u.y, z=a.z+u.z} + + -- Return the offset coordinate. + return COORDINATE:NewFromVec3(v) +end --- Returns a random @{DCS#Vec3} vector within a range, indicating the point in 3D of the POSITIONABLE within the mission. -- @param Wrapper.Positionable#POSITIONABLE self @@ -390,6 +428,27 @@ function POSITIONABLE:GetBoundingBox() --R2.1 end +--- Get the object size. +-- @param #POSITIONABLE self +-- @return DCS#Distance Max size of object in x, z or 0 if bounding box could not be obtained. +-- @return DCS#Distance Length x or 0 if bounding box could not be obtained. +-- @return DCS#Distance Height y or 0 if bounding box could not be obtained. +-- @return DCS#Distance Width z or 0 if bounding box could not be obtained. +function POSITIONABLE:GetObjectSize() + + -- Get bounding box. + local box=self:GetBoundingBox() + + if box then + local x=box.max.x+math.abs(box.min.x) --length + local y=box.max.y+math.abs(box.min.y) --height + local z=box.max.z+math.abs(box.min.z) --width + return math.max(x,z), x , y, z + end + + return 0,0,0,0 +end + --- Get the bounding radius of the underlying POSITIONABLE DCS Object. -- @param #POSITIONABLE self -- @param #number mindist (Optional) If bounding box is smaller than this value, mindist is returned. @@ -656,6 +715,14 @@ function POSITIONABLE:GetVelocityMPS() return 0 end +--- Returns the POSITIONABLE velocity in knots. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #number The velocity in knots. +function POSITIONABLE:GetVelocityKNOTS() + self:F2( self.PositionableName ) + return UTILS.MpsToKnots(self:GetVelocityMPS()) +end + --- Returns the Angle of Attack of a positionable. -- @param Wrapper.Positionable#POSITIONABLE self -- @return #number Angle of attack in degrees. @@ -706,8 +773,8 @@ end --- Returns the unit's climb or descent angle. -- @param Wrapper.Positionable#POSITIONABLE self --- @return #number Climb or descent angle in degrees. -function POSITIONABLE:GetClimbAnge() +-- @return #number Climb or descent angle in degrees. Or 0 if velocity vector norm is zero (or nil). Or nil, if the position of the POSITIONABLE returns nil. +function POSITIONABLE:GetClimbAngle() -- Get position of the unit. local unitpos = self:GetPosition() @@ -719,10 +786,17 @@ function POSITIONABLE:GetClimbAnge() if unitvel and UTILS.VecNorm(unitvel)~=0 then - return math.asin(unitvel.y/UTILS.VecNorm(unitvel)) - + -- Calculate climb angle. + local angle=math.asin(unitvel.y/UTILS.VecNorm(unitvel)) + + -- Return angle in degrees. + return math.deg(angle) + else + return 0 end end + + return nil end --- Returns the pitch angle of a unit. diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index d24a0b0b0..4bbcaac95 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -555,7 +555,7 @@ end -- @return #number The relative amount of fuel (from 0.0 to 1.0). -- @return #nil The DCS Unit is not existing or alive. function UNIT:GetFuel() - self:F( self.UnitName ) + self:F3( self.UnitName ) local DCSUnit = self:GetDCSObject() @@ -571,7 +571,7 @@ end -- @param #UNIT self -- @return #list A list of one @{Wrapper.Unit}. function UNIT:GetUnits() - self:F2( { self.UnitName } ) + self:F3( { self.UnitName } ) local DCSUnit = self:GetDCSObject() local Units = {} @@ -783,6 +783,27 @@ function UNIT:GetThreatLevel() end +--- Triggers an explosion at the coordinates of the unit. +-- @param #UNIT self +-- @param #number power Power of the explosion in kg TNT. Default 100 kg TNT. +-- @param #number delay (Optional) Delay of explosion in seconds. +-- @return #UNIT self +function UNIT:Explode(power, delay) + + -- Default. + power=power or 100 + + -- Check if delay or not. + if delay and delay>0 then + -- Delayed call. + SCHEDULER:New(nil, self.Explode, {self, power}, delay) + else + -- Create an explotion at the coordinate of the unit. + self:GetCoordinate():Explosion(power) + end + + return self +end -- Is functions @@ -902,29 +923,31 @@ end function UNIT:InAir() self:F2( self.UnitName ) + -- Get DCS unit object. local DCSUnit = self:GetDCSObject() --DCS#Unit if DCSUnit then --- Implementation of workaround. The original code is below. --- This to simulate the landing on buildings. - - local UnitInAir = true + -- Get DCS result of whether unit is in air or not. + local UnitInAir = DCSUnit:inAir() + + -- Get unit category. local UnitCategory = DCSUnit:getDesc().category - if UnitCategory == Unit.Category.HELICOPTER then + + -- If DCS says that it is in air, check if this is really the case, since we might have landed on a building where inAir()=true but actually is not. + -- This is a workaround since DCS currently does not acknoledge that helos land on buildings. + -- Note however, that the velocity check will fail if the ground is moving, e.g. on an aircraft carrier! + if UnitInAir==true and UnitCategory == Unit.Category.HELICOPTER then local VelocityVec3 = DCSUnit:getVelocity() - local Velocity = ( VelocityVec3.x ^ 2 + VelocityVec3.y ^ 2 + VelocityVec3.z ^ 2 ) ^ 0.5 -- in meters / sec + local Velocity = UTILS.VecNorm(VelocityVec3) local Coordinate = DCSUnit:getPoint() local LandHeight = land.getHeight( { x = Coordinate.x, y = Coordinate.z } ) local Height = Coordinate.y - LandHeight if Velocity < 1 and Height <= 60 then UnitInAir = false end - else - UnitInAir = DCSUnit:inAir() end - - + self:T3( UnitInAir ) return UnitInAir end diff --git a/Moose Setup/Eclipse/Moose Loader Dynamic.launch b/Moose Setup/Eclipse/Moose Loader Dynamic.launch new file mode 100644 index 000000000..5efaf2485 --- /dev/null +++ b/Moose Setup/Eclipse/Moose Loader Dynamic.launch @@ -0,0 +1,6 @@ + + + + + + diff --git a/Moose Setup/Eclipse/Moose Loader Static.launch b/Moose Setup/Eclipse/Moose Loader Static.launch new file mode 100644 index 000000000..4ee25e04a --- /dev/null +++ b/Moose Setup/Eclipse/Moose Loader Static.launch @@ -0,0 +1,6 @@ + + + + + + diff --git a/Moose Setup/Moose Templates/Moose_Dynamic_Loader.lua b/Moose Setup/Moose Templates/Moose_Dynamic_Loader.lua index 24ba37689..d63555297 100644 --- a/Moose Setup/Moose Templates/Moose_Dynamic_Loader.lua +++ b/Moose Setup/Moose Templates/Moose_Dynamic_Loader.lua @@ -18,3 +18,5 @@ __Moose.Include = function( IncludeFile ) end __Moose.Includes = {} + +__Moose.Include( 'Scripts/Moose/Modules.lua' ) diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index 9ef0e3f57..b1da22f5c 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -59,12 +59,24 @@ Functional/Suppression.lua Functional/PseudoATC.lua Functional/Warehouse.lua +Ops/Airboss.lua +Ops/RecoveryTanker.lua +Ops/RescueHelo.lua + AI/AI_Balancer.lua +AI/AI_Air.lua AI/AI_A2A.lua AI/AI_A2A_Patrol.lua AI/AI_A2A_Cap.lua AI/AI_A2A_Gci.lua AI/AI_A2A_Dispatcher.lua +AI/AI_A2G.lua +AI/AI_A2G_Engage.lua +AI/AI_A2G_BAI.lua +AI/AI_A2G_CAS.lua +AI/AI_A2G_SEAD.lua +AI/AI_A2G_Patrol.lua +AI/AI_A2G_Dispatcher.lua AI/AI_Patrol.lua AI/AI_Cap.lua AI/AI_Cas.lua @@ -100,4 +112,4 @@ Tasking/Task_Cargo_CSAR.lua Tasking/Task_Cargo_Dispatcher.lua Tasking/TaskZoneCapture.lua -Moose.lua +Globals.lua diff --git a/Moose Setup/Moose_Create.lua b/Moose Setup/Moose_Create.lua index 500a56965..42a3213aa 100644 --- a/Moose Setup/Moose_Create.lua +++ b/Moose Setup/Moose_Create.lua @@ -12,15 +12,15 @@ print( "Moose development path : " .. MooseDevelopmentPath ) print( "Moose setup path : " .. MooseSetupPath ) print( "Moose target path : " .. MooseTargetPath ) -local MooseSourcesFilePath = MooseSetupPath .. "/Moose.files" -local MooseFilePath = MooseTargetPath.."/Moose.lua" +local MooseModulesFilePath = MooseDevelopmentPath .. "/Modules.lua" +local LoaderFilePath = MooseTargetPath .. "/Moose.lua" -print( "Reading Moose source list : " .. MooseSourcesFilePath ) +print( "Reading Moose source list : " .. MooseModulesFilePath ) -local MooseFile = io.open( MooseFilePath, "w" ) +local LoaderFile = io.open( LoaderFilePath, "w" ) if MooseDynamicStatic == "S" then - MooseFile:write( "env.info( '*** MOOSE GITHUB Commit Hash ID: " .. MooseCommitHash .. " ***' )\n" ) + LoaderFile:write( "env.info( '*** MOOSE GITHUB Commit Hash ID: " .. MooseCommitHash .. " ***' )\n" ) end local MooseLoaderPath @@ -35,27 +35,26 @@ local MooseLoader = io.open( MooseLoaderPath, "r" ) local MooseLoaderText = MooseLoader:read( "*a" ) MooseLoader:close() -MooseFile:write( MooseLoaderText ) +LoaderFile:write( MooseLoaderText ) - -local MooseSourcesFile = io.open( MooseSourcesFilePath, "r" ) +local MooseSourcesFile = io.open( MooseModulesFilePath, "r" ) local MooseSource = MooseSourcesFile:read("*l") while( MooseSource ) do if MooseSource ~= "" then + MooseSource = string.match( MooseSource, "Scripts/Moose/(.+)'" ) local MooseFilePath = MooseDevelopmentPath .. "/" .. MooseSource if MooseDynamicStatic == "D" then - print( "Load dynamic: " .. MooseSource ) - MooseFile:write( "__Moose.Include( 'Scripts/Moose/" .. MooseSource .. "' )\n" ) + print( "Load dynamic: " .. MooseFilePath ) end if MooseDynamicStatic == "S" then - print( "Load static: " .. MooseSource ) + print( "Load static: " .. MooseFilePath ) local MooseSourceFile = io.open( MooseFilePath, "r" ) local MooseSourceFileText = MooseSourceFile:read( "*a" ) MooseSourceFile:close() - MooseFile:write( MooseSourceFileText ) + LoaderFile:write( MooseSourceFileText ) end end @@ -63,13 +62,13 @@ while( MooseSource ) do end if MooseDynamicStatic == "D" then - MooseFile:write( "BASE:TraceOnOff( true )\n" ) + LoaderFile:write( "BASE:TraceOnOff( true )\n" ) end if MooseDynamicStatic == "S" then - MooseFile:write( "BASE:TraceOnOff( false )\n" ) + LoaderFile:write( "BASE:TraceOnOff( false )\n" ) end -MooseFile:write( "env.info( '*** MOOSE INCLUDE END *** ' )\n" ) +LoaderFile:write( "env.info( '*** MOOSE INCLUDE END *** ' )\n" ) MooseSourcesFile:close() -MooseFile:close() +LoaderFile:close()