diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index 393413c45..044248fa2 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -308,46 +308,7 @@ FLIGHTGROUP.Attribute = { -- @field #number damage Damage of element in percent. -- @field Wrapper.Airbase#AIRBASE.ParkingSpot parking The parking spot table the element is parking on. ---- Flight group task status. --- @type FLIGHTGROUP.TaskStatus --- @field #string SCHEDULED Task is scheduled. --- @field #string EXECUTING Task is being executed. --- @field #string PAUSED Task is paused. --- @field #string DONE Task is done. -FLIGHTGROUP.TaskStatus={ - SCHEDULED="scheduled", - EXECUTING="executing", - PAUSED="paused", - DONE="done", -} ---- Flight group task status. --- @type FLIGHTGROUP.TaskType --- @field #string SCHEDULED Task is scheduled and will be executed at a given time. --- @field #string WAYPOINT Task is executed at a specific waypoint. -FLIGHTGROUP.TaskType={ - SCHEDULED="scheduled", - WAYPOINT="waypoint", -} - ---- Flight group task structure. --- @type FLIGHTGROUP.Task --- @field #string type Type of task: either SCHEDULED or WAYPOINT. --- @field #number id Task ID. Running number to get the task. --- @field #number prio Priority. --- @field #number time Abs. mission time when to execute the task. --- @field #table dcstask DCS task structure. --- @field #string description Brief text which describes the task. --- @field #string status Task status. --- @field #number duration Duration before task is cancelled in seconds. Default never. --- @field #number timestamp Abs. mission time, when task was started. --- @field #number waypoint Waypoint index if task is a waypoint task. --- @field Core.UserFlag#USERFLAG stopflag If flag is set to 1 (=true), the task is stopped. - ---- Enroute task. --- @type FLIGHTGROUP.EnrouteTask --- @field DCS#Task DCStask DCS task structure table. --- @field #number WaypointIndex Waypoint number at which the enroute task is added. --- Ammo data. -- @type FLIGHTGROUP.Ammo @@ -576,134 +537,8 @@ end -- User functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Add a *scheduled* task. --- @param #FLIGHTGROUP self --- @param #table task DCS task table structure. --- @param #string clock Mission time when task is executed. Default in 5 seconds. If argument passed as #number, it defines a relative delay in seconds. --- @param #string description Brief text describing the task, e.g. "Attack SAM". --- @param #number prio Priority of the task. --- @param #number duration Duration before task is cancelled in seconds counted after task started. Default never. --- @return #FLIGHTGROUP.Task The task structure. -function FLIGHTGROUP:AddTask(task, clock, description, prio, duration) - - local newtask=self:NewTaskScheduled(task, clock, description, prio, duration) - - -- Add to table. - table.insert(self.taskqueue, newtask) - - -- Info. - self:I(self.lid..string.format("Adding SCHEDULED task %s starting at %s", newtask.description, UTILS.SecondsToClock(newtask.time, true))) - self:T3({newtask=newtask}) - - return newtask -end - ---- Create a *scheduled* task. --- @param #FLIGHTGROUP self --- @param #table task DCS task table structure. --- @param #string clock Mission time when task is executed. Default in 5 seconds. If argument passed as #number, it defines a relative delay in seconds. --- @param #string description Brief text describing the task, e.g. "Attack SAM". --- @param #number prio Priority of the task. --- @param #number duration Duration before task is cancelled in seconds counted after task started. Default never. --- @return #FLIGHTGROUP.Task The task structure. -function FLIGHTGROUP:NewTaskScheduled(task, clock, description, prio, duration) - - -- Increase counter. - self.taskcounter=self.taskcounter+1 - - -- Set time. - local time=timer.getAbsTime()+5 - if clock then - if type(clock)=="string" then - time=UTILS.ClockToSeconds(clock) - elseif type(clock)=="number" then - time=timer.getAbsTime()+clock - end - end - - -- Task data structure. - local newtask={} --#FLIGHTGROUP.Task - newtask.status=FLIGHTGROUP.TaskStatus.SCHEDULED - newtask.dcstask=task - newtask.description=description or task.id - newtask.prio=prio or 50 - newtask.time=time - newtask.id=self.taskcounter - newtask.duration=duration - newtask.waypoint=-1 - newtask.type=FLIGHTGROUP.TaskType.SCHEDULED - newtask.stopflag=USERFLAG:New(string.format("%s StopTaskFlag %d", self.groupname, newtask.id)) - newtask.stopflag:Set(0) - - return newtask -end - ---- Add a *waypoint* task. --- @param #FLIGHTGROUP self --- @param #table task DCS task table structure. --- @param #number waypointindex Number of waypoint. Counting starts at one! Default is the as *next* waypoint. --- @param #string description Brief text describing the task, e.g. "Attack SAM". --- @param #number prio Priority of the task. Number between 1 and 100. Default is 50. --- @param #number duration Duration before task is cancelled in seconds counted after task started. Default never. --- @return #FLIGHTGROUP.Task The task structure. -function FLIGHTGROUP:AddTaskWaypoint(task, waypointindex, description, prio, duration) - - -- Increase counter. - self.taskcounter=self.taskcounter+1 - - -- Task data structure. - local newtask={} --#FLIGHTGROUP.Task - newtask.description=description - newtask.status=FLIGHTGROUP.TaskStatus.SCHEDULED - newtask.dcstask=task - newtask.prio=prio or 50 - newtask.id=self.taskcounter - newtask.duration=duration - newtask.time=0 - newtask.waypoint=waypointindex or (self.currentwp and self.currentwp+1 or 2) - newtask.type=FLIGHTGROUP.TaskType.WAYPOINT - newtask.stopflag=USERFLAG:New(string.format("%s StopTaskFlag %d", self.groupname, newtask.id)) - newtask.stopflag:Set(0) - - -- Add to table. - table.insert(self.taskqueue, newtask) - - -- Info. - self:I(self.lid..string.format("Adding WAYPOINT task %s at WP %d", newtask.description, newtask.waypoint)) - self:T3({newtask=newtask}) - - -- Update route. - --self:_CheckFlightDone(1) - self:__UpdateRoute(-1) - - return newtask -end - ---- Add an *enroute* task. --- @param #FLIGHTGROUP self --- @param #table task DCS task table structure. -function FLIGHTGROUP:AddTaskEnroute(task) - if not self.taskenroute then - self.taskenroute={} - end - - -- Check not to add the same task twice! - local gotit=false - for _,Task in pairs(self.taskenroute) do - if Task.id==task.id then - gotit=true - break - end - end - - if not gotit then - table.insert(self.taskenroute, task) - end - - -end --- Add an *enroute* task to attack targets in a certain **circular** zone. -- @param #FLIGHTGROUP self @@ -716,87 +551,9 @@ function FLIGHTGROUP:AddTaskEnrouteEngageTargetsInZone(ZoneRadius, TargetTypes, end ---- Remove task from task queue. --- @param #FLIGHTGROUP self --- @param #FLIGHTGROUP.Task Task The task to be removed from the queue. --- @return #boolean True if task could be removed. -function FLIGHTGROUP:RemoveTask(Task) - for i=#self.taskqueue,1,-1 do - local task=self.taskqueue[i] --#FLIGHTGROUP.Task - - if task.id==Task.id then - - -- Remove task from queue. - table.remove(self.taskqueue, i) - - -- Update route if this is a waypoint task. - if task.type==FLIGHTGROUP.TaskType.WAYPOINT and task.status==FLIGHTGROUP.TaskStatus.SCHEDULED then - self:_CheckFlightDone(1) - --self:__UpdateRoute(-1) - end - - return true - end - end - - return false -end ---- Add mission to queue. --- @param #FLIGHTGROUP self --- @param Ops.Auftrag#AUFTRAG Mission Mission for this group. --- @return #FLIGHTGROUP self -function FLIGHTGROUP:AddMission(Mission) - - -- Add flight group to mission. - Mission:AddFlightGroup(self) - - -- Set flight status to SCHEDULED.. - Mission:SetFlightStatus(self, AUFTRAG.FlightStatus.SCHEDULED) - - -- Set mission status to SCHEDULED. - Mission:Scheduled() - -- Add mission to queue. - table.insert(self.missionqueue, Mission) - - -- Info text. - local text=string.format("Added %s mission %s starting at %s, stopping at %s", - tostring(Mission.type), tostring(Mission.name), UTILS.SecondsToClock(Mission.Tstart, true), Mission.Tstop and UTILS.SecondsToClock(Mission.Tstop, true) or "INF") - self:I(self.lid..text) - - return self -end - ---- Remove mission from queue. --- @param #FLIGHTGROUP self --- @param Ops.Auftrag#AUFTRAG Mission Mission to be removed. --- @return #FLIGHTGROUP self -function FLIGHTGROUP:RemoveMission(Mission) - - for i,_mission in pairs(self.missionqueue) do - local mission=_mission --Ops.Auftrag#AUFTRAG - - if mission.auftragsnummer==Mission.auftragsnummer then - - -- Remove mission waypoint task. - local Task=Mission:GetFlightWaypointTask(self) - - if Task then - self:RemoveTask(Task) - end - - -- Remove mission from queue. - table.remove(self.missionqueue, i) - - return self - end - - end - - return self -end --- Set AIRWING the flight group belongs to. -- @param #FLIGHTGROUP self @@ -815,16 +572,6 @@ function FLIGHTGROUP:GetAirWing() return self.airwing end ---- Set detection on or off. --- @param #FLIGHTGROUP self --- @param #boolean Switch If true, detection is on. If false or nil, detection is off. Default is off. --- @return #FLIGHTGROUP self -function FLIGHTGROUP:SetDetection(Switch) - self.detectionOn=Switch - return self -end - - --- Set the FLIGHTCONTROL controlling this flight group. -- @param #FLIGHTGROUP self -- @param Ops.FlightControl#FLIGHTCONTROL flightcontrol The FLIGHTCONTROL object. @@ -920,89 +667,7 @@ function FLIGHTGROUP:SetFuelCriticalThreshold(threshold, rtb) return self end ---- Define a SET of zones that trigger and event if the group enters or leaves any of the zones. --- @param #FLIGHTGROUP self --- @param Core.Set#SET_ZONE CheckZonesSet Set of zones. --- @return #FLIGHTGROUP self -function FLIGHTGROUP:SetCheckZones(CheckZonesSet) - self.checkzones=CheckZonesSet - return self -end ---- Add a zone that triggers and event if the group enters or leaves any of the zones. --- @param #FLIGHTGROUP self --- @param Core.Zone#ZONE CheckZone Zone to check. --- @return #FLIGHTGROUP self -function FLIGHTGROUP:AddCheckZone(CheckZone) - if not self.checkzones then - self.checkzones=SET_ZONE:New() - end - self.checkzones:AddZone(CheckZone) - return self -end - ---- Get set of detected units. --- @param #FLIGHTGROUP self --- @return Core.Set#SET_UNIT Set of detected units. -function FLIGHTGROUP:GetDetectedUnits() - return self.detectedunits -end - ---- Get MOOSE group object. --- @param #FLIGHTGROUP self --- @return Wrapper.Group#GROUP Moose group object. -function FLIGHTGROUP:GetGroup() - return self.group -end - ---- Get flight group name. --- @param #FLIGHTGROUP self --- @return #string Group name. -function FLIGHTGROUP:GetName() - return self.groupname -end - ---- Get current coordinate of the group. --- @param #FLIGHTGROUP self --- @return Core.Point#COORDINATE The coordinate (of the first unit) of the group. -function FLIGHTGROUP:GetCoordinate() - if self:IsAlive()~=nil then - return self.group:GetCoordinate() - else - self:E(self.lid.."WARNING: Group is not alive. Cannot get coordinate!") - end - return nil -end - ---- Get waypoint. --- @param #FLIGHTGROUP self --- @param #number indx Waypoint index. --- @return #table Waypoint table. -function FLIGHTGROUP:GetWaypoint(indx) - return self.waypoints[indx] -end - ---- Get final waypoint. --- @param #FLIGHTGROUP self --- @return #table Waypoint table. -function FLIGHTGROUP:GetWaypointFinal() - return self.waypoints[#self.waypoints] -end - ---- Get next waypoint. --- @param #FLIGHTGROUP self --- @return #table Waypoint table. -function FLIGHTGROUP:GetWaypointNext() - local n=math.min(self.currentwp+1, #self.waypoints) - return self.waypoints[n] -end - ---- Get current waypoint. --- @param #FLIGHTGROUP self --- @return #table Waypoint table. -function FLIGHTGROUP:GetWaypointCurrent() - return self.waypoints[self.currentwp] -end --- Check if flight is in state in utero. -- @param #FLIGHTGROUP self @@ -1168,33 +833,7 @@ function FLIGHTGROUP:CanAirToAir(ExcludeGuns) end end ---- Activate a *late activated* group. --- @param #FLIGHTGROUP self --- @param #number delay (Optional) Delay in seconds before the group is activated. Default is immediately. --- @return #FLIGHTGROUP self -function FLIGHTGROUP:Activate(delay) - if delay and delay>0 then - self:T2(self.lid..string.format("Activating late activated group in %d seconds", delay)) - self:ScheduleOnce(delay, FLIGHTGROUP.Activate, self) - else - - if self:IsAlive()==false then - - self:T(self.lid.."Activating late activated group") - self.group:Activate() - self.isLateActivated=false - - elseif self:IsAlive()==true then - self:E(self.lid.."WARNING: Activating group that is already activated") - else - self:E(self.lid.."ERROR: Activating group that is does not exist!") - end - - end - - return self -end --- Start an *uncontrolled* group. -- @param #FLIGHTGROUP self @@ -1221,89 +860,6 @@ function FLIGHTGROUP:StartUncontrolled(delay) return self end ---- Set DCS task. Enroute tasks are injected automatically. --- @param #FLIGHTGROUP self --- @param #table DCSTask DCS task structure. --- @return #FLIGHTGROUP self -function FLIGHTGROUP:SetTask(DCSTask) - - if self:IsAlive() then - - if self.taskcurrent>0 then - - -- TODO: Why the hell did I do this? It breaks scheduled tasks. I comment it out for now to see where it fails. - --local task=self:GetTaskCurrent() - --self:RemoveTask(task) - --self.taskcurrent=0 - - end - - -- Inject enroute tasks. - if self.taskenroute and #self.taskenroute>0 then - if tostring(DCSTask.id)=="ComboTask" then - for _,task in pairs(self.taskenroute) do - table.insert(DCSTask.params.tasks, 1, task) - end - else - local tasks=UTILS.DeepCopy(self.taskenroute) - table.insert(tasks, DCSTask) - - DCSTask=self.group.TaskCombo(self, tasks) - end - end - - -- Set task. - self.group:SetTask(DCSTask) - - -- Debug info. - local text=string.format("SETTING Task %s", tostring(DCSTask.id)) - if tostring(DCSTask.id)=="ComboTask" then - for i,task in pairs(DCSTask.params.tasks) do - text=text..string.format("\n[%d] %s", i, tostring(task.id)) - end - end - self:I(self.lid..text) - end - - return self -end - ---- Push DCS task. --- @param #FLIGHTGROUP self --- @param #table DCSTask DCS task structure. --- @return #FLIGHTGROUP self -function FLIGHTGROUP:PushTask(DCSTask) - - if self:IsAlive() then - - -- Push task. - self.group:PushTask(DCSTask) - - -- Debug info. - local text=string.format("PUSHING Task %s", tostring(DCSTask.id)) - if tostring(DCSTask.id)=="ComboTask" then - for i,task in pairs(DCSTask.params.tasks) do - text=text..string.format("\n[%d] %s", i, tostring(task.id)) - end - end - self:I(self.lid..text) - end - - return self -end - ---- Clear DCS tasks. --- @param #FLIGHTGROUP self --- @param #table DCSTask DCS task structure. --- @return #FLIGHTGROUP self -function FLIGHTGROUP:ClearTasks() - if self:IsAlive() then - self.group:ClearTasks() - self:I(self.lid..string.format("CLEARING Tasks")) - end - return self -end - --- Clear the group for landing when it is holding. -- @param #FLIGHTGROUP self -- @param #number Delay Delay in seconds before landing clearance is given. @@ -1323,46 +879,6 @@ function FLIGHTGROUP:ClearToLand(Delay) return self end - ---- Route group along waypoints. Enroute tasks are also applied. --- @param #FLIGHTGROUP self --- @param #table waypoints Table of waypoints. --- @return #FLIGHTGROUP self -function FLIGHTGROUP:Route(waypoints) - - if self:IsAlive() then - - -- DCS task combo. - local Tasks={} - - -- Enroute tasks. - if self.taskenroute then - for _,TaskEnroute in pairs(self.taskenroute) do - --table.insert(Tasks, TaskEnroute) - end - end - - -- Route (Mission) task. - local TaskRoute=self.group:TaskRoute(waypoints) - table.insert(Tasks, TaskRoute) - - -- TaskCombo of enroute and mission tasks. - local TaskCombo=self.group:TaskCombo(Tasks) - - -- Set tasks. - if #Tasks>1 then - self:SetTask(TaskCombo) - else - self:SetTask(TaskRoute) - end - - else - self:E(self.lid.."ERROR: Group is not alive!") - end - - return self -end - --- Get min fuel of group. This returns the relative fuel amount of the element lowest fuel in the group. -- @param #FLIGHTGROUP self -- @return #number Relative fuel in percent. @@ -1388,31 +904,6 @@ function FLIGHTGROUP:GetFuelMin() return fuelmin*100 end ---- Self destruction of group. An explosion is created at the position of each element. --- @param #FLIGHTGROUP self --- @param #number Delay Delay in seconds. Default now. --- @param #number ExplosionPower (Optional) Explosion power in kg TNT. Default 500 kg. --- @return #number Relative fuel in percent. -function FLIGHTGROUP:SelfDestruction(Delay, ExplosionPower) - - if Delay and Delay>0 then - self:ScheduleOnce(Delay, FLIGHTGROUP.SelfDestruction, self, 0, ExplosionPower) - else - - -- Loop over all elements. - for i,_element in pairs(self.elements) do - local element=_element --#FLIGHTGROUP.Element - - local unit=element.unit - - if unit and unit:IsAlive() then - unit:Explode(ExplosionPower) - end - end - end - -end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Start & Status ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/FlightGroupOld.lua b/Moose Development/Moose/Ops/FlightGroupOld.lua new file mode 100644 index 000000000..393413c45 --- /dev/null +++ b/Moose Development/Moose/Ops/FlightGroupOld.lua @@ -0,0 +1,6458 @@ +--- **Ops** - (AI) Flight Group. +-- +-- **Main Features:** +-- +-- * Monitor flight status of elements or entire group. +-- * Monitor fuel and ammo status. +-- * Sophisticated task queueing system. +-- * Many additional events for each element and the whole group. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Ops.FlightGroup +-- @image OPS_FlightGroup.png + + +--- FLIGHTGROUP class. +-- @type FLIGHTGROUP +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. Messages to all about status. +-- @field #number verbose Verbosity level. 0=silent. +-- @field #string lid Class id string for output to DCS log file. +-- @field #string groupname Name of flight group. +-- @field Wrapper.Group#GROUP group Flight group object. +-- @field #string type Aircraft type of flight group. +-- @field #table elements Table of elements, i.e. units of the group. +-- @field #table waypoints Table of waypoints. +-- @field #table waypoints0 Table of initial waypoints. +-- @field #table currentwp Current waypoint. +-- @field #table taskqueue Queue of tasks. +-- @field #number taskcounter Running number of task ids. +-- @field #number taskcurrent ID of current task. If 0, there is no current task assigned. +-- @field #table taskenroute Enroute task of the group. +-- @field #table taskpaused Paused tasks. +-- @field #table missionqueue Queue of missions. +-- @field #number currentmission The ID (auftragsnummer) of the currently assigned AUFTRAG. +-- @field Core.Set#SET_UNIT detectedunits Set of detected units. +-- @field Wrapper.Airbase#AIRBASE homebase The home base of the flight group. +-- @field Wrapper.Airbase#AIRBASE destbase The destination base of the flight group. +-- @field Core.Zone#ZONE homezone The home zone of the flight group. Set when spawn happens in air. +-- @field Core.Zone#ZONE destzone The destination zone of the flight group. Set when final waypoint is in air. +-- @field #string attribute Generalized attribute. +-- @field #string actype Type name of the aircraft. +-- @field #number speedmax Max speed in km/h. +-- @field #number rangemax Max range in km. +-- @field #number ceiling Max altitude the aircraft can fly at in meters. +-- @field #number tankertype The refueling system type (0=boom, 1=probe), if the group is a tanker. +-- @field #number refueltype The refueling system type (0=boom, 1=probe), if the group can refuel from a tanker. +-- @field #FLIGHTGROUP.Ammo ammo Ammunition data. Number of Guns, Rockets, Bombs, Missiles. +-- @field #boolean ai If true, flight is purely AI. If false, flight contains at least one human player. +-- @field #boolean fuellow Fuel low switch. +-- @field #number fuellowthresh Low fuel threshold in percent. +-- @field #boolean fuellowrtb RTB on low fuel switch. +-- @field #boolean fuelcritical Fuel critical switch. +-- @field #number fuelcriticalthresh Critical fuel threshold in percent. +-- @field #boolean fuelcriticalrtb RTB on critical fuel switch. +-- @field #boolean passedfinalwp Group has passed the final waypoint. +-- @field Ops.AirWing#AIRWING airwing The airwing the flight group belongs to. +-- @field Ops.FlightControl#FLIGHTCONTROL flightcontrol The flightcontrol handling this group. +-- @field Ops.Airboss#AIRBOSS airboss The airboss handling this group. +-- @field Core.UserFlag#USERFLAG flaghold Flag for holding. +-- @field #number Tholding Abs. mission time stamp when the group reached the holding point. +-- @field #number Tparking Abs. mission time stamp when the group was spawned uncontrolled and is parking. +-- @field #table menu F10 radio menu. +-- @field #string controlstatus Flight control status. +-- @field Core.Set#SET_ZONE checkzones Set of zones. +-- @field Core.Set#SET_ZONE inzones Set of zones in which the group is currently in. +-- @field #boolean groupinitialized If true, group parameters were initialized. +-- @field #boolean ishelo If true, the is a helicopter group. +-- @field #boolean detectionOn If true, detected units of the group are analyzed. +-- +-- @field #number tacanChannelDefault The default TACAN channel. +-- @field #string tacanMorseDefault The default TACAN morse code. +-- @field #number tacanChannel The currenly used TACAN channel. +-- @field #string tacanMorse The currently used TACAN morse code. +-- @field #boolean tacanOn If true, TACAN is currently active. +-- @field Wrapper.Unit#UNIT tacanBeacon The unit acting as TACAN beacon. +-- +-- @field #number radioFreqDefault Default radio frequency in MHz. +-- @field #number radioFreq Currently used radio frequency in MHz. +-- @field #number radioModuDefault Default Radio modulation `radio.modulation.AM` or `radio.modulation.FM`. +-- @field #number radioModu Currently used radio modulation `radio.modulation.AM` or `radio.modulation.FM`. +-- @field #boolean radioOn If true, radio is currently turned on. +-- @field Core.RadioQueue#RADIOQUEUE radioQueue Radio queue. +-- +-- @field #number CallsignName Call sign name. +-- @field #number CallsignNumber Call sign number. +-- +-- @field #boolean eplrsDefault Default EPLRS data link setting. +-- @field #boolean eplrs If true, EPLRS data link is on. +-- +-- @field #string roeDefault Default ROE setting. +-- @field #string rotDefault Default ROT setting. +-- @field #string roe Current ROE setting. +-- @field #string rot Current ROT setting. +-- +-- @field #number formationDefault Default formation setting. +-- @field #number formation Current formation setting. +-- +-- @field Ops.Auftrag#AUFTRAG missionpaused Paused mission. +-- +-- @field Core.Point#COORDINATE position Current position of the group. +-- @field #number traveldist Distance traveled in meters. This is a lower bound! +-- @field #number traveltime Time. +-- @field #number fuelmass Inital fuel in kg. +-- +-- @extends Core.Fsm#FSM + +--- *To invent an airplane is nothing. To build one is something. To fly is everything.* -- Otto Lilienthal +-- +-- === +-- +-- ![Banner Image](..\Presentations\FlightGroup\FLIGHTGROUP_Main.jpg) +-- +-- # The FLIGHTGROUP Concept +-- +-- # Events +-- +-- This class introduces a lot of additional events that will be handy in many situations. +-- Certain events like landing, takeoff etc. are triggered for each element and also have a corresponding event when the whole group reaches this state. +-- +-- ## Spawning +-- +-- ## Parking +-- +-- ## Taxiing +-- +-- ## Takeoff +-- +-- ## Airborne +-- +-- ## Landed +-- +-- ## Arrived +-- +-- ## Dead +-- +-- ## Fuel +-- +-- ## Ammo +-- +-- ## Detected Units +-- +-- ## Check In Zone +-- +-- ## Passing Waypoint +-- +-- +-- # Tasking +-- +-- The FLIGHTGROUP class significantly simplifies the monitoring of DCS tasks. Two types of tasks can be set +-- +-- * **Scheduled Tasks** +-- * **Waypoint Tasks** +-- +-- ## Scheduled Tasks +-- +-- ## Waypoint Tasks +-- +-- # Missions +-- +-- ## Anti-ship +-- +-- ## AWACS +-- +-- ## INTERCEPT +-- +-- +-- # Examples +-- +-- Here are some examples to show how things are done. +-- +-- ## 1. Spawn +-- +-- ## 2. Attack Group +-- +-- ## 3. Whatever +-- +-- ## 4. Simple Tanker +-- +-- ## 5. Simple AWACS +-- +-- ## 6. Scheduled Tasks +-- +-- ## 7. Waypoint Tasks +-- +-- ## 8. Enroute Tasks +-- +-- +-- +-- +-- @field #FLIGHTGROUP +FLIGHTGROUP = { + ClassName = "FLIGHTGROUP", + Debug = false, + verbose = 0, + lid = nil, + groupname = nil, + group = nil, + grouptemplate = nil, + type = nil, + waypoints = nil, + waypoints0 = nil, + currentwp = -100, + elements = {}, + taskqueue = {}, + taskcounter = nil, + taskcurrent = nil, + taskenroute = nil, + taskpaused = {}, + missionqueue = {}, + currentmission = nil, + detectedunits = {}, + homebase = nil, + destbase = nil, + homezone = nil, + destzone = nil, + attribute = nil, + actype = nil, + speedmax = nil, + rangemax = nil, + ceiling = nil, + fuellow = false, + fuellowthresh = nil, + fuellowrtb = nil, + fuelcritical = nil, + fuelcriticalthresh = nil, + fuelcriticalrtb = false, + squadron = nil, + flightcontrol = nil, + flaghold = nil, + Tholding = nil, + Tparking = nil, + menu = nil, + checkzones = nil, + inzones = nil, + groupinitialized = nil, + ishelo = nil, + beacon = nil, +} + + +--- Status of flight group element. +-- @type FLIGHTGROUP.ElementStatus +-- @field #string INUTERO Element was not spawned yet or its status is unknown so far. +-- @field #string SPAWNED Element was spawned into the world. +-- @field #string PARKING Element is parking after spawned on ramp. +-- @field #string ENGINEON Element started its engines. +-- @field #string TAXIING Element is taxiing after engine startup. +-- @field #string TAKEOFF Element took of after takeoff event. +-- @field #string AIRBORNE Element is airborne. Either after takeoff or after air start. +-- @field #string LANDING Element is landing. +-- @field #string LANDED Element landed and is taxiing to its parking spot. +-- @field #string ARRIVED Element arrived at its parking spot and shut down its engines. +-- @field #string DEAD Element is dead after it crashed, pilot ejected or pilot dead events. +FLIGHTGROUP.ElementStatus={ + INUTERO="inutero", + SPAWNED="spawned", + PARKING="parking", + ENGINEON="engineon", + TAXIING="taxiing", + TAKEOFF="takeoff", + AIRBORNE="airborne", + LANDING="landing", + LANDED="landed", + ARRIVED="arrived", + DEAD="dead", +} + +--- Generalized attribute. See [DCS attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes) on hoggit. +-- @type FLIGHTGROUP.Attribute +-- @field #string TRANSPORTPLANE Airplane with transport capability. This can be used to transport other assets. +-- @field #string AWACS Airborne Early Warning and Control System. +-- @field #string FIGHTER Fighter, interceptor, ... airplane. +-- @field #string BOMBER Aircraft which can be used for strategic bombing. +-- @field #string TANKER Airplane which can refuel other aircraft. +-- @field #string TRANSPORTHELO Helicopter with transport capability. This can be used to transport other assets. +-- @field #string ATTACKHELO Attack helicopter. +-- @field #string UAV Unpiloted Aerial Vehicle, e.g. drones. +-- @field #string OTHER Other aircraft type. +FLIGHTGROUP.Attribute = { + TRANSPORTPLANE="TransportPlane", + AWACS="AWACS", + FIGHTER="Fighter", + BOMBER="Bomber", + TANKER="Tanker", + TRANSPORTHELO="TransportHelo", + ATTACKHELO="AttackHelo", + UAV="UAV", + OTHER="Other", +} + +--- Flight group element. +-- @type FLIGHTGROUP.Element +-- @field #string name Name of the element, i.e. the unit/client. +-- @field Wrapper.Unit#UNIT unit Element unit object. +-- @field Wrapper.Group#GROUP group Group object of the element. +-- @field #string modex Tail number. +-- @field #string skill Skill level. +-- @field #boolean ai If true, element is AI. +-- @field Wrapper.Client#CLIENT client The client if element is occupied by a human player. +-- @field #table pylons Table of pylons. +-- @field #number fuelmass Mass of fuel in kg. +-- @field #number category Aircraft category. +-- @field #string categoryname Aircraft category name. +-- @field #string callsign Call sign, e.g. "Uzi 1-1". +-- @field #string status Status, i.e. born, parking, taxiing. See @{#FLIGHTGROUP.ElementStatus}. +-- @field #number damage Damage of element in percent. +-- @field Wrapper.Airbase#AIRBASE.ParkingSpot parking The parking spot table the element is parking on. + +--- Flight group task status. +-- @type FLIGHTGROUP.TaskStatus +-- @field #string SCHEDULED Task is scheduled. +-- @field #string EXECUTING Task is being executed. +-- @field #string PAUSED Task is paused. +-- @field #string DONE Task is done. +FLIGHTGROUP.TaskStatus={ + SCHEDULED="scheduled", + EXECUTING="executing", + PAUSED="paused", + DONE="done", +} + +--- Flight group task status. +-- @type FLIGHTGROUP.TaskType +-- @field #string SCHEDULED Task is scheduled and will be executed at a given time. +-- @field #string WAYPOINT Task is executed at a specific waypoint. +FLIGHTGROUP.TaskType={ + SCHEDULED="scheduled", + WAYPOINT="waypoint", +} + +--- Flight group task structure. +-- @type FLIGHTGROUP.Task +-- @field #string type Type of task: either SCHEDULED or WAYPOINT. +-- @field #number id Task ID. Running number to get the task. +-- @field #number prio Priority. +-- @field #number time Abs. mission time when to execute the task. +-- @field #table dcstask DCS task structure. +-- @field #string description Brief text which describes the task. +-- @field #string status Task status. +-- @field #number duration Duration before task is cancelled in seconds. Default never. +-- @field #number timestamp Abs. mission time, when task was started. +-- @field #number waypoint Waypoint index if task is a waypoint task. +-- @field Core.UserFlag#USERFLAG stopflag If flag is set to 1 (=true), the task is stopped. + +--- Enroute task. +-- @type FLIGHTGROUP.EnrouteTask +-- @field DCS#Task DCStask DCS task structure table. +-- @field #number WaypointIndex Waypoint number at which the enroute task is added. + +--- Ammo data. +-- @type FLIGHTGROUP.Ammo +-- @field #number Total Total amount of ammo. +-- @field #number Guns Amount of gun shells. +-- @field #number Bombs Amount of bombs. +-- @field #number Rockets Amount of rockets. +-- @field #number Missiles Amount of missiles. +-- @field #number MissilesAA Amount of air-to-air missiles. +-- @field #number MissilesAG Amount of air-to-ground missiles. +-- @field #number MissilesAS Amount of anti-ship missiles. + + +--- FLIGHTGROUP class version. +-- @field #string version +FLIGHTGROUP.version="0.4.1" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Use new UnitLost event instead of crash/dead. +-- TODO: Options EPLRS, Afterburner restrict etc. +-- DONE: Add TACAN beacon. +-- TODO: Damage? +-- TODO: shot events? +-- TODO: Marks to add waypoints/tasks on-the-fly. +-- TODO: Mark assigned parking spot on F10 map. +-- TODO: Let user request a parking spot via F10 marker :) +-- TODO: Monitor traveled distance in air ==> calculate fuel consumption ==> calculate range remaining. Will this give half way accurate results? +-- TODO: Out of AG/AA missiles. Safe state of out-of-ammo. +-- DONE: Add tasks. +-- DONE: Waypoints, read, add, insert, detour. +-- DONE: Get ammo. +-- DONE: Get pylons. +-- DONE: Fuel threshhold ==> RTB. +-- DONE: ROE +-- NOGO: Respawn? With correct loadout, fuelstate. Solved in DCS 2.5.6! + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new FLIGHTGROUP object and start the FSM. +-- @param #FLIGHTGROUP self +-- @param #string groupname Name of the group. +-- @param #string autostart (Optional) If `true` or `nil` automatically start the FSM (default). If `false`, use FLIGHTGROUP:Start() manually. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:New(groupname, autostart) + + -- First check if we already have a flight group for this group. + local fg=_DATABASE:GetFlightGroup(groupname) + if fg then + fg:I(fg.lid..string.format("WARNING: Flight group %s already exists in data base!", groupname)) + return fg + end + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #FLIGHTGROUP + + --self.group=AIGroup + self.groupname=tostring(groupname) + + -- Set some string id for output to DCS.log file. + self.lid=string.format("FLIGHTGROUP %s | ", self.groupname) + + -- Start State. + self:SetStartState("Stopped") + + -- Init set of detected units. + self.detectedunits=SET_UNIT:New() + + -- Init inzone set. + self.inzones=SET_ZONE:New() + + -- Defaults + self:SetFuelLowThreshold() + self:SetFuelCriticalThreshold() + self:SetDefaultROE() + self:SetDefaultROT() + self:SetDetection() + + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "InUtero") -- Start FSM. + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + self:AddTransition("*", "FlightStatus", "*") -- Status update. + self:AddTransition("*", "QueueUpdate", "*") -- Update task and mission queues. + + self:AddTransition("*", "DetectedUnit", "*") -- Add a newly detected unit to the detected units set. + self:AddTransition("*", "DetectedUnitNew", "*") -- Add a newly detected unit to the detected units set. + self:AddTransition("*", "DetectedUnitKnown", "*") -- Add a newly detected unit to the detected units set. + self:AddTransition("*", "DetectedUnitLost", "*") -- Group lost a detected target. + + self:AddTransition("*", "UpdateRoute", "*") -- Update route of group. Only if airborne. + self:AddTransition("*", "Respawn", "*") -- Respawn group. + + self:AddTransition("*", "RTB", "Inbound") -- Group is returning to destination base. + self:AddTransition("*", "RTZ", "Inbound") -- Group is returning to destination zone. Not implemented yet! + self:AddTransition("Inbound", "Holding", "Holding") -- Group is in holding pattern. + + self:AddTransition("*", "Refuel", "Going4Fuel") -- Group is send to refuel at a tanker. Not implemented yet! + self:AddTransition("Going4Fuel", "Refueled", "Airborne") -- Group is send to refuel at a tanker. Not implemented yet! + + self:AddTransition("*", "LandAt", "LandingAt") -- Helo group is ordered to land at a specific point. + self:AddTransition("LandingAt", "LandedAt", "LandedAt") -- Helo group landed landed at a specific point. + + self:AddTransition("*", "PassingWaypoint", "*") -- Group passed a waypoint. + self:AddTransition("*", "GotoWaypoint", "*") -- Group switches to a specific waypoint. + self:AddTransition("*", "Wait", "Waiting") -- Group is orbiting. + + self:AddTransition("*", "FuelLow", "*") -- Fuel state of group is low. Default ~25%. + self:AddTransition("*", "FuelCritical", "*") -- Fuel state of group is critical. Default ~10%. + + self:AddTransition("*", "OutOfAmmo", "*") -- Group is completely out of ammo. + self:AddTransition("*", "OutOfGuns", "*") -- Group is out of gun shells. + self:AddTransition("*", "OutOfRockets", "*") -- Group is out of rockets. + self:AddTransition("*", "OutOfBombs", "*") -- Group is out of bombs. + self:AddTransition("*", "OutOfMissiles", "*") -- Group is out of missiles. + self:AddTransition("*", "OutOfMissilesAA", "*") -- Group is out of A2A missiles. Not implemented yet! + self:AddTransition("*", "OutOfMissilesAG", "*") -- Group is out of A2G missiles. Not implemented yet! + self:AddTransition("*", "OutOfMissilesAS", "*") -- Group is out of A2G missiles. Not implemented yet! + + self:AddTransition("*", "TaskExecute", "*") -- Group will execute a task. + self:AddTransition("*", "TaskPause", "*") -- Pause current task. Not implemented yet! + self:AddTransition("*", "TaskCancel", "*") -- Cancel current task. + self:AddTransition("*", "TaskDone", "*") -- Task is over. + + self:AddTransition("*", "MissionStart", "*") -- Mission is started. + self:AddTransition("*", "MissionExecute", "*") -- Mission execution began. + self:AddTransition("*", "MissionCancel", "*") -- Cancel current mission. + self:AddTransition("*", "PauseMission", "*") -- Pause the current mission. + self:AddTransition("*", "UnpauseMission", "*") -- Unpause the the paused mission. + self:AddTransition("*", "MissionDone", "*") -- Mission is over. + + self:AddTransition("Airborne", "EngageTargets", "Engaging") -- Engage targets. + self:AddTransition("Engaging", "Disengage", "Airborne") -- Engagement over. + + self:AddTransition("*", "ElementSpawned", "*") -- An element was spawned. + self:AddTransition("*", "ElementParking", "*") -- An element is parking. + self:AddTransition("*", "ElementEngineOn", "*") -- An element spooled up the engines. + self:AddTransition("*", "ElementTaxiing", "*") -- An element is taxiing to the runway. + self:AddTransition("*", "ElementTakeoff", "*") -- An element took off. + self:AddTransition("*", "ElementAirborne", "*") -- An element is airborne. + self:AddTransition("*", "ElementLanded", "*") -- An element landed. + self:AddTransition("*", "ElementArrived", "*") -- An element arrived. + self:AddTransition("*", "ElementDead", "*") -- An element crashed, ejected, or pilot dead. + + self:AddTransition("*", "ElementOutOfAmmo", "*") -- An element is completely out of ammo. + + self:AddTransition("*", "CheckZone", "*") -- Check if flight enters/leaves a certain zone. + self:AddTransition("*", "EnterZone", "*") -- Flight entered a certain zone. + self:AddTransition("*", "LeaveZone", "*") -- Flight leaves a certain zone. + + self:AddTransition("*", "FlightSpawned", "Spawned") -- The whole flight group was spawned. + self:AddTransition("*", "FlightParking", "Parking") -- The whole flight group is parking. + self:AddTransition("*", "FlightTaxiing", "Taxiing") -- The whole flight group is taxiing. + self:AddTransition("*", "FlightTakeoff", "Airborne") -- The whole flight group is airborne. + self:AddTransition("*", "FlightAirborne", "Airborne") -- The whole flight group is airborne. + self:AddTransition("*", "FlightLanding", "Landing") -- The whole flight group is landing. + self:AddTransition("*", "FlightLanded", "Landed") -- The whole flight group has landed. + self:AddTransition("*", "FlightArrived", "Arrived") -- The whole flight group has arrived. + self:AddTransition("*", "FlightDead", "Dead") -- The whole flight group is dead. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the FLIGHTGROUP. Initializes parameters and starts event handlers. + -- @function [parent=#FLIGHTGROUP] Start + -- @param #FLIGHTGROUP self + + --- Triggers the FSM event "Start" after a delay. Starts the FLIGHTGROUP. Initializes parameters and starts event handlers. + -- @function [parent=#FLIGHTGROUP] __Start + -- @param #FLIGHTGROUP self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the FLIGHTGROUP and all its event handlers. + -- @param #FLIGHTGROUP self + + --- Triggers the FSM event "Stop" after a delay. Stops the FLIGHTGROUP and all its event handlers. + -- @function [parent=#FLIGHTGROUP] __Stop + -- @param #FLIGHTGROUP self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "FlightStatus". + -- @function [parent=#FLIGHTGROUP] FlightStatus + -- @param #FLIGHTGROUP self + + --- Triggers the FSM event "FlightStatus" after a delay. + -- @function [parent=#FLIGHTGROUP] __FlightStatus + -- @param #FLIGHTGROUP self + -- @param #number delay Delay in seconds. + + + -- Debug trace. + if false then + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(1) + end + + + -- Init task counter. + self.taskcurrent=0 + self.taskcounter=0 + + -- Holding flag. + self.flaghold=USERFLAG:New(string.format("%s_FlagHold", self.groupname)) + self.flaghold:Set(0) + + -- Add to data base. + _DATABASE:AddFlightGroup(self) + + -- Autostart. + if autostart==true or autostart==nil then + self:Start() + end + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add a *scheduled* task. +-- @param #FLIGHTGROUP self +-- @param #table task DCS task table structure. +-- @param #string clock Mission time when task is executed. Default in 5 seconds. If argument passed as #number, it defines a relative delay in seconds. +-- @param #string description Brief text describing the task, e.g. "Attack SAM". +-- @param #number prio Priority of the task. +-- @param #number duration Duration before task is cancelled in seconds counted after task started. Default never. +-- @return #FLIGHTGROUP.Task The task structure. +function FLIGHTGROUP:AddTask(task, clock, description, prio, duration) + + local newtask=self:NewTaskScheduled(task, clock, description, prio, duration) + + -- Add to table. + table.insert(self.taskqueue, newtask) + + -- Info. + self:I(self.lid..string.format("Adding SCHEDULED task %s starting at %s", newtask.description, UTILS.SecondsToClock(newtask.time, true))) + self:T3({newtask=newtask}) + + return newtask +end + +--- Create a *scheduled* task. +-- @param #FLIGHTGROUP self +-- @param #table task DCS task table structure. +-- @param #string clock Mission time when task is executed. Default in 5 seconds. If argument passed as #number, it defines a relative delay in seconds. +-- @param #string description Brief text describing the task, e.g. "Attack SAM". +-- @param #number prio Priority of the task. +-- @param #number duration Duration before task is cancelled in seconds counted after task started. Default never. +-- @return #FLIGHTGROUP.Task The task structure. +function FLIGHTGROUP:NewTaskScheduled(task, clock, description, prio, duration) + + -- Increase counter. + self.taskcounter=self.taskcounter+1 + + -- Set time. + local time=timer.getAbsTime()+5 + if clock then + if type(clock)=="string" then + time=UTILS.ClockToSeconds(clock) + elseif type(clock)=="number" then + time=timer.getAbsTime()+clock + end + end + + -- Task data structure. + local newtask={} --#FLIGHTGROUP.Task + newtask.status=FLIGHTGROUP.TaskStatus.SCHEDULED + newtask.dcstask=task + newtask.description=description or task.id + newtask.prio=prio or 50 + newtask.time=time + newtask.id=self.taskcounter + newtask.duration=duration + newtask.waypoint=-1 + newtask.type=FLIGHTGROUP.TaskType.SCHEDULED + newtask.stopflag=USERFLAG:New(string.format("%s StopTaskFlag %d", self.groupname, newtask.id)) + newtask.stopflag:Set(0) + + return newtask +end + +--- Add a *waypoint* task. +-- @param #FLIGHTGROUP self +-- @param #table task DCS task table structure. +-- @param #number waypointindex Number of waypoint. Counting starts at one! Default is the as *next* waypoint. +-- @param #string description Brief text describing the task, e.g. "Attack SAM". +-- @param #number prio Priority of the task. Number between 1 and 100. Default is 50. +-- @param #number duration Duration before task is cancelled in seconds counted after task started. Default never. +-- @return #FLIGHTGROUP.Task The task structure. +function FLIGHTGROUP:AddTaskWaypoint(task, waypointindex, description, prio, duration) + + -- Increase counter. + self.taskcounter=self.taskcounter+1 + + -- Task data structure. + local newtask={} --#FLIGHTGROUP.Task + newtask.description=description + newtask.status=FLIGHTGROUP.TaskStatus.SCHEDULED + newtask.dcstask=task + newtask.prio=prio or 50 + newtask.id=self.taskcounter + newtask.duration=duration + newtask.time=0 + newtask.waypoint=waypointindex or (self.currentwp and self.currentwp+1 or 2) + newtask.type=FLIGHTGROUP.TaskType.WAYPOINT + newtask.stopflag=USERFLAG:New(string.format("%s StopTaskFlag %d", self.groupname, newtask.id)) + newtask.stopflag:Set(0) + + -- Add to table. + table.insert(self.taskqueue, newtask) + + -- Info. + self:I(self.lid..string.format("Adding WAYPOINT task %s at WP %d", newtask.description, newtask.waypoint)) + self:T3({newtask=newtask}) + + -- Update route. + --self:_CheckFlightDone(1) + self:__UpdateRoute(-1) + + return newtask +end + +--- Add an *enroute* task. +-- @param #FLIGHTGROUP self +-- @param #table task DCS task table structure. +function FLIGHTGROUP:AddTaskEnroute(task) + + + if not self.taskenroute then + self.taskenroute={} + end + + -- Check not to add the same task twice! + local gotit=false + for _,Task in pairs(self.taskenroute) do + if Task.id==task.id then + gotit=true + break + end + end + + if not gotit then + table.insert(self.taskenroute, task) + end + + +end + +--- Add an *enroute* task to attack targets in a certain **circular** zone. +-- @param #FLIGHTGROUP self +-- @param Core.Zone#ZONE_RADIUS ZoneRadius The circular zone, where to engage targets. +-- @param #table TargetTypes (Optional) The target types, passed as a table, i.e. mind the curly brackets {}. Default {"Air"}. +-- @param #number Priority (Optional) Priority. Default 0. +function FLIGHTGROUP:AddTaskEnrouteEngageTargetsInZone(ZoneRadius, TargetTypes, Priority) + local Task=self.group:EnRouteTaskEngageTargetsInZone(ZoneRadius:GetVec2(), ZoneRadius:GetRadius(), TargetTypes, Priority) + self:AddTaskEnroute(Task) +end + + +--- Remove task from task queue. +-- @param #FLIGHTGROUP self +-- @param #FLIGHTGROUP.Task Task The task to be removed from the queue. +-- @return #boolean True if task could be removed. +function FLIGHTGROUP:RemoveTask(Task) + + for i=#self.taskqueue,1,-1 do + local task=self.taskqueue[i] --#FLIGHTGROUP.Task + + if task.id==Task.id then + + -- Remove task from queue. + table.remove(self.taskqueue, i) + + -- Update route if this is a waypoint task. + if task.type==FLIGHTGROUP.TaskType.WAYPOINT and task.status==FLIGHTGROUP.TaskStatus.SCHEDULED then + self:_CheckFlightDone(1) + --self:__UpdateRoute(-1) + end + + return true + end + end + + return false +end + +--- Add mission to queue. +-- @param #FLIGHTGROUP self +-- @param Ops.Auftrag#AUFTRAG Mission Mission for this group. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:AddMission(Mission) + + -- Add flight group to mission. + Mission:AddFlightGroup(self) + + -- Set flight status to SCHEDULED.. + Mission:SetFlightStatus(self, AUFTRAG.FlightStatus.SCHEDULED) + + -- Set mission status to SCHEDULED. + Mission:Scheduled() + + -- Add mission to queue. + table.insert(self.missionqueue, Mission) + + -- Info text. + local text=string.format("Added %s mission %s starting at %s, stopping at %s", + tostring(Mission.type), tostring(Mission.name), UTILS.SecondsToClock(Mission.Tstart, true), Mission.Tstop and UTILS.SecondsToClock(Mission.Tstop, true) or "INF") + self:I(self.lid..text) + + return self +end + +--- Remove mission from queue. +-- @param #FLIGHTGROUP self +-- @param Ops.Auftrag#AUFTRAG Mission Mission to be removed. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:RemoveMission(Mission) + + for i,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission.auftragsnummer==Mission.auftragsnummer then + + -- Remove mission waypoint task. + local Task=Mission:GetFlightWaypointTask(self) + + if Task then + self:RemoveTask(Task) + end + + -- Remove mission from queue. + table.remove(self.missionqueue, i) + + return self + end + + end + + return self +end + +--- Set AIRWING the flight group belongs to. +-- @param #FLIGHTGROUP self +-- @param Ops.AirWing#AIRWING airwing The AIRWING object. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetAirwing(airwing) + self:I(self.lid..string.format("Add flight to AIRWING %s", airwing.alias)) + self.airwing=airwing + return self +end + +--- Get airwing the flight group belongs to. +-- @param #FLIGHTGROUP self +-- @return Ops.AirWing#AIRWING The AIRWING object. +function FLIGHTGROUP:GetAirWing() + return self.airwing +end + +--- Set detection on or off. +-- @param #FLIGHTGROUP self +-- @param #boolean Switch If true, detection is on. If false or nil, detection is off. Default is off. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetDetection(Switch) + self.detectionOn=Switch + return self +end + + +--- Set the FLIGHTCONTROL controlling this flight group. +-- @param #FLIGHTGROUP self +-- @param Ops.FlightControl#FLIGHTCONTROL flightcontrol The FLIGHTCONTROL object. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetFlightControl(flightcontrol) + + -- Check if there is already a FC. + if self.flightcontrol then + if self.flightcontrol.airbasename==flightcontrol.airbasename then + -- Flight control is already controlling this flight! + return + else + -- Remove flight from previous FC. + self.flightcontrol:_RemoveFlight(self) + end + end + + -- Set FC. + self:I(self.lid..string.format("Setting FLIGHTCONTROL to airbase %s", flightcontrol.airbasename)) + self.flightcontrol=flightcontrol + + -- Add flight to all flights. + table.insert(flightcontrol.flights, self) + + -- Update flight's F10 menu. + if self.ai==false then + self:_UpdateMenu(0.5) + end + + return self +end + +--- Get the FLIGHTCONTROL controlling this flight group. +-- @param #FLIGHTGROUP self +-- @return Ops.FlightControl#FLIGHTCONTROL The FLIGHTCONTROL object. +function FLIGHTGROUP:GetFlightControl() + return self.flightcontrol +end + + +--- Set the AIRBOSS controlling this flight group. +-- @param #FLIGHTGROUP self +-- @param Ops.Airboss#AIRBOSS airboss The AIRBOSS object. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetAirboss(airboss) + self.airboss=airboss + return self +end + +--- Set low fuel threshold. Triggers event "FuelLow" and calls event function "OnAfterFuelLow". +-- @param #FLIGHTGROUP self +-- @param #number threshold Fuel threshold in percent. Default 25 %. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetFuelLowThreshold(threshold) + self.fuellowthresh=threshold or 25 + return self +end + +--- Set if low fuel threshold is reached, flight goes RTB. +-- @param #FLIGHTGROUP self +-- @param #boolean switch If true or nil, flight goes RTB. If false, turn this off. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetFuelLowRTB(switch) + if switch==false then + self.fuellowrtb=false + else + self.fuellowrtb=true + end + return self +end + +--- Set if low fuel threshold is reached, flight tries to refuel at the neares tanker. +-- @param #FLIGHTGROUP self +-- @param #boolean switch If true or nil, flight goes for refuelling. If false, turn this off. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetFuelLowRefuel(switch) + if switch==false then + self.fuellowrefuel=false + else + self.fuellowrefuel=true + end + return self +end + +--- Set fuel critical threshold. Triggers event "FuelCritical" and event function "OnAfterFuelCritical". +-- @param #FLIGHTGROUP self +-- @param #number threshold Fuel threshold in percent. Default 10 %. +-- @param #boolean rtb If true, RTB on fuel critical event. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetFuelCriticalThreshold(threshold, rtb) + self.fuelcriticalthresh=threshold or 10 + self.fuelcriticalrtb=rtb + return self +end + +--- Define a SET of zones that trigger and event if the group enters or leaves any of the zones. +-- @param #FLIGHTGROUP self +-- @param Core.Set#SET_ZONE CheckZonesSet Set of zones. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetCheckZones(CheckZonesSet) + self.checkzones=CheckZonesSet + return self +end + +--- Add a zone that triggers and event if the group enters or leaves any of the zones. +-- @param #FLIGHTGROUP self +-- @param Core.Zone#ZONE CheckZone Zone to check. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:AddCheckZone(CheckZone) + if not self.checkzones then + self.checkzones=SET_ZONE:New() + end + self.checkzones:AddZone(CheckZone) + return self +end + +--- Get set of detected units. +-- @param #FLIGHTGROUP self +-- @return Core.Set#SET_UNIT Set of detected units. +function FLIGHTGROUP:GetDetectedUnits() + return self.detectedunits +end + +--- Get MOOSE group object. +-- @param #FLIGHTGROUP self +-- @return Wrapper.Group#GROUP Moose group object. +function FLIGHTGROUP:GetGroup() + return self.group +end + +--- Get flight group name. +-- @param #FLIGHTGROUP self +-- @return #string Group name. +function FLIGHTGROUP:GetName() + return self.groupname +end + +--- Get current coordinate of the group. +-- @param #FLIGHTGROUP self +-- @return Core.Point#COORDINATE The coordinate (of the first unit) of the group. +function FLIGHTGROUP:GetCoordinate() + if self:IsAlive()~=nil then + return self.group:GetCoordinate() + else + self:E(self.lid.."WARNING: Group is not alive. Cannot get coordinate!") + end + return nil +end + +--- Get waypoint. +-- @param #FLIGHTGROUP self +-- @param #number indx Waypoint index. +-- @return #table Waypoint table. +function FLIGHTGROUP:GetWaypoint(indx) + return self.waypoints[indx] +end + +--- Get final waypoint. +-- @param #FLIGHTGROUP self +-- @return #table Waypoint table. +function FLIGHTGROUP:GetWaypointFinal() + return self.waypoints[#self.waypoints] +end + +--- Get next waypoint. +-- @param #FLIGHTGROUP self +-- @return #table Waypoint table. +function FLIGHTGROUP:GetWaypointNext() + local n=math.min(self.currentwp+1, #self.waypoints) + return self.waypoints[n] +end + +--- Get current waypoint. +-- @param #FLIGHTGROUP self +-- @return #table Waypoint table. +function FLIGHTGROUP:GetWaypointCurrent() + return self.waypoints[self.currentwp] +end + +--- Check if flight is in state in utero. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight is not spawned yet. +function FLIGHTGROUP:IsInUtero() + return self:Is("InUtero") +end + +--- Check if flight is in state spawned. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight is spawned. +function FLIGHTGROUP:IsSpawned() + return self:Is("Spawned") +end + +--- Check if flight is parking. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight is parking after spawned. +function FLIGHTGROUP:IsParking() + return self:Is("Parking") +end + +--- Check if flight is parking. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight is taxiing after engine start up. +function FLIGHTGROUP:IsTaxiing() + return self:Is("Taxiing") +end + +--- Check if flight is airborne. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight is airborne. +function FLIGHTGROUP:IsAirborne() + return self:Is("Airborne") +end + +--- Check if flight is waiting after passing final waypoint. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight is waiting. +function FLIGHTGROUP:IsWaiting() + return self:Is("Waiting") +end + +--- Check if flight is landing. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight is landing, i.e. on final approach. +function FLIGHTGROUP:IsLanding() + return self:Is("Landing") +end + +--- Check if flight has landed and is now taxiing to its parking spot. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight has landed +function FLIGHTGROUP:IsLanded() + return self:Is("Landed") +end + +--- Check if flight has arrived at its destination parking spot. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight has arrived at its destination and is parking. +function FLIGHTGROUP:IsArrived() + return self:Is("Arrived") +end + +--- Check if flight is inbound and traveling to holding pattern. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight is holding. +function FLIGHTGROUP:IsInbound() + return self:Is("Inbound") +end + +--- Check if flight is holding and waiting for landing clearance. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight is holding. +function FLIGHTGROUP:IsHolding() + return self:Is("Holding") +end + +--- Check if flight is going for fuel. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight is refueling. +function FLIGHTGROUP:IsGoing4Fuel() + return self:Is("Going4Fuel") +end + +--- Check if helo(!) flight is ordered to land at a specific point. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, group has task to land somewhere. +function FLIGHTGROUP:IsLandingAt() + return self:Is("LandingAt") +end + +--- Check if helo(!) flight is currently landed at a specific point. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, group is currently landed at the assigned position and waiting until task is complete. +function FLIGHTGROUP:IsLandedAt() + return self:Is("LandedAt") +end + +--- Check if flight is dead. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, all units/elements of the flight are dead. +function FLIGHTGROUP:IsDead() + return self:Is("Dead") +end + +--- Check if flight FSM is stopped. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, FSM state is stopped. +function FLIGHTGROUP:IsStopped() + return self:Is("Stopped") +end + +--- Check if flight is low on fuel. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight is low on fuel. +function FLIGHTGROUP:IsFuelLow() + return self.fuellow +end + +--- Check if flight is critical on fuel. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight is critical on fuel. +function FLIGHTGROUP:IsFuelCritical() + return self.fuelcritical +end + +--- Check if flight is alive. +-- @param #FLIGHTGROUP self +-- @return #boolean *true* if group is exists and is activated, *false* if group is exist but is NOT activated. *nil* otherwise, e.g. the GROUP object is *nil* or the group is not spawned yet. +function FLIGHTGROUP:IsAlive() + + if self.group then + return self.group:IsAlive() + end + + return nil +end + +--- Check if flight can do air-to-ground tasks. +-- @param #FLIGHTGROUP self +-- @param #boolean ExcludeGuns If true, exclude gun +-- @return #boolean *true* if has air-to-ground weapons. +function FLIGHTGROUP:CanAirToGround(ExcludeGuns) + local ammo=self:GetAmmoTot() + if ExcludeGuns then + return ammo.MissilesAG+ammo.Rockets+ammo.Bombs>0 + else + return ammo.MissilesAG+ammo.Rockets+ammo.Bombs+ammo.Guns>0 + end +end + +--- Check if flight can do air-to-air attacks. +-- @param #FLIGHTGROUP self +-- @param #boolean ExcludeGuns If true, exclude available gun shells. +-- @return #boolean *true* if has air-to-ground weapons. +function FLIGHTGROUP:CanAirToAir(ExcludeGuns) + local ammo=self:GetAmmoTot() + if ExcludeGuns then + return ammo.MissilesAA>0 + else + return ammo.MissilesAA+ammo.Guns>0 + end +end + +--- Activate a *late activated* group. +-- @param #FLIGHTGROUP self +-- @param #number delay (Optional) Delay in seconds before the group is activated. Default is immediately. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:Activate(delay) + + if delay and delay>0 then + self:T2(self.lid..string.format("Activating late activated group in %d seconds", delay)) + self:ScheduleOnce(delay, FLIGHTGROUP.Activate, self) + else + + if self:IsAlive()==false then + + self:T(self.lid.."Activating late activated group") + self.group:Activate() + self.isLateActivated=false + + elseif self:IsAlive()==true then + self:E(self.lid.."WARNING: Activating group that is already activated") + else + self:E(self.lid.."ERROR: Activating group that is does not exist!") + end + + end + + return self +end + +--- Start an *uncontrolled* group. +-- @param #FLIGHTGROUP self +-- @param #number delay (Optional) Delay in seconds before the group is started. Default is immediately. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:StartUncontrolled(delay) + + if delay and delay>0 then + self:T2(self.lid..string.format("Starting uncontrolled group in %d seconds", delay)) + self:ScheduleOnce(delay, FLIGHTGROUP.StartUncontrolled, self) + else + + if self:IsAlive() then + --TODO: check Alive==true and Alive==false ==> Activate first + self:I(self.lid.."Starting uncontrolled group") + self.group:StartUncontrolled(delay) + self.isUncontrolled=true + else + self:E(self.lid.."ERROR: Could not start uncontrolled group as it is NOT alive!") + end + + end + + return self +end + +--- Set DCS task. Enroute tasks are injected automatically. +-- @param #FLIGHTGROUP self +-- @param #table DCSTask DCS task structure. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetTask(DCSTask) + + if self:IsAlive() then + + if self.taskcurrent>0 then + + -- TODO: Why the hell did I do this? It breaks scheduled tasks. I comment it out for now to see where it fails. + --local task=self:GetTaskCurrent() + --self:RemoveTask(task) + --self.taskcurrent=0 + + end + + -- Inject enroute tasks. + if self.taskenroute and #self.taskenroute>0 then + if tostring(DCSTask.id)=="ComboTask" then + for _,task in pairs(self.taskenroute) do + table.insert(DCSTask.params.tasks, 1, task) + end + else + local tasks=UTILS.DeepCopy(self.taskenroute) + table.insert(tasks, DCSTask) + + DCSTask=self.group.TaskCombo(self, tasks) + end + end + + -- Set task. + self.group:SetTask(DCSTask) + + -- Debug info. + local text=string.format("SETTING Task %s", tostring(DCSTask.id)) + if tostring(DCSTask.id)=="ComboTask" then + for i,task in pairs(DCSTask.params.tasks) do + text=text..string.format("\n[%d] %s", i, tostring(task.id)) + end + end + self:I(self.lid..text) + end + + return self +end + +--- Push DCS task. +-- @param #FLIGHTGROUP self +-- @param #table DCSTask DCS task structure. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:PushTask(DCSTask) + + if self:IsAlive() then + + -- Push task. + self.group:PushTask(DCSTask) + + -- Debug info. + local text=string.format("PUSHING Task %s", tostring(DCSTask.id)) + if tostring(DCSTask.id)=="ComboTask" then + for i,task in pairs(DCSTask.params.tasks) do + text=text..string.format("\n[%d] %s", i, tostring(task.id)) + end + end + self:I(self.lid..text) + end + + return self +end + +--- Clear DCS tasks. +-- @param #FLIGHTGROUP self +-- @param #table DCSTask DCS task structure. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:ClearTasks() + if self:IsAlive() then + self.group:ClearTasks() + self:I(self.lid..string.format("CLEARING Tasks")) + end + return self +end + +--- Clear the group for landing when it is holding. +-- @param #FLIGHTGROUP self +-- @param #number Delay Delay in seconds before landing clearance is given. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:ClearToLand(Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, FLIGHTGROUP.ClearToLand, self) + else + + if self:IsHolding() then + self:I(self.lid..string.format("Clear to land ==> setting holding flag to 1 (true)")) + self.flaghold:Set(1) + end + + end + return self +end + + +--- Route group along waypoints. Enroute tasks are also applied. +-- @param #FLIGHTGROUP self +-- @param #table waypoints Table of waypoints. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:Route(waypoints) + + if self:IsAlive() then + + -- DCS task combo. + local Tasks={} + + -- Enroute tasks. + if self.taskenroute then + for _,TaskEnroute in pairs(self.taskenroute) do + --table.insert(Tasks, TaskEnroute) + end + end + + -- Route (Mission) task. + local TaskRoute=self.group:TaskRoute(waypoints) + table.insert(Tasks, TaskRoute) + + -- TaskCombo of enroute and mission tasks. + local TaskCombo=self.group:TaskCombo(Tasks) + + -- Set tasks. + if #Tasks>1 then + self:SetTask(TaskCombo) + else + self:SetTask(TaskRoute) + end + + else + self:E(self.lid.."ERROR: Group is not alive!") + end + + return self +end + +--- Get min fuel of group. This returns the relative fuel amount of the element lowest fuel in the group. +-- @param #FLIGHTGROUP self +-- @return #number Relative fuel in percent. +function FLIGHTGROUP:GetFuelMin() + + local fuelmin=math.huge + for i,_element in pairs(self.elements) do + local element=_element --#FLIGHTGROUP.Element + + local unit=element.unit + + local life=unit:GetLife() + + if unit and unit:IsAlive() and life>1 then + local fuel=unit:GetFuel() + if fuel0 then + self:ScheduleOnce(Delay, FLIGHTGROUP.SelfDestruction, self, 0, ExplosionPower) + else + + -- Loop over all elements. + for i,_element in pairs(self.elements) do + local element=_element --#FLIGHTGROUP.Element + + local unit=element.unit + + if unit and unit:IsAlive() then + unit:Explode(ExplosionPower) + end + end + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Start & Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the FLIGHTGROUP FSM and event handlers. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterStart(From, Event, To) + + -- Short info. + local text=string.format("Starting flight group v%s", FLIGHTGROUP.version) + self:I(self.lid..text) + + -- Set current waypoint. Counting starts a one. + self.currentwp=1 + + -- Check if the group is already alive and if so, add its elements. + local group=GROUP:FindByName(self.groupname) + + if group then + + -- Set group object. + self.group=group + + -- Get units of group. + local units=group:GetUnits() or {} + + -- Add elemets. + for _,unit in pairs(units) do + local element=self:AddElementByName(unit:GetName()) + end + + if not self.groupinitialized then + self:_InitGroup() + end + + if group:IsAlive() then + + -- Debug info. + self:T(self.lid..string.format("Found EXISTING and ALIVE group %s at start with %d/%d units/elements", group:GetName(), #units, #self.elements)) + + -- Trigger spawned event for all elements. + for _,element in pairs(self.elements) do + -- Add a little delay or the OnAfterSpawned function is not even initialized and will not be called. + self:__ElementSpawned(0.1, element) + end + + else + -- Debug info. + self:T(self.lid..string.format("Found EXISTING but LATE ACTIVATED group %s at start with %d/%d units/elements", group:GetName(), #units, #self.elements)) + end + + end + + -- Handle events: + self:HandleEvent(EVENTS.Birth, self.OnEventBirth) + self:HandleEvent(EVENTS.EngineStartup, self.OnEventEngineStartup) + self:HandleEvent(EVENTS.Takeoff, self.OnEventTakeOff) + self:HandleEvent(EVENTS.Land, self.OnEventLanding) + self:HandleEvent(EVENTS.EngineShutdown, self.OnEventEngineShutdown) + self:HandleEvent(EVENTS.PilotDead, self.OnEventPilotDead) + self:HandleEvent(EVENTS.Ejection, self.OnEventEjection) + self:HandleEvent(EVENTS.Crash, self.OnEventCrash) + self:HandleEvent(EVENTS.RemoveUnit, self.OnEventRemoveUnit) + + -- Start the status monitoring. + self:__CheckZone(-1) + self:__FlightStatus(-2) + self:__QueueUpdate(-3) +end + +--- On after Start event. Starts the FLIGHTGROUP FSM and event handlers. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterStop(From, Event, To) + + -- Check if group is still alive. + if self:IsAlive() then + + -- Set element parking spot to FREE (after arrived for example). + if self.flightcontrol then + for _,_element in pairs(self.elements) do + local element=_element --#FLIGHTGROUP.Element + self:_SetElementParkingFree(element) + end + end + + -- Destroy group. No event is generated. + self.group:Destroy(false) + end + + -- Handle events: + self:UnHandleEvent(EVENTS.Birth) + self:UnHandleEvent(EVENTS.EngineStartup) + self:UnHandleEvent(EVENTS.Takeoff) + self:UnHandleEvent(EVENTS.Land) + self:UnHandleEvent(EVENTS.EngineShutdown) + self:UnHandleEvent(EVENTS.PilotDead) + self:UnHandleEvent(EVENTS.Ejection) + self:UnHandleEvent(EVENTS.Crash) + self:UnHandleEvent(EVENTS.RemoveUnit) + + self.CallScheduler:Clear() + + _DATABASE.FLIGHTGROUPS[self.groupname]=nil + + self:I(self.lid.."STOPPED! Unhandled events, cleared scheduler and removed from database.") +end + + +--- On after "FlightStatus" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterFlightStatus(From, Event, To) + + -- FSM state. + local fsmstate=self:GetState() + + --- + -- Detection + --- + + -- Check if group has detected any units. + if self.detectionOn then + self:_CheckDetectedUnits() + end + + --- + -- Parking + --- + + -- Check if flight began to taxi (if it was parking). + if self:IsParking() then + for _,_element in pairs(self.elements) do + local element=_element --#FLIGHTGROUP.Element + if element.parking then + + -- Get distance to assigned parking spot. + local dist=element.unit:GetCoordinate():Get2DDistance(element.parking.Coordinate) + + -- If distance >10 meters, we consider the unit as taxiing. + -- TODO: Check distance threshold! If element is taxiing, the parking spot is free again. + -- When the next plane is spawned on this spot, collisions should be avoided! + if dist>10 then + if element.status==FLIGHTGROUP.ElementStatus.ENGINEON then + self:ElementTaxiing(element) + end + end + + else + --self:E(self.lid..string.format("Element %s is in PARKING queue but has no parking spot assigned!", element.name)) + end + end + end + + --- + -- Elements + --- + + local nTaskTot, nTaskSched, nTaskWP=self:CountRemainingTasks() + local nMissions=self:CountRemainingMissison() + + -- Short info. + if self.verbose>0 then + local text=string.format("Status %s [%d/%d]: Tasks=%d (%d,%d) Current=%d. Missions=%s. Waypoint=%d/%d. Detected=%d. Destination=%s, FC=%s", + fsmstate, #self.elements, #self.elements, nTaskTot, nTaskSched, nTaskWP, self.taskcurrent, nMissions, self.currentwp or 0, self.waypoints and #self.waypoints or 0, + self.detectedunits:Count(), self.destbase and self.destbase:GetName() or "unknown", self.flightcontrol and self.flightcontrol.airbasename or "none") + self:I(self.lid..text) + end + + -- Element status. + if self.verbose>1 then + local text="Elements:" + for i,_element in pairs(self.elements) do + local element=_element --#FLIGHTGROUP.Element + + local name=element.name + local status=element.status + local unit=element.unit + local fuel=unit:GetFuel() or 0 + local life=unit:GetLifeRelative() or 0 + local parking=element.parking and tostring(element.parking.TerminalID) or "X" + + -- Check if element is not dead and we missed an event. + if life<0 and element.status~=FLIGHTGROUP.ElementStatus.DEAD and element.status~=FLIGHTGROUP.ElementStatus.INUTERO then + self:ElementDead(element) + end + + -- Get ammo. + local ammo=self:GetAmmoElement(element) + + -- Output text for element. + text=text..string.format("\n[%d] %s: status=%s, fuel=%.1f, life=%.1f, guns=%d, rockets=%d, bombs=%d, missiles=%d (AA=%d, AG=%d, AS=%s), parking=%s", + i, name, status, fuel*100, life*100, ammo.Guns, ammo.Rockets, ammo.Bombs, ammo.Missiles, ammo.MissilesAA, ammo.MissilesAG, ammo.MissilesAS, parking) + end + if #self.elements==0 then + text=text.." none!" + end + self:I(self.lid..text) + end + + --- + -- Distance travelled + --- + + if self.verbose>1 and self:IsAlive() and self.position then + + local time=timer.getAbsTime() + + -- Current position. + local position=self:GetCoordinate() + + -- Travelled distance since last check. + local ds=self.position:Get3DDistance(position) + + -- Time interval. + local dt=time-self.traveltime + + -- Speed. + local v=ds/dt + + -- Add up travelled distance. + self.traveldist=self.traveldist+ds + + + -- Max fuel time remaining. + local TmaxFuel=math.huge + + for _,_element in pairs(self.elements) do + local element=_element --#FLIGHTGROUP.Element + + -- Get relative fuel of element. + local fuel=element.unit:GetFuel() or 0 + + -- Relative fuel used since last check. + local dFrel=element.fuelrel-fuel + + -- Relative fuel used per second. + local dFreldt=dFrel/dt + + -- Fuel remaining in seconds. + local Tfuel=fuel/dFreldt + + if Tfuel Tfuel=%.1f min", element.name, fuel*100, dFrel*100, dFreldt*100*60, Tfuel/60)) + + -- Store rel fuel. + element.fuelrel=fuel + end + + -- Log outut. + self:I(self.lid..string.format("Travelled ds=%.1f km dt=%.1f s ==> v=%.1f knots. Fuel left for %.1f min", self.traveldist/1000, dt, UTILS.MpsToKnots(v), TmaxFuel/60)) + + + -- Update parameters. + self.traveltime=time + self.position=position + end + + --- + -- Tasks + --- + + -- Task queue. + if #self.taskqueue>0 and self.verbose>1 then + local text=string.format("Tasks #%d", #self.taskqueue) + for i,_task in pairs(self.taskqueue) do + local task=_task --#FLIGHTGROUP.Task + local name=task.description + local taskid=task.dcstask.id or "unknown" + local status=task.status + local clock=UTILS.SecondsToClock(task.time, true) + local eta=task.time-timer.getAbsTime() + local started=task.timestamp and UTILS.SecondsToClock(task.timestamp, true) or "N/A" + local duration=-1 + if task.duration then + duration=task.duration + if task.timestamp then + -- Time the task is running. + duration=task.duration-(timer.getAbsTime()-task.timestamp) + else + -- Time the task is supposed to run. + duration=task.duration + end + end + -- Output text for element. + if task.type==FLIGHTGROUP.TaskType.SCHEDULED then + text=text..string.format("\n[%d] %s (%s): status=%s, scheduled=%s (%d sec), started=%s, duration=%d", i, taskid, name, status, clock, eta, started, duration) + elseif task.type==FLIGHTGROUP.TaskType.WAYPOINT then + text=text..string.format("\n[%d] %s (%s): status=%s, waypoint=%d, started=%s, duration=%d, stopflag=%d", i, taskid, name, status, task.waypoint, started, duration, task.stopflag:Get()) + end + end + self:I(self.lid..text) + end + + --- + -- Missions + --- + + -- Current mission name. + if self.verbose>0 then + local Mission=self:GetMissionByID(self.currentmission) + + -- Current status. + local text=string.format("Missions %d, Current: %s", self:CountRemainingMissison(), Mission and Mission.name or "none") + for i,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + local Cstart= UTILS.SecondsToClock(mission.Tstart, true) + local Cstop = mission.Tstop and UTILS.SecondsToClock(mission.Tstop, true) or "INF" + text=text..string.format("\n[%d] %s (%s) status=%s (%s), Time=%s-%s, prio=%d wp=%s targets=%d", + i, tostring(mission.name), mission.type, mission:GetFlightStatus(self), tostring(mission.status), Cstart, Cstop, mission.prio, tostring(mission:GetFlightWaypointIndex(self)), mission:CountMissionTargets()) + end + self:I(self.lid..text) + end + + --- + -- Fuel State + --- + + -- Only if group is in air. + if self:IsAlive() and self.group:IsAirborne(true) then + + local fuelmin=self:GetFuelMin() + + if fuelmin>=self.fuellowthresh then + self.fuellow=false + end + + if fuelmin>=self.fuelcriticalthresh then + self.fuelcritical=false + end + + + -- Low fuel? + if fuelmin=task.time then + return task + end + end + + return nil +end + +--- Get next mission. +-- @param #FLIGHTGROUP self +-- @return Ops.Auftrag#AUFTRAG Next mission or *nil*. +function FLIGHTGROUP:_GetNextMission() + + -- Number of missions. + local Nmissions=#self.missionqueue + + -- Treat special cases. + if Nmissions==0 then + return nil + end + + -- Sort results table wrt times they have already been engaged. + local function _sort(a, b) + local taskA=a --Ops.Auftrag#AUFTRAG + local taskB=b --Ops.Auftrag#AUFTRAG + return (taskA.prio1 groups to have passed. + -- TODO: Can I do this more rigorously? + self:ScheduleOnce(1, reset) + + else + + -- Set homebase if not already set. + if EventData.Place then + self.homebase=self.homebase or EventData.Place + end + + -- Get element. + local element=self:GetElementByName(unitname) + + -- Create element spawned event if not already present. + if not self:_IsElement(unitname) then + element=self:AddElementByName(unitname) + end + + -- Set element to spawned state. + self:T3(self.lid..string.format("EVENT: Element %s born ==> spawned", element.name)) + self:ElementSpawned(element) + + end + + end + +end + +--- Flightgroup event function handling the crash of a unit. +-- @param #FLIGHTGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function FLIGHTGROUP:OnEventEngineStartup(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + -- Get element. + local element=self:GetElementByName(unitname) + + if element then + + if self:IsAirborne() or self:IsInbound() or self:IsHolding() then + -- TODO: what? + else + self:T3(self.lid..string.format("EVENT: Element %s started engines ==> taxiing (if AI)", element.name)) + -- TODO: could be that this element is part of a human flight group. + -- Problem: when player starts hot, the AI does too and starts to taxi immidiately :( + -- when player starts cold, ? + if self.ai then + self:ElementEngineOn(element) + else + if element.ai then + -- AI wingmen will start taxiing even if the player/client is still starting up his engines :( + self:ElementEngineOn(element) + end + end + end + + end + + end + +end + +--- Flightgroup event function handling the crash of a unit. +-- @param #FLIGHTGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function FLIGHTGROUP:OnEventTakeOff(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + -- Get element. + local element=self:GetElementByName(unitname) + + if element then + self:T3(self.lid..string.format("EVENT: Element %s took off ==> airborne", element.name)) + self:ElementTakeoff(element, EventData.Place) + end + + end + +end + +--- Flightgroup event function handling the crash of a unit. +-- @param #FLIGHTGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function FLIGHTGROUP:OnEventLanding(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + -- Get element. + local element=self:GetElementByName(unitname) + + local airbase=EventData.Place + + local airbasename="unknown" + if airbase then + airbasename=tostring(airbase:GetName()) + end + + if element then + self:T3(self.lid..string.format("EVENT: Element %s landed at %s ==> landed", element.name, airbasename)) + self:ElementLanded(element, airbase) + end + + end + +end + +--- Flightgroup event function handling the crash of a unit. +-- @param #FLIGHTGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function FLIGHTGROUP:OnEventEngineShutdown(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + -- Get element. + local element=self:GetElementByName(unitname) + + if element then + + if element.unit and element.unit:IsAlive() then + + local airbase=element.unit:GetCoordinate():GetClosestAirbase() + local parking=self:GetParkingSpot(element, 10, airbase) + + if airbase and parking then + self:ElementArrived(element, airbase, parking) + self:T3(self.lid..string.format("EVENT: Element %s shut down engines ==> arrived", element.name)) + else + self:T3(self.lid..string.format("EVENT: Element %s shut down engines (in air) ==> dead", element.name)) + self:ElementDead(element) + end + + else + + self:I(self.lid..string.format("EVENT: Element %s shut down engines but is NOT alive ==> waiting for crash event (==> dead)", element.name)) + + end + + else + -- element is nil + end + + end + +end + + +--- Flightgroup event function handling the crash of a unit. +-- @param #FLIGHTGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function FLIGHTGROUP:OnEventCrash(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + -- Get element. + local element=self:GetElementByName(unitname) + + if element then + self:T3(self.lid..string.format("EVENT: Element %s crashed ==> dead", element.name)) + self:ElementDead(element) + end + + end + +end + +--- Flightgroup event function handling the crash of a unit. +-- @param #FLIGHTGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function FLIGHTGROUP:OnEventRemoveUnit(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + -- Get element. + local element=self:GetElementByName(unitname) + + if element then + self:I(self.lid..string.format("EVENT: Element %s removed ==> dead", element.name)) + self:ElementDead(element) + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "ElementSpawned" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #FLIGHTGROUP.Element Element The flight group element. +function FLIGHTGROUP:onafterElementSpawned(From, Event, To, Element) + self:T(self.lid..string.format("Element spawned %s", Element.name)) + + -- Set element status. + self:_UpdateStatus(Element, FLIGHTGROUP.ElementStatus.SPAWNED) + + if Element.unit:InAir() then + -- Trigger ElementAirborne event. Add a little delay because spawn is also delayed! + self:__ElementAirborne(0.11, Element) + else + + -- Get parking spot. + local spot=self:GetParkingSpot(Element, 10) + + if spot then + + -- Trigger ElementParking event. Add a little delay because spawn is also delayed! + self:__ElementParking(0.11, Element, spot) + + else + -- TODO: This can happen if spawned on deck of a carrier! + self:E(self.lid..string.format("Element spawned not in air but not on any parking spot.")) + self:__ElementParking(0.11, Element) + end + end +end + +--- On after "ElementParking" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param #FLIGHTGROUP.ParkingSpot Spot Parking Spot. +function FLIGHTGROUP:onafterElementParking(From, Event, To, Element, Spot) + self:T(self.lid..string.format("Element parking %s at spot %s", Element.name, Element.parking and tostring(Element.parking.TerminalID) or "N/A")) + + -- Set element status. + self:_UpdateStatus(Element, FLIGHTGROUP.ElementStatus.PARKING) + + if Spot then + self:_SetElementParkingAt(Element, Spot) + end + + if self:IsTakeoffCold() then + -- Wait for engine startup event. + elseif self:IsTakeoffHot() then + self:__ElementEngineOn(0.5, Element) -- delay a bit to allow all elements + elseif self:IsTakeoffRunway() then + self:__ElementEngineOn(0.5, Element) + end +end + +--- On after "ElementEngineOn" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #FLIGHTGROUP.Element Element The flight group element. +function FLIGHTGROUP:onafterElementEngineOn(From, Event, To, Element) + + -- Debug info. + self:T(self.lid..string.format("Element %s started engines", Element.name)) + + -- Set element status. + self:_UpdateStatus(Element, FLIGHTGROUP.ElementStatus.ENGINEON) +end + +--- On after "ElementTaxiing" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #FLIGHTGROUP.Element Element The flight group element. +function FLIGHTGROUP:onafterElementTaxiing(From, Event, To, Element) + + -- Get terminal ID. + local TerminalID=Element.parking and tostring(Element.parking.TerminalID) or "N/A" + + -- Debug info. + self:I(self.lid..string.format("Element taxiing %s. Parking spot %s is now free", Element.name, TerminalID)) + + -- Set parking spot to free. Also for FC. + self:_SetElementParkingFree(Element) + + -- Set element status. + self:_UpdateStatus(Element, FLIGHTGROUP.ElementStatus.TAXIING) +end + +--- On after "ElementTakeoff" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Wrapper.Airbase#AIRBASE airbase The airbase if applicable or nil. +function FLIGHTGROUP:onafterElementTakeoff(From, Event, To, Element, airbase) + self:I(self.lid..string.format("Element takeoff %s at %s airbase.", Element.name, airbase and airbase:GetName() or "unknown")) + + -- Helos with skids just take off without taxiing! + if Element.parking then + self:_SetElementParkingFree(Element) + end + + -- Set element status. + self:_UpdateStatus(Element, FLIGHTGROUP.ElementStatus.TAKEOFF, airbase) + + -- Trigger element airborne event. + self:__ElementAirborne(2, Element) +end + +--- On after "ElementAirborne" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #FLIGHTGROUP.Element Element The flight group element. +function FLIGHTGROUP:onafterElementAirborne(From, Event, To, Element) + self:T2(self.lid..string.format("Element airborne %s", Element.name)) + + -- Set element status. + self:_UpdateStatus(Element, FLIGHTGROUP.ElementStatus.AIRBORNE) +end + +--- On after "ElementLanded" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Wrapper.Airbase#AIRBASE airbase The airbase if applicable or nil. +function FLIGHTGROUP:onafterElementLanded(From, Event, To, Element, airbase) + self:T2(self.lid..string.format("Element landed %s at %s airbase", Element.name, airbase and airbase:GetName() or "unknown")) + + -- Helos with skids land directly on parking spots. + if self.ishelo then + + local Spot=self:GetParkingSpot(Element, 10, airbase) + + self:_SetElementParkingAt(Element, Spot) + + end + + -- Set element status. + self:_UpdateStatus(Element, FLIGHTGROUP.ElementStatus.LANDED, airbase) +end + +--- On after "ElementArrived" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Wrapper.Airbase#AIRBASE airbase The airbase, where the element arrived. +-- @param Wrapper.Airbase#AIRBASE.ParkingSpot Parking The Parking spot the element has. +function FLIGHTGROUP:onafterElementArrived(From, Event, To, Element, airbase, Parking) + self:T(self.lid..string.format("Element arrived %s at %s airbase using parking spot %d", Element.name, airbase and airbase:GetName() or "unknown", Parking and Parking.TerminalID or -99)) + + self:_SetElementParkingAt(Element, Parking) + + -- Set element status. + self:_UpdateStatus(Element, FLIGHTGROUP.ElementStatus.ARRIVED) +end + +--- On after "ElementDead" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #FLIGHTGROUP.Element Element The flight group element. +function FLIGHTGROUP:onafterElementDead(From, Event, To, Element) + self:T(self.lid..string.format("Element dead %s.", Element.name)) + + if self.flightcontrol and Element.parking then + self.flightcontrol:SetParkingFree(Element.parking) + end + + -- Not parking any more. + Element.parking=nil + + -- Set element status. + self:_UpdateStatus(Element, FLIGHTGROUP.ElementStatus.DEAD) +end + + +--- On after "FlightSpawned" event. Sets the template, initializes the waypoints. +-- @param #FLIGHTGROUP self +-- @param Wrapper.Group#GROUP Group Flight group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterFlightSpawned(From, Event, To) + self:I(self.lid..string.format("Flight spawned!")) + + if self.ai then + + -- Set default ROE and ROT options. + self:SetOptionROE() + self:SetOptionROT() + + -- TODO: make this input. + self.group:SetOption(AI.Option.Air.id.PROHIBIT_JETT, true) + self.group:SetOption(AI.Option.Air.id.PROHIBIT_AB, true) -- Does not seem to work. AI still used the after burner. + self.group:SetOption(AI.Option.Air.id.RTB_ON_BINGO, false) + --self.group:SetOption(AI.Option.Air.id.RADAR_USING, AI.Option.Air.val.RADAR_USING.FOR_CONTINUOUS_SEARCH) + + -- Turn TACAN beacon on. + if self.tacanChannelDefault then + self:SwitchTACANOn(self.tacanChannelDefault, self.tacanMorseDefault) + end + + -- Turn on the radio. + if self.radioFreqDefault then + self:SwitchRadioOn(self.radioFreqDefault, self.radioModuDefault) + end + + else + + -- F10 other menu. + self:_UpdateMenu() + + end + +end + +--- On after "FlightParking" event. Add flight to flightcontrol of airbase. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterFlightParking(From, Event, To) + self:I(self.lid..string.format("Flight is parking")) + + local airbase=self.group:GetCoordinate():GetClosestAirbase() + + local airbasename=airbase:GetName() or "unknown" + + -- Parking time stamp. + self.Tparking=timer.getAbsTime() + + -- Get FC of this airbase. + local flightcontrol=_DATABASE:GetFlightControl(airbasename) + + if flightcontrol then + + -- Set FC for this flight + self:SetFlightControl(flightcontrol) + + if self.flightcontrol then + + -- Set flight status. + self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.PARKING) + + -- Update player menu. + if not self.ai then + self:_UpdateMenu(0.5) + end + + end + end +end + +--- On after "FlightTaxiing" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterFlightTaxiing(From, Event, To) + self:T(self.lid..string.format("Flight is taxiing")) + + -- Parking over. + self.Tparking=nil + + -- TODO: need a better check for the airbase. + local airbase=self.group:GetCoordinate():GetClosestAirbase(nil, self.group:GetCoalition()) + + if self.flightcontrol and airbase and self.flightcontrol.airbasename==airbase:GetName() then + + -- Add AI flight to takeoff queue. + if self.ai then + -- AI flights go directly to TAKEOFF as we don't know when they finished taxiing. + self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.TAKEOFF) + else + -- Human flights go to TAXI OUT queue. They will go to the ready for takeoff queue when they request it. + self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.TAXIOUT) + -- Update menu. + self:_UpdateMenu() + end + + end + +end + +--- On after "FlightTakeoff" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Airbase#AIRBASE airbase The airbase the flight landed. +function FLIGHTGROUP:onafterFlightTakeoff(From, Event, To, airbase) + self:T(self.lid..string.format("Flight takeoff from %s", airbase and airbase:GetName() or "unknown airbase")) + + -- Remove flight from all FC queues. + if self.flightcontrol and airbase and self.flightcontrol.airbasename==airbase:GetName() then + self.flightcontrol:_RemoveFlight(self) + self.flightcontrol=nil + end + +end + +--- On after "FlightAirborne" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterFlightAirborne(From, Event, To) + self:I(self.lid..string.format("Flight airborne")) + + if not self.ai then + self:_UpdateMenu() + end +end + +--- On after "FlightLanding" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterFlightLanding(From, Event, To) + self:T(self.lid..string.format("Flight is landing")) + + self:_SetElementStatusAll(FLIGHTGROUP.ElementStatus.LANDING) + +end + +--- On after "FlightLanded" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Airbase#AIRBASE airbase The airbase the flight landed. +function FLIGHTGROUP:onafterFlightLanded(From, Event, To, airbase) + self:T(self.lid..string.format("Flight landed at %s", airbase and airbase:GetName() or "unknown place")) + + if self:IsLandingAt() then + self:LandedAt() + else + if self.flightcontrol and airbase and self.flightcontrol.airbasename==airbase:GetName() then + -- Add flight to taxiinb queue. + self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.TAXIINB) + end + end +end + +--- On after "FlightArrived" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterFlightArrived(From, Event, To) + self:T(self.lid..string.format("Flight arrived")) + + -- Flight Control + if self.flightcontrol then + -- Add flight to arrived queue. + self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.ARRIVED) + end + + -- Stop and despawn in 5 min. + self:__Stop(5*60) +end + +--- On after "FlightDead" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterFlightDead(From, Event, To) + self:I(self.lid..string.format("Flight dead!")) + + -- Delete waypoints so they are re-initialized at the next spawn. + self.waypoints=nil + self.groupinitialized=false + + -- Remove flight from all FC queues. + if self.flightcontrol then + self.flightcontrol:_RemoveFlight(self) + self.flightcontrol=nil + end + + -- Cancel all mission. + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + self:MissionCancel(mission) + mission:FlightDead(self) + + end + + -- Stop + self:Stop() +end + + +--- On before "UpdateRoute" event. Update route of group, e.g after new waypoints and/or waypoint tasks have been added. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number n Waypoint number. +-- @return #boolean Transision allowed? +function FLIGHTGROUP:onbeforeUpdateRoute(From, Event, To, n) + + -- Is transition allowed? We assume yes until proven otherwise. + local allowed=true + local trepeat=nil + + if self:IsAlive() and (self:IsAirborne() or self:IsWaiting() or self:IsInbound() or self:IsHolding()) then + -- Alive & Airborne ==> Update route possible. + self:T3(self.lid.."Update route possible. Group is ALIVE and AIRBORNE or WAITING or INBOUND or HOLDING") + elseif self:IsDead() then + -- Group is dead! No more updates. + self:E(self.lid.."Update route denied. Group is DEAD!") + allowed=false + else + -- Not airborne yet. Try again in 1 sec. + self:T3(self.lid.."FF update route denied ==> checking back in 5 sec") + trepeat=-5 + allowed=false + end + + if n and n<1 then + self:E(self.lid.."Update route denied because waypoint n<1!") + allowed=false + end + + if not self.currentwp then + self:E(self.lid.."Update route denied because self.currentwp=nil!") + allowed=false + end + + local N=n or self.currentwp+1 + if not N or N<1 then + self:E(self.lid.."FF update route denied because N=nil or N<1") + trepeat=-5 + allowed=false + end + + if self.taskcurrent>0 then + self:E(self.lid.."Update route denied because taskcurrent>0") + allowed=false + end + + -- Not good, because mission will never start. Better only check if there is a current task! + --if self.currentmission then + --end + + -- Only AI flights. + if not self.ai then + allowed=false + end + + -- Debug info. + self:T2(self.lid..string.format("Onbefore Updateroute allowed=%s state=%s repeat in %s", tostring(allowed), self:GetState(), tostring(trepeat))) + + if trepeat then + self:__UpdateRoute(trepeat, n) + end + + return allowed +end + +--- On after "UpdateRoute" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number n Waypoint number. Default is next waypoint. +function FLIGHTGROUP:onafterUpdateRoute(From, Event, To, n) + + -- Update route from this waypoint number onwards. + n=n or self.currentwp+1 + + -- Update waypoint tasks, i.e. inject WP tasks into waypoint table. + self:_UpdateWaypointTasks() + + -- Waypoints. + local wp={} + + -- Current velocity. + local speed=self.group and self.group:GetVelocityKMH() or 100 + + -- Set current waypoint or we get problem that the _PassingWaypoint function is triggered too early, i.e. right now and not when passing the next WP. + local current=self.group:GetCoordinate():WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, speed, true, nil, {}, "Current") + table.insert(wp, current) + + -- Add remaining waypoints to route. + for i=n, #self.waypoints do + table.insert(wp, self.waypoints[i]) + end + + -- Debug info. + local hb=self.homebase and self.homebase:GetName() or "unknown" + local db=self.destbase and self.destbase:GetName() or "unknown" + self:T(self.lid..string.format("Updating route for WP #%d-%d homebase=%s destination=%s", n, #wp, hb, db)) + + + if #wp>1 then + + -- Route group to all defined waypoints remaining. + self:Route(wp, 1) + + else + + --- + -- No waypoints left + --- + + self:_CheckFlightDone() + + end + +end + +--- On after "Respawn" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #table Template The template used to respawn the group. +function FLIGHTGROUP:onafterRespawn(From, Event, To, Template) + + self:T(self.lid.."Respawning group!") + + local template=UTILS.DeepCopy(Template or self.template) + + if self.group and self.group:InAir() then + template.lateActivation=false + self.respawning=true + self.group=self.group:Respawn(template) + end + +end + +--- On after "PassingWaypoint" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number n Waypoint number passed. +-- @param #number N Final waypoint number. +function FLIGHTGROUP:onafterPassingWaypoint(From, Event, To, n, N) + local text=string.format("Flight passed waypoint %d/%d", n, N) + self:T(self.lid..text) + MESSAGE:New(text, 30, "DEBUG"):ToAllIf(self.Debug) + + -- Get all waypoint tasks. + local tasks=self:GetTasksWaypoint(n) + + -- Debug info. + local text=string.format("WP %d/%d tasks:", n, N) + if #tasks>0 then + for i,_task in pairs(tasks) do + local task=_task --#FLIGHTGROUP.Task + text=text..string.format("\n[%d] %s", i, task.description) + end + else + text=text.." None" + end + self:T(self.lid..text) + + + -- Tasks at this waypoints. + local taskswp={} + + -- TODO: maybe set waypoint enroute tasks? + + for _,task in pairs(tasks) do + local Task=task --#FLIGHTGROUP.Task + + -- Task execute. + table.insert(taskswp, self.group:TaskFunction("FLIGHTGROUP._TaskExecute", self, Task)) + + -- Stop condition if userflag is set to 1 or task duration over. + local TaskCondition=self.group:TaskCondition(nil, Task.stopflag:GetName(), 1, nil, Task.duration) + + -- Controlled task. + table.insert(taskswp, self.group:TaskControlled(Task.dcstask, TaskCondition)) + + -- Task done. + table.insert(taskswp, self.group:TaskFunction("FLIGHTGROUP._TaskDone", self, Task)) + + end + + -- Execute waypoint tasks. + if #taskswp>0 then + self:SetTask(self.group:TaskCombo(taskswp)) + end + + -- Final AIR waypoint reached? + if n==N then + + -- Set switch to true. + self.passedfinalwp=true + + -- Check if all tasks/mission are done? If so, RTB or WAIT. + -- Note, we delay it for a second to let the OnAfterPassingwaypoint function to be executed in case someone wants to add another waypoint there. + if #taskswp==0 then + self:_CheckFlightDone(1) + end + + end + +end + +--- Check if flight is done, i.e. +-- +-- * passed the final waypoint, +-- * no current task +-- * no current mission +-- * number of remaining tasks is zero +-- * number of remaining missions is zero +-- +-- @param #FLIGHTGROUP self +-- @param #number delay Delay in seconds. +function FLIGHTGROUP:_CheckFlightDone(delay) + + if self:IsAlive() and self.ai then + + if delay and delay>0 then + -- Delayed call. + self:ScheduleOnce(delay, FLIGHTGROUP._CheckFlightDone, self) + else + + -- First check if there is a paused mission that + if self.missionpaused then + self:UnpauseMission() + return + end + + + -- Number of tasks remaining. + local nTasks=self:CountRemainingTasks() + + -- Number of mission remaining. + local nMissions=self:CountRemainingMissison() + + -- Final waypoint passed? + if self.passedfinalwp then + + -- Got current mission or task? + if self.currentmission==nil and self.taskcurrent==0 then + + -- Number of remaining tasks/missions? + if nTasks==0 and nMissions==0 then + + -- Send flight to destination. + if self.destbase then + self:I(self.lid.."Passed Final WP and No current and/or future missions/task ==> RTB!") + self:__RTB(-3, self.destbase) + elseif self.destzone then + self:I(self.lid.."Passed Final WP and No current and/or future missions/task ==> RTZ!") + self:__RTZ(-3, self.destzone) + else + self:I(self.lid.."Passed Final WP and NO Tasks/Missions left. No DestBase or DestZone ==> Wait!") + self:__Wait(-1) + end + + else + self:I(self.lid..string.format("Passed Final WP but Tasks=%d or Missions=%d left in the queue. Wait!", nTasks, nMissions)) + self:__Wait(-1) + end + else + self:I(self.lid..string.format("Passed Final WP but still have current Task (#%s) or Mission (#%s) left to do", tostring(self.taskcurrent), tostring(self.currentmission))) + end + else + self:I(self.lid..string.format("Flight (status=%s) did NOT pass the final waypoint yet ==> update route", self:GetState())) + self:__UpdateRoute(-1) + end + end + + end + +end + +--- On after "GotoWaypoint" event. Group will got to the given waypoint and execute its route from there. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number n The goto waypoint number. +function FLIGHTGROUP:onafterGotoWaypoint(From, Event, To, n) + + -- The last waypoint passed was n-1 + self.currentwp=n-1 + + -- TODO: switch to re-enable waypoint tasks. + if false then + local tasks=self:GetTasksWaypoint(n) + + for _,_task in pairs(tasks) do + local task=_task --#FLIGHTGROUP.Task + task.status=FLIGHTGROUP.TaskStatus.SCHEDULED + end + + end + + -- Update the route. + self:UpdateRoute() + +end + +--- On before "RTB" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Airbase#AIRBASE airbase The airbase to hold at. +-- @param #number SpeedTo Speed used for travelling from current position to holding point in knots. +-- @param #number SpeedHold Holding speed in knots. +function FLIGHTGROUP:onbeforeRTB(From, Event, To, airbase, SpeedTo, SpeedHold) + + if self:IsAlive() then + + local allowed=true + local Tsuspend=nil + + if airbase==nil then + self:E(self.lid.."ERROR: Airbase is nil in RTB() call!") + allowed=false + end + + -- Check that coaliton is okay. We allow same (blue=blue, red=red) or landing on neutral bases. + if airbase and airbase:GetCoalition()~=self.group:GetCoalition() and airbase:GetCoalition()>0 then + self:E(self.lid..string.format("ERROR: Wrong airbase coalition %d in RTB() call! We allow only same as group %d or neutral airbases 0.", airbase:GetCoalition(), self.group:GetCoalition())) + allowed=false + end + + if not self.group:IsAirborne(true) then + self:I(self.lid..string.format("WARNING: Group is not AIRBORNE ==> RTB event is suspended for 10 sec.")) + allowed=false + Tsuspend=-10 + end + + -- Only if fuel is not low or critical. + if not (self:IsFuelLow() or self:IsFuelCritical()) then + + -- Check if there are remaining tasks. + local Ntot,Nsched, Nwp=self:CountRemainingTasks() + + if self.taskcurrent>0 then + self:I(self.lid..string.format("WARNING: Got current task ==> RTB event is suspended for 10 sec.")) + Tsuspend=-10 + allowed=false + end + + if Nsched>0 then + self:I(self.lid..string.format("WARNING: Still got %d SCHEDULED tasks in the queue ==> RTB event is suspended for 10 sec.", Nsched)) + Tsuspend=-10 + allowed=false + end + + if Nwp>0 then + self:I(self.lid..string.format("WARNING: Still got %d WAYPOINT tasks in the queue ==> RTB event is suspended for 10 sec.", Nwp)) + Tsuspend=-10 + allowed=false + end + + end + + if Tsuspend and not allowed then + self:__RTB(Tsuspend, airbase, SpeedTo, SpeedHold) + end + + return allowed + + else + self:E(self.lid.."WARNING: Group is not alive! RTB call not allowed.") + return false + end + +end + +--- On after "RTB" event. Order flight to hold at an airbase and wait for signal to land. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Airbase#AIRBASE airbase The airbase to hold at. +-- @param #number SpeedTo Speed used for traveling from current position to holding point in knots. Default 75% of max speed. +-- @param #number SpeedHold Holding speed in knots. Default 250 kts. +-- @param #number SpeedLand Landing speed in knots. Default 170 kts. +function FLIGHTGROUP:onafterRTB(From, Event, To, airbase, SpeedTo, SpeedHold, SpeedLand) + + self:I(self.lid..string.format("RTB: event=%s: %s --> %s to %s", Event, From, To, airbase:GetName())) + + -- Set the destination base. + self.destbase=airbase + + -- Clear holding time in any case. + self.Tholding=nil + + -- Defaults: + SpeedTo=SpeedTo or UTILS.KmphToKnots(self.speedCruise) + SpeedHold=SpeedHold or (self.ishelo and 80 or 250) + SpeedLand=SpeedLand or (self.ishelo and 40 or 170) + + -- Debug message. + local text=string.format("Flight group set to hold at airbase %s. SpeedTo=%d, SpeedHold=%d, SpeedLand=%d", airbase:GetName(), SpeedTo, SpeedHold, SpeedLand) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + local althold=self.ishelo and 1000+math.random(10)*100 or math.random(4,10)*1000 + + -- Holding points. + local c0=self.group:GetCoordinate() + local p0=airbase:GetZone():GetRandomCoordinate():SetAltitude(UTILS.FeetToMeters(althold)) + local p1=nil + local wpap=nil + + -- Do we have a flight control? + local fc=_DATABASE:GetFlightControl(airbase:GetName()) + if fc then + -- Get holding point from flight control. + local HoldingPoint=fc:_GetHoldingpoint(self) + p0=HoldingPoint.pos0 + p1=HoldingPoint.pos1 + + -- Debug marks. + if self.Debug then + p0:MarkToAll("Holding point P0") + p1:MarkToAll("Holding point P1") + end + + -- Set flightcontrol for this flight. + self:SetFlightControl(fc) + + -- Add flight to inbound queue. + self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.INBOUND) + end + + -- Altitude above ground for a glide slope of 3 degrees. + local x1=self.ishelo and UTILS.NMToMeters(5.0) or UTILS.NMToMeters(10) + local x2=self.ishelo and UTILS.NMToMeters(2.5) or UTILS.NMToMeters(5) + local alpha=math.rad(3) + local h1=x1*math.tan(alpha) + local h2=x2*math.tan(alpha) + + local runway=airbase:GetActiveRunway() + + -- Set holding flag to 0=false. + self.flaghold:Set(0) + + local holdtime=5*60 + if fc or self.airboss then + holdtime=nil + end + + -- Task fuction when reached holding point. + local TaskArrived=self.group:TaskFunction("FLIGHTGROUP._ReachedHolding", self) + + -- Orbit until flaghold=1 (true) but max 5 min if no FC is giving the landing clearance. + local TaskOrbit = self.group:TaskOrbit(p0, nil, UTILS.KnotsToMps(SpeedHold), p1) + local TaskLand = self.group:TaskCondition(nil, self.flaghold.UserFlagName, 1, nil, holdtime) + local TaskHold = self.group:TaskControlled(TaskOrbit, TaskLand) + local TaskKlar = self.group:TaskFunction("FLIGHTGROUP._ClearedToLand", self) -- Once the holding flag becomes true, set trigger FLIGHTLANDING, i.e. set flight STATUS to LANDING. + + -- Waypoints from current position to holding point. + local wp={} + wp[#wp+1]=c0:WaypointAir(nil, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(SpeedTo), true , nil, {}, "Current Pos") + wp[#wp+1]=p0:WaypointAir(nil, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(SpeedTo), true , nil, {TaskArrived, TaskHold, TaskKlar}, "Holding Point") + + -- Approach point: 10 NN in direction of runway. + if airbase:GetAirbaseCategory()==Airbase.Category.AIRDROME then + + --- + -- Airdrome + --- + + local papp=airbase:GetCoordinate():Translate(x1, runway.heading-180):SetAltitude(h1) + wp[#wp+1]=papp:WaypointAirTurningPoint(nil, UTILS.KnotsToKmph(SpeedLand), {}, "Final Approach") + + -- Okay, it looks like it's best to specify the coordinates not at the airbase but a bit away. This causes a more direct landing approach. + local pland=airbase:GetCoordinate():Translate(x2, runway.heading-180):SetAltitude(h2) + wp[#wp+1]=pland:WaypointAirLanding(UTILS.KnotsToKmph(SpeedLand), airbase, {}, "Landing") + + elseif airbase:GetAirbaseCategory()==Airbase.Category.SHIP then + + --- + -- Ship + --- + + local pland=airbase:GetCoordinate() + wp[#wp+1]=pland:WaypointAirLanding(UTILS.KnotsToKmph(SpeedLand), airbase, {}, "Landing") + + end + + if self.ai then + + local routeto=false + if fc or world.event.S_EVENT_KILL then + routeto=true + end + + -- Clear all tasks. + self:ClearTasks() + + -- Respawn? + if routeto then + + --self:I(self.lid.."FF route (not repawn)") + + -- Just route the group. Respawn might happen when going from holding to final. + self:Route(wp, 1) + + else + + --self:I(self.lid.."FF respawn (not route)") + + -- Get group template. + local Template=self.group:GetTemplate() + + -- Set route points. + Template.route.points=wp + + --Respawn the group with new waypoints. + self:Respawn(Template) + + end + + end + +end + +--- On before "Wait" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Coord Coordinate where to orbit. Default current position. +-- @param #number Altitude Altitude in feet. Default 10000 ft. +-- @param #number Speed Speed in knots. Default 250 kts. +function FLIGHTGROUP:onbeforeWait(From, Event, To, Coord, Altitude, Speed) + + local allowed=true + local Tsuspend=nil + + -- Check if there are remaining tasks. + local Ntot,Nsched, Nwp=self:CountRemainingTasks() + + if self.taskcurrent>0 then + self:I(self.lid..string.format("WARNING: Got current task ==> WAIT event is suspended for 10 sec.")) + Tsuspend=-10 + allowed=false + end + + if Nsched>0 then + self:I(self.lid..string.format("WARNING: Still got %d SCHEDULED tasks in the queue ==> WAIT event is suspended for 10 sec.", Nsched)) + Tsuspend=-10 + allowed=false + end + + if Nwp>0 then + self:I(self.lid..string.format("WARNING: Still got %d WAYPOINT tasks in the queue ==> WAIT event is suspended for 10 sec.", Nwp)) + Tsuspend=-10 + allowed=false + end + + if Tsuspend and not allowed then + self:__Wait(Tsuspend, Coord, Altitude, Speed) + end + + return allowed +end + + +--- On after "Wait" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Coord Coordinate where to orbit. Default current position. +-- @param #number Altitude Altitude in feet. Default 10000 ft. +-- @param #number Speed Speed in knots. Default 250 kts. +function FLIGHTGROUP:onafterWait(From, Event, To, Coord, Altitude, Speed) + + Coord=Coord or self.group:GetCoordinate() + Altitude=Altitude or (self.ishelo and 1000 or 10000) + Speed=Speed or (self.ishelo and 80 or 250) + + -- Debug message. + local text=string.format("Flight group set to wait/orbit at altitude %d m and speed %.1f km/h", Altitude, Speed) + MESSAGE:New(text, 30, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + --TODO: set ROE passive. introduce roe event/state/variable. + + -- Orbit task. + local TaskOrbit=self.group:TaskOrbit(Coord, UTILS.FeetToMeters(Altitude), UTILS.KnotsToMps(Speed)) + + -- Set task. + self:SetTask(TaskOrbit) + +end + + +--- On after "Refuel" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Coordinate The coordinate. +function FLIGHTGROUP:onafterRefuel(From, Event, To, Coordinate) + + -- Debug message. + local text=string.format("Flight group set to refuel at the nearest tanker") + MESSAGE:New(text, 30, "DEBUG"):ToAllIf(self.Debug) + self:I(self.lid..text) + + --TODO: set ROE passive. introduce roe event/state/variable. + + --TODO: cancel current task + + self:PauseMission() + + -- Refueling task. + local TaskRefuel=self.group:TaskRefueling() + local TaskFunction=self.group:TaskFunction("FLIGHTGROUP._FinishedRefuelling", self) + local DCSTasks={TaskRefuel, TaskFunction} + + local Speed=self.speedCruise + + local coordinate=self.group:GetCoordinate() + + Coordinate=Coordinate or coordinate:Translate(UTILS.NMToMeters(5), self.group:GetHeading(), true) + + local wp0=coordinate:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, Speed, true) + local wp9=Coordinate:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, Speed, true, nil, DCSTasks, "Refuel") + + self:Route({wp0, wp9}) + +end + +--- On after "Refueled" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterRefueled(From, Event, To) + -- Debug message. + local text=string.format("Flight group finished refuelling") + MESSAGE:New(text, 30, "DEBUG"):ToAllIf(self.Debug) + self:I(self.lid..text) + + -- Check if flight is done. + self:_CheckFlightDone(1) + +end + + +--- On after "Holding" event. Flight arrived at the holding point. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterHolding(From, Event, To) + + -- Set holding flag to 0 (just in case). + self.flaghold:Set(0) + + -- Holding time stamp. + self.Tholding=timer.getAbsTime() + + local text=string.format("Flight group %s is HOLDING now", self.groupname) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:I(self.lid..text) + + -- Add flight to waiting/holding queue. + if self.flightcontrol then + + -- Set flight status to holding + self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.HOLDING) + + if not self.ai then + self:_UpdateMenu() + end + + elseif self.airboss then + + if self.ishelo then + + local carrierpos=self.airboss:GetCoordinate() + local carrierheading=self.airboss:GetHeading() + + local Distance=UTILS.NMToMeters(5) + local Angle=carrierheading+90 + local altitude=math.random(12, 25)*100 + local oc=carrierpos:Translate(Distance,Angle):SetAltitude(altitude, true) + + -- Orbit until flaghold=1 (true) but max 5 min if no FC is giving the landing clearance. + local TaskOrbit=self.group:TaskOrbit(oc, nil, UTILS.KnotsToMps(50)) + local TaskLand=self.group:TaskCondition(nil, self.flaghold.UserFlagName, 1) + local TaskHold=self.group:TaskControlled(TaskOrbit, TaskLand) + local TaskKlar=self.group:TaskFunction("FLIGHTGROUP._ClearedToLand", self) -- Once the holding flag becomes true, set trigger FLIGHTLANDING, i.e. set flight STATUS to LANDING. + + local DCSTask=self.group:TaskCombo({TaskOrbit, TaskHold, TaskKlar}) + + self:SetTask(DCSTask) + end + + end + +end + +--- On after "EngageTargets" event. Order to engage a set of units. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Set#SET_UNIT TargetUnitSet +function FLIGHTGROUP:onafterEngageTargets(From, Event, To, TargetUnitSet) + + local DCSTasks={} + + for _,_unit in paris(TargetUnitSet:GetSet()) do + local unit=_unit --Wrapper.Unit#UNIT + local task=self.group:TaskAttackUnit(unit, true) + table.insert(DCSTasks) + end + + -- Task combo. + local DCSTask=self.group:TaskCombo(DCSTasks) + + --TODO needs a task function that calls EngageDone or so event and updates the route again. + + -- Lets try if pushtask actually leaves the remaining tasks untouched. + self:SetTask(DCSTask) + +end + +--- On before "LandAt" event. Check we have a helo group. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Coordinate The coordinate where to land. Default is current position. +-- @param #number Duration The duration in seconds to remain on ground. Default 600 sec (10 min). +function FLIGHTGROUP:onbeforeLandAt(From, Event, To, Coordinate, Duration) + return self.ishelo +end + +--- On after "LandAt" event. Order helicopter to land at a specific point. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Coordinate The coordinate where to land. Default is current position. +-- @param #number Duration The duration in seconds to remain on ground. Default 600 sec (10 min). +function FLIGHTGROUP:onafterLandAt(From, Event, To, Coordinate, Duration) + + -- Duration. + Duration=Duration or 600 + + Coordinate=Coordinate or self:GetCoordinate() + + local DCStask=self.group:TaskLandAtVec2(Coordinate:GetVec2(), Duration) + + local Task=self:NewTaskScheduled(DCStask, 1, "Task_Land_At", 0) + + -- Add task with high priority. + --self:AddTask(task, 1, "Task_Land_At", 0) + + self:TaskExecute(Task) + +end + +--- On after "DetectedUnit" event. Add newly detected unit to detected units set. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Unit#UNIT Unit The detected unit. +function FLIGHTGROUP:onafterDetectedUnit(From, Event, To, Unit) + self:T2(self.lid..string.format("Detected unit %s", Unit:GetName())) + self.detectedunits:AddUnit(Unit) +end + +--- On after "DetectedUnitNew" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Unit#UNIT Unit The detected unit. +function FLIGHTGROUP:onafterDetectedUnitNew(From, Event, To, Unit) + self:T(self.lid..string.format("Detected New unit %s", Unit:GetName())) +end + + +--- On after "FuelLow" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterFuelLow(From, Event, To) + + -- Debug message. + local text=string.format("Low fuel for flight group %s", self.groupname) + MESSAGE:New(text, 30, "DEBUG"):ToAllIf(self.Debug) + self:I(self.lid..text) + + -- Set switch to true. + self.fuellow=true + + -- Back to destination or home. + local airbase=self.destbase or self.homebase + + if self.airwing then + + -- Get closest tanker from airwing that can refuel this flight. + local tanker=self.airwing:GetTankerForFlight(self) + + if tanker then + + -- Send flight to tanker with refueling task. + self:Refuel(tanker.flightgroup:GetCoordinate()) + + else + + if airbase and self.fuellowrtb then + self:RTB(airbase) + --TODO: RTZ + end + + end + + else + + if self.fuellowrefuel and self.refueltype then + + local tanker=self:FindNearestTanker(50) + + if tanker then + + self:I(self.lid..string.format("Send to refuel at tanker %s", tanker:GetName())) + + self:Refuel() + + return + end + end + + if airbase and self.fuellowrtb then + self:RTB(airbase) + --TODO: RTZ + end + + end + +end + +--- On after "FuelCritical" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterFuelCritical(From, Event, To) + + -- Debug message. + local text=string.format("Critical fuel for flight group %s", self.groupname) + MESSAGE:New(text, 30, "DEBUG"):ToAllIf(self.Debug) + self:I(self.lid..text) + + -- Set switch to true. + self.fuelcritical=true + + -- Airbase. + local airbase=self.destbase or self.homebase + + if airbase and self.fuelcriticalrtb and not self:IsGoing4Fuel() then + self:RTB(airbase) + --TODO: RTZ + end +end + + +--- On after "EnterZone" event. Sets self.inzones[zonename]=true. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Zone#ZONE Zone The zone that the group entered. +function FLIGHTGROUP:onafterEnterZone(From, Event, To, Zone) + local zonename=Zone and Zone:GetName() or "unknown" + self:T2(self.lid..string.format("Entered Zone %s", zonename)) + self.inzones:Add(Zone:GetName(), Zone) +end + +--- On after "LeaveZone" event. Sets self.inzones[zonename]=false. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Zone#ZONE Zone The zone that the group entered. +function FLIGHTGROUP:onafterLeaveZone(From, Event, To, Zone) + local zonename=Zone and Zone:GetName() or "unknown" + self:T2(self.lid..string.format("Left Zone %s", zonename)) + self.inzones:Remove(zonename, true) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Task functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get the unfinished waypoint tasks +-- @param #FLIGHTGROUP self +-- @param #number n Waypoint index. Counting starts at one. +-- @return #table Table of tasks. Table could also be empty {}. +function FLIGHTGROUP:GetTasksWaypoint(n) + + -- Tasks table. + local tasks={} + + -- Sort queue. + self:_SortTaskQueue() + + -- Look for first task that SCHEDULED. + for _,_task in pairs(self.taskqueue) do + local task=_task --#FLIGHTGROUP.Task + if task.type==FLIGHTGROUP.TaskType.WAYPOINT and task.status==FLIGHTGROUP.TaskStatus.SCHEDULED and task.waypoint==n then + table.insert(tasks, task) + end + end + + return tasks +end + +--- Sort task queue. +-- @param #FLIGHTGROUP self +function FLIGHTGROUP:_SortTaskQueue() + + -- Sort results table wrt prio and then start time. + local function _sort(a, b) + local taskA=a --#FLIGHTGROUP.Task + local taskB=b --#FLIGHTGROUP.Task + return (taskA.prio0 then + self:TaskCancel() + end + + -- Set current task. + self.taskcurrent=Task.id + + -- Set time stamp. + Task.timestamp=timer.getAbsTime() + + -- Task status executing. + Task.status=FLIGHTGROUP.TaskStatus.EXECUTING + + if Task.dcstask.id=="Formation" then + + -- Set of group(s) to follow Mother. + local followSet=SET_GROUP:New():AddGroup(self.group) + + local param=Task.dcstask.params + + local followUnit=UNIT:FindByName(param.unitname) + + -- Define AI Formation object. + Task.formation=AI_FORMATION:New(followUnit, followSet, "Formation", "Follow X at given parameters.") + + -- Formation parameters. + Task.formation:FormationCenterWing(-param.offsetX, 50, math.abs(param.altitude), 50, param.offsetZ, 50) + + -- Set follow time interval. + Task.formation:SetFollowTimeInterval(param.dtFollow) + + -- Formation mode. + Task.formation:SetFlightModeFormation(self.group) + + -- Start formation FSM. + Task.formation:Start() + + else + + -- If task is scheduled (not waypoint) set task. + if Task.type==FLIGHTGROUP.TaskType.SCHEDULED then + + local DCStasks={} + if Task.dcstask.id=='ComboTask' then + -- Loop over all combo tasks. + for TaskID, Task in ipairs(Task.dcstask.params.tasks) do + table.insert(DCStasks, Task) + end + else + table.insert(DCStasks, Task.dcstask) + end + + -- Combo task. + local TaskCombo=self.group:TaskCombo(DCStasks) + + -- Stop condition! + local TaskCondition=self.group:TaskCondition(nil, Task.stopflag:GetName(), 1, nil, Task.duration) + + -- Controlled task. + local TaskControlled=self.group:TaskControlled(TaskCombo, TaskCondition) + + -- Task done. + local TaskDone=self.group:TaskFunction("FLIGHTGROUP._TaskDone", self, Task) + + -- Final task. + local TaskFinal=self.group:TaskCombo({TaskControlled, TaskDone}) + + -- Set task for group. + self:SetTask(TaskFinal, 1) + + end + + end + + -- Get mission of this task (if any). + local Mission=self:GetMissionByTaskID(self.taskcurrent) + if Mission then + -- Set AUFTRAG status. + self:MissionExecute(Mission) + end + +end + +--- On after "TaskPause" event. Pauses the current task. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterTaskPause(From, Event, To) + + local Task=self:GetTaskCurrent() + + if Task then + + -- Clear all tasks. + self:ClearTasks() + + -- Task status executing. + Task.status=FLIGHTGROUP.TaskStatus.PAUSED + + end + +end + +--- On after "TaskUnPause" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterTaskUnpause(From, Event, To) + + --self.task +end + +--- On after "TaskCancel" event. Cancels the current task or simply sets the status to DONE if the task is not the current one. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #FLIGHTGROUP.Task Task The task to cancel. Default is the current task (if any). +function FLIGHTGROUP:onafterTaskCancel(From, Event, To, Task) + + -- Get current task. + local currenttask=self:GetTaskCurrent() + + -- If no task, we take the current task. But this could also be *nil*! + Task=Task or currenttask + + if Task then + + -- Check if the task is the current task? + if currenttask and Task.id==currenttask.id then + + -- Current stop flag value. I noticed cases, where setting the flag to 1 would not cancel the task, e.g. when firing HARMS on a dead ship. + local stopflag=Task.stopflag:Get() + + -- Debug info. + local text=string.format("Current task %s ID=%d cancelled (flag %s=%d)", Task.description, Task.id, Task.stopflag:GetName(), stopflag) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:I(self.lid..text) + + -- Set stop flag. When the flag is true, the _TaskDone function is executed and calls :TaskDone() + Task.stopflag:Set(1) + + if Task.dcstask.id=="Formation" then + Task.formation:Stop() + self:TaskDone(Task) + elseif stopflag==1 then + -- Manual call TaskDone if setting flag to one was not successful. + self:TaskDone(Task) + end + + else + + -- Debug info. + self:I(self.lid..string.format("TaskCancel: Setting task %s ID=%d to DONE", Task.description, Task.id)) + + -- Call task done function. + self:TaskDone(Task) + + + --[[ + local mission=self:GetMissionByTaskID(Task.id) + + -- Is this a waypoint task? + if Task.type==FLIGHTGROUP.TaskType.WAYPOINT and Task.waypoint then + + -- Check that this is a mission waypoint and no other tasks are defined here. + if mission and #self:GetTasksWaypoint(Task.waypoint)==0 then + self:RemoveWaypoint(Task.waypoint) + end + end + ]] + end + + else + + local text=string.format("WARNING: No (current) task to cancel!") + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:E(self.lid..text) + + end + +end + +--- On before "TaskDone" event. Deny transition if task status is PAUSED. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #FLIGHTGROUP.Task Task +function FLIGHTGROUP:onbeforeTaskDone(From, Event, To, Task) + + local allowed=true + + if Task.status==FLIGHTGROUP.TaskStatus.PAUSED then + allowed=false + end + + return allowed +end + +--- On after "TaskDone" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #FLIGHTGROUP.Task Task +function FLIGHTGROUP:onafterTaskDone(From, Event, To, Task) + + -- Debug message. + local text=string.format("Task done: %s ID=%d", Task.description, Task.id) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:I(self.lid..text) + + -- No current task. + if Task.id==self.taskcurrent then + self.taskcurrent=0 + end + + -- Task status done. + Task.status=FLIGHTGROUP.TaskStatus.DONE + + -- Check if this task was the task of the current mission ==> Mission Done! + local Mission=self:GetMissionByTaskID(Task.id) + + if Mission and Mission:IsNotOver() then + + local status=Mission:GetFlightStatus(self) + + if status~=AUFTRAG.FlightStatus.PAUSED then + self:I(self.lid.."FF Task Done ==> Mission Done!") + self:MissionDone(Mission) + else + --Mission paused. Do nothing! + end + else + self:I(self.lid.."FF Task Done but NO mission found ==> _CheckFlightDone in 1 sec") + self:_CheckFlightDone(1) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Mission functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On before "MissionStart" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Auftrag#AUFTRAG Mission The mission table. +function FLIGHTGROUP:onbeforeMissionStart(From, Event, To, Mission) + + -- Debug info. + self:I(self.lid..string.format("Starting mission %s, FSM=%s, LateActivated=%s, UnControlled=%s", tostring(Mission.name), self:GetState(), tostring(self:IsLateActivated()), tostring(self:IsUncontrolled()))) + + -- Delay for route to mission. Group needs to be activated and controlled. + local delay=1 + + -- Check if group is spawned. + if self:IsInUtero() then + + -- Activate group if it is late activated. + if self:IsLateActivated() then + self:Activate(delay) + delay=delay+1 + end + + end + + -- Startup group if it is uncontrolled. + if self:IsParking() and self:IsUncontrolled() then + self:StartUncontrolled(delay) + else + --env.info("FF hallo!") + --self:StartUncontrolled(5) + end + + return true +end + +--- On after "MissionStart" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Auftrag#AUFTRAG Mission The mission table. +function FLIGHTGROUP:onafterMissionStart(From, Event, To, Mission) + + -- Debug output. + local text=string.format("Starting %s Mission %s, target %s", Mission.type, tostring(Mission.name), Mission:GetTargetName()) + self:T(self.lid..text) + MESSAGE:New(text, 30, self.groupname):ToAllIf(true) + + -- Set current mission. + self.currentmission=Mission.auftragsnummer + + -- Set flight mission status to STARTED. + Mission:SetFlightStatus(self, AUFTRAG.FlightStatus.STARTED) + + -- Set mission status to STARTED. + Mission:Started() + + -- Route flight to mission zone. + self:RouteToMission(Mission, 3) + +end + +--- On after "MissionExecute" event. Mission execution began. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Auftrag#AUFTRAG Mission The mission table. +function FLIGHTGROUP:onafterMissionExecute(From, Event, To, Mission) + + local text=string.format("Executing %s Mission %s, target %s", Mission.type, tostring(Mission.name), Mission:GetTargetName()) + self:T(self.lid..text) + MESSAGE:New(text, 30, self.groupname):ToAllIf(true) + + -- Set flight mission status to EXECUTING. + Mission:SetFlightStatus(self, AUFTRAG.FlightStatus.EXECUTING) + + -- Set mission status to EXECUTING. + Mission:Executing() + + -- Formation + if Mission.optionFormation then + self:SwitchFormation(Mission.optionFormation) + end + +end + +--- On after "PauseMission" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterPauseMission(From, Event, To) + + local Mission=self:GetMissionCurrent() + + if Mission then + + -- Set flight mission status to PAUSED. + Mission:SetFlightStatus(self, AUFTRAG.FlightStatus.PAUSED) + + -- Get mission waypoint task. + local Task=Mission:GetFlightWaypointTask(self) + + -- Debug message. + self:I(self.lid..string.format("FF pausing current mission %s. Task=%s", tostring(Mission.name), tostring(Task and Task.description or "WTF"))) + + -- Cancelling the mission is actually cancelling the current task. + self:TaskCancel(Task) + + -- Set mission to pause so we can unpause it later. + self.missionpaused=Mission + + end + +end + +--- On after "UnpauseMission" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterUnpauseMission(From, Event, To) + + self:I(self.lid..string.format("Unpausing mission")) + + if self.missionpaused then + + local mission=self:GetMissionByID(self.missionpaused.auftragsnummer) + + self:MissionStart(mission) + + self.missionpaused=nil + else + self:E(self.lid.."ERROR: No mission to unpause!") + end + +end + + +--- On after "MissionCancel" event. Cancels the mission. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Auftrag#AUFTRAG Mission The mission to be cancelled. +function FLIGHTGROUP:onafterMissionCancel(From, Event, To, Mission) + + if self.currentmission and Mission.auftragsnummer==self.currentmission then + + -- Get mission waypoint task. + local Task=Mission:GetFlightWaypointTask(self) + + -- Debug info. + self:I(self.lid..string.format("FF Cancel current mission %s. Task=%s", tostring(Mission.name), tostring(Task and Task.description or "WTF"))) + + -- Cancelling the mission is actually cancelling the current task. + -- Note that two things can happen. + -- 1.) Flight is still on the way to the waypoint (status should be STARTED). In this case there would not be a current task! + -- 2.) Flight already passed the mission waypoint (status should be EXECUTING). + + self:TaskCancel(Task) + + else + + -- Not the current mission. + -- TODO: remove mission from queue? + + -- Set mission flight status. + Mission:SetFlightStatus(self, AUFTRAG.FlightStatus.CANCELLED) + + -- Send flight RTB or WAIT if nothing left to do. + self:_CheckFlightDone(1) + + end + +end + +--- On after "MissionDone" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Auftrag#AUFTRAG Mission +function FLIGHTGROUP:onafterMissionDone(From, Event, To, Mission) + + -- Debug info. + local text=string.format("Mission %s DONE!", Mission.name) + self:I(self.lid..text) + MESSAGE:New(text, 30, self.groupname):ToAllIf(true) + + -- Set Flight status. + Mission:SetFlightStatus(self, AUFTRAG.FlightStatus.DONE) + + -- Set current mission to nil. + if self.currentmission and Mission.auftragsnummer==self.currentmission then + self.currentmission=nil + end + + -- Remove mission waypoint. + local wpidx=Mission:GetFlightWaypointIndex(self) + if wpidx then + self:RemoveWaypoint(wpidx) + end + + -- Decrease patrol data. + if Mission.patroldata then + Mission.patroldata.noccupied=Mission.patroldata.noccupied-1 + AIRWING.UpdatePatrolPointMarker(Mission.patroldata) + end + + -- TODO: reset mission specific parameters like radio, ROE etc. + + -- Check if flight is done. + self:_CheckFlightDone(1) + +end + +--- Route group to mission. +-- @param #FLIGHTGROUP self +-- @param Ops.Auftrag#AUFTRAG mission The mission table. +-- @param #number delay Delay in seconds. +function FLIGHTGROUP:RouteToMission(mission, delay) + + if delay and delay>0 then + -- Delayed call. + self:ScheduleOnce(delay, FLIGHTGROUP.RouteToMission, self, mission) + else + + -- Next waypoint. + local nextwaypoint=self.currentwp+1 + + -- Create waypoint coordinate half way between us and the target. + local waypointcoord=self.group:GetCoordinate():GetIntermediateCoordinate(mission:GetTargetCoordinate(), mission.missionFraction) + local alt=waypointcoord.y + + -- Add some randomization. + waypointcoord=ZONE_RADIUS:New("Temp", waypointcoord:GetVec2(), 1000):GetRandomCoordinate():SetAltitude(alt, false) + + -- Set altitude of mission waypoint. + if mission.missionAltitude then + waypointcoord:SetAltitude(mission.missionAltitude, true) + end + env.info(string.format("FF mission alt=%d", waypointcoord.y)) + + -- Add enroute tasks. + for _,task in pairs(mission.enrouteTasks) do + self:AddTaskEnroute(task) + end + + -- Add waypoint. + self:AddWaypointAir(waypointcoord, nextwaypoint, UTILS.KmphToKnots(self.speedCruise), false) + + -- Special for Troop transport. + if mission.type==AUFTRAG.Type.TROOPTRANSPORT then + + -- Refresh DCS task with the known controllable. + mission.DCStask=mission:GetDCSMissionTask(self.group) + + -- Add task to embark for the troops. + for _,_group in pairs(mission.transportGroupSet.Set) do + local group=_group --Wrapper.Group#GROUP + + if group and group:IsAlive() then + local DCSTask=group:TaskEmbarkToTransport(mission.transportPickup, 500) + group:SetTask(DCSTask, 5) + end + + end + + end + + -- Add waypoint task. UpdateRoute is called inside. + local waypointtask=self:AddTaskWaypoint(mission.DCStask, nextwaypoint, mission.name, mission.prio, mission.duration) + + -- Set waypoint task. + mission:SetFlightWaypointTask(self, waypointtask) + + -- Set waypoint index. + mission:SetFlightWaypointIndex(self, nextwaypoint) + + --- + -- Mission Specific Settings + --- + + -- ROE + if mission.optionROE then + self:SetOptionROE(mission.optionROE) + end + -- ROT + if mission.optionROT then + self:SetOptionROT(mission.optionROT) + end + -- Radio + if mission.radioFreq then + self:SwitchRadioOn(mission.radioFreq, mission.radioModu) + end + -- TACAN + if mission.tacanChannel then + self:SwitchTACANOn(mission.tacanChannel, mission.tacanMorse) + end + -- Formation + if mission.optionFormation then + self:SwitchFormation(mission.optionFormation) + end + + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Special Task Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Function called when a task is executed. +--@param Wrapper.Group#GROUP group Group which should execute the task. +--@param #FLIGHTGROUP flightgroup Flight group. +--@param #FLIGHTGROUP.Task task Task. +function FLIGHTGROUP._TaskExecute(group, flightgroup, task) + + -- Debug message. + local text=string.format("_TaskExecute %s", task.description) + flightgroup:T3(flightgroup.lid..text) + + -- Set current task to nil so that the next in line can be executed. + if flightgroup then + flightgroup:TaskExecute(task) + end +end + +--- Function called when a task is done. +--@param Wrapper.Group#GROUP group Group for which the task is done. +--@param #FLIGHTGROUP flightgroup Flight group. +--@param #FLIGHTGROUP.Task task Task. +function FLIGHTGROUP._TaskDone(group, flightgroup, task) + + -- Debug message. + local text=string.format("_TaskDone %s", task.description) + flightgroup:T3(flightgroup.lid..text) + + -- Set current task to nil so that the next in line can be executed. + if flightgroup then + flightgroup:TaskDone(task) + end +end + +--- Function called when a group is passing a waypoint. +--@param Wrapper.Group#GROUP group Group that passed the waypoint +--@param #FLIGHTGROUP flightgroup Flightgroup object. +--@param #number i Waypoint number that has been reached. +function FLIGHTGROUP._PassingWaypoint(group, flightgroup, i) + + local final=#flightgroup.waypoints or 1 + + -- Debug message. + local text=string.format("Group passing waypoint %d of %d.", i, final) + flightgroup:T3(flightgroup.lid..text) + + -- Set current waypoint. + flightgroup.currentwp=i + + -- Trigger PassingWaypoint event. + flightgroup:PassingWaypoint(i, final) + +end + +--- Function called when flight has reached the holding point. +-- @param Wrapper.Group#GROUP group Group object. +-- @param #FLIGHTGROUP flightgroup Flight group object. +function FLIGHTGROUP._ReachedHolding(group, flightgroup) + flightgroup:I(flightgroup.lid..string.format("Group reached holding point")) + + -- Trigger Holding event. + flightgroup:__Holding(-1) +end + +--- Function called when flight has reached the holding point. +-- @param Wrapper.Group#GROUP group Group object. +-- @param #FLIGHTGROUP flightgroup Flight group object. +function FLIGHTGROUP._ClearedToLand(group, flightgroup) + flightgroup:I(flightgroup.lid..string.format("Group was cleared to land")) + + -- Trigger FlightLanding event. + flightgroup:__FlightLanding(-1) +end + +--- Function called when flight finished refuelling. +-- @param Wrapper.Group#GROUP group Group object. +-- @param #FLIGHTGROUP flightgroup Flight group object. +function FLIGHTGROUP._FinishedRefuelling(group, flightgroup) + flightgroup:T(flightgroup.lid..string.format("Group finished refueling")) + + -- Trigger Holding event. + flightgroup:__Refueled(-1) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Initialize group parameters. Also initializes waypoints if self.waypoints is nil. +-- @param #FLIGHTGROUP self +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:_InitGroup() + + -- First check if group was already initialized. + if self.groupinitialized then + self:E(self.lid.."WARNING: Group was already initialized!") + return + end + + -- Get template of group. + self.template=self.group:GetTemplate() + + -- Helo group. + self.ishelo=self.group:IsHelicopter() + + -- Is (template) group uncontrolled. + self.isUncontrolled=self.template.uncontrolled + + -- Is (template) group late activated. + self.isLateActivated=self.template.lateActivation + + -- Max speed in km/h. + self.speedmax=self.group:GetSpeedMax() + + -- Cruise speed limit 350 kts for fixed and 80 knots for rotary wings. + local speedCruiseLimit=self.ishelo and UTILS.KnotsToKmph(80) or UTILS.KnotsToKmph(350) + + -- Cruise speed: 70% of max speed but within limit. + self.speedCruise=math.min(self.speedmax*0.7, speedCruiseLimit) + + -- Group ammo. + self.ammo=self:GetAmmoTot() + + -- Initial fuel mass. + -- TODO: this is a unit property! + self.fuelmass=0 + + self.traveldist=0 + self.traveltime=timer.getAbsTime() + self.position=self:GetCoordinate() + + -- Radio parameters from template. + self.radioOn=self.template.communication + self.radioFreq=self.template.frequency + self.radioModu=self.template.modulation + + -- If not set by the use explicitly yet, we take the template values as defaults. + if not self.radioFreqDefault then + self.radioFreqDefault=self.radioFreq + self.radioModuDefault=self.radioModu + end + + -- Set default formation. + if not self.formationDefault then + if self.ishelo then + self.formationDefault=ENUMS.Formation.RotaryWing.EchelonLeft.D300 + else + self.formationDefault=ENUMS.Formation.FixedWing.EchelonLeft.Group + end + end + + self.ai=not self:_IsHuman(self.group) + + if not self.ai then + self.menu=self.menu or {} + self.menu.atc=self.menu.atc or {} + self.menu.atc.root=self.menu.atc.root or MENU_GROUP:New(self.group, "ATC") + end + + self:SwitchFormation(self.formationDefault) + + -- Get first unit. This is used to extract other parameters. + local unit=self.group:GetUnit(1) + + if unit then + + self.rangemax=unit:GetRange() + + self.descriptors=unit:GetDesc() + + self.actype=unit:GetTypeName() + + self.ceiling=self.descriptors.Hmax + + self.tankertype=select(2, unit:IsTanker()) + self.refueltype=select(2, unit:IsRefuelable()) + + -- Init waypoints. + if not self.waypoints then + self:InitWaypoints() + end + + -- Debug info. + local text=string.format("Initialized Flight Group %s:\n", self.groupname) + text=text..string.format("AC type = %s\n", self.actype) + text=text..string.format("Speed max = %.1f Knots\n", UTILS.KmphToKnots(self.speedmax)) + text=text..string.format("Range max = %.1f km\n", self.rangemax/1000) + text=text..string.format("Ceiling = %.1f feet\n", UTILS.MetersToFeet(self.ceiling)) + text=text..string.format("Tanker type = %s\n", tostring(self.tankertype)) + text=text..string.format("Refuel type = %s\n", tostring(self.refueltype)) + text=text..string.format("AI = %s\n", tostring(self.ai)) + text=text..string.format("Helicopter = %s\n", tostring(self.group:IsHelicopter())) + text=text..string.format("Elements = %d\n", #self.elements) + text=text..string.format("Waypoints = %d\n", #self.waypoints) + text=text..string.format("Radio = %.1f MHz %s %s\n", self.radioFreq, UTILS.GetModulationName(self.radioModu), tostring(self.radioOn)) + text=text..string.format("Ammo = %d (G=%d/R=%d/B=%d/M=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Bombs, self.ammo.Missiles) + text=text..string.format("FSM state = %s\n", self:GetState()) + text=text..string.format("Is alive = %s\n", tostring(self.group:IsAlive())) + text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated())) + text=text..string.format("Uncontrolled = %s\n", tostring(self:IsUncontrolled())) + text=text..string.format("Start Air = %s\n", tostring(self:IsTakeoffAir())) + text=text..string.format("Start Cold = %s\n", tostring(self:IsTakeoffCold())) + text=text..string.format("Start Hot = %s\n", tostring(self:IsTakeoffHot())) + text=text..string.format("Start Rwy = %s\n", tostring(self:IsTakeoffRunway())) + self:I(self.lid..text) + + -- Init done. + self.groupinitialized=true + + end + + return self +end + +--- Add an element to the flight group. +-- @param #FLIGHTGROUP self +-- @param #string unitname Name of unit. +-- @return #FLIGHTGROUP.Element The element or nil. +function FLIGHTGROUP:AddElementByName(unitname) + + local unit=UNIT:FindByName(unitname) + + if unit then + + local element={} --#FLIGHTGROUP.Element + + element.name=unitname + element.unit=unit + element.status=FLIGHTGROUP.ElementStatus.INUTERO + element.group=unit:GetGroup() + + element.modex=element.unit:GetTemplate().onboard_num + element.skill=element.unit:GetTemplate().skill + element.pylons=element.unit:GetTemplatePylons() + element.fuelmass0=element.unit:GetTemplatePayload().fuel + element.fuelmass=element.fuelmass0 + element.fuelrel=element.unit:GetFuel() + element.category=element.unit:GetUnitCategory() + element.categoryname=element.unit:GetCategoryName() + element.callsign=element.unit:GetCallsign() + element.size=element.unit:GetObjectSize() + + if element.skill=="Client" or element.skill=="Player" then + element.ai=false + element.client=CLIENT:FindByName(unitname) + else + element.ai=true + end + + local text=string.format("Adding element %s: status=%s, skill=%s, modex=%s, fuelmass=%.1f (%d %%), category=%d, categoryname=%s, callsign=%s, ai=%s", + element.name, element.status, element.skill, element.modex, element.fuelmass, element.fuelrel, element.category, element.categoryname, element.callsign, tostring(element.ai)) + self:I(self.lid..text) + + -- Add element to table. + table.insert(self.elements, element) + + return element + end + + return nil +end + +--- Check if a unit is and element of the flightgroup. +-- @param #FLIGHTGROUP self +-- @param #string unitname Name of unit. +-- @return #FLIGHTGROUP.Element The element. +function FLIGHTGROUP:GetElementByName(unitname) + + for _,_element in pairs(self.elements) do + local element=_element --#FLIGHTGROUP.Element + + if element.name==unitname then + return element + end + + end + + return nil +end + +--- Check if a unit is and element of the flightgroup. +-- @param #FLIGHTGROUP self +-- @return Wrapper.Airbase#AIRBASE Final destination airbase or #nil. +function FLIGHTGROUP:GetHomebaseFromWaypoints() + + local wp=self:GetWaypoint(1) + + if wp then + + if wp and wp.action and wp.action==COORDINATE.WaypointAction.FromParkingArea + or wp.action==COORDINATE.WaypointAction.FromParkingAreaHot + or wp.action==COORDINATE.WaypointAction.FromRunway then + + -- Get airbase ID depending on airbase category. + local airbaseID=wp.airdromeId or wp.helipadId + + local airbase=AIRBASE:FindByID(airbaseID) + + return airbase + end + + --TODO: Handle case where e.g. only one WP but that is not landing. + --TODO: Probably other cases need to be taken care of. + + end + + return nil +end + +--- Find the nearest friendly airbase (same or neutral coalition). +-- @param #FLIGHTGROUP self +-- @param #number Radius Search radius in NM. Default 50 NM. +-- @return Wrapper.Airbase#AIRBASE Closest tanker group #nil. +function FLIGHTGROUP:FindNearestAirbase(Radius) + + local coord=self:GetCoordinate() + + local dmin=math.huge + local airbase=nil --Wrapper.Airbase#AIRBASE + for _,_airbase in pairs(AIRBASE.GetAllAirbases()) do + local ab=_airbase --Wrapper.Airbase#AIRBASE + + local coalitionAB=ab:GetCoalition() + + if coalitionAB==self:GetCoalition() or coalitionAB==coalition.side.NEUTRAL then + + if airbase then + local d=ab:GetCoordinate():Get2DDistance(coord) + + if dself.currentwp or nwaypoints==1 then + + -- Debug info. + self:T2(self.lid..string.format("Updating waypoint task for waypoint %d/%d. Last waypoint passed %d.", i, nwaypoints, self.currentwp)) + + -- Tasks of this waypoint + local taskswp={} + + -- At each waypoint report passing. + local TaskPassingWaypoint=self.group:TaskFunction("FLIGHTGROUP._PassingWaypoint", self, i) + table.insert(taskswp, TaskPassingWaypoint) + + -- Waypoint task combo. + wp.task=self.group:TaskCombo(taskswp) + + end + end + +end + +--- Initialize Mission Editor waypoints. +-- @param #FLIGHTGROUP self +-- @param #table waypoints Table of waypoints. Default is from group template. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:InitWaypoints(waypoints) + + -- Template waypoints. + self.waypoints0=self.group:GetTemplateRoutePoints() + + -- Waypoints of group as defined in the ME. + self.waypoints=waypoints or UTILS.DeepCopy(self.waypoints0) + + -- Get home and destination airbases from waypoints. + self.homebase=self:GetHomebaseFromWaypoints() + self.destbase=self:GetDestinationFromWaypoints() + + -- Remove the landing waypoint. We use RTB for that. It makes adding new waypoints easier as we do not have to check if the last waypoint is the landing waypoint. + if self.destbase then + table.remove(self.waypoints, #self.waypoints) + else + self.destbase=self.homebase + end + + -- Debug info. + self:T(self.lid..string.format("Initializing %d waypoints. Homebase %s ==> %s Destination", #self.waypoints, self.homebase and self.homebase:GetName() or "unknown", self.destbase and self.destbase:GetName() or "uknown")) + + -- Update route. + if #self.waypoints>0 then + + -- Check if only 1 wp? + if #self.waypoints==1 then + self.passedfinalwp=true + end + + -- Update route (when airborne). + --self:_CheckFlightDone(1) + self:__UpdateRoute(-1) + end + + return self +end + +--- Add a waypoint to the flight plan. +-- @param #FLIGHTGROUP self +-- @param Core.Point#COORDINATE coordinate The coordinate of the waypoint. Use COORDINATE:SetAltitude(altitude) to define the altitude. +-- @param #number wpnumber Waypoint number. Default at the end. +-- @param #number speed Speed in knots. Default 350 kts. +-- @param #boolean updateroute If true or nil, call UpdateRoute. If false, no call. +-- @return #number Waypoint index. +function FLIGHTGROUP:AddWaypointAir(coordinate, wpnumber, speed, updateroute) + + -- Waypoint number. Default is at the end. + wpnumber=wpnumber or #self.waypoints+1 + + if wpnumber>self.currentwp then + self.passedfinalwp=false + end + + -- Speed in knots. + speed=speed or 350 + + -- Speed at waypoint. + local speedkmh=UTILS.KnotsToKmph(speed) + + -- Create air waypoint. + local wp=coordinate:WaypointAir(COORDINATE.WaypointAltType.BARO, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, speedkmh, true, nil, {}, string.format("Added Waypoint #%d", wpnumber)) + + -- Add to table. + table.insert(self.waypoints, wpnumber, wp) + + -- Debug info. + self:T(self.lid..string.format("Adding AIR waypoint #%d, speed=%.1f knots. Last waypoint passed was #%s. Total waypoints #%d", wpnumber, speed, self.currentwp, #self.waypoints)) + + -- Shift all waypoint tasks after the inserted waypoint. + for _,_task in pairs(self.taskqueue) do + local task=_task --#FLIGHTGROUP.Task + if task.type==FLIGHTGROUP.TaskType.WAYPOINT and task.waypoint and task.waypoint>=wpnumber then + task.waypoint=task.waypoint+1 + end + end + + -- Shift all mission waypoints after the inserted waypoint. + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + -- Get mission waypoint index. + local wpidx=mission:GetFlightWaypointIndex(self) + + -- Increase number if this waypoint lies in the future. + if wpidx and wpidx>=wpnumber then + mission:SetFlightWaypointIndex(self, wpidx+1) + end + + end + + -- Update route. + if updateroute==nil or updateroute==true then + self:_CheckFlightDone(1) + --self:__UpdateRoute(-1) + end + + return wpnumber +end + +--- Remove a waypoint. +-- @param #FLIGHTGROUP self +-- @param #number wpindex Waypoint number. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:RemoveWaypoint(wpindex) + + if self.waypoints then + + -- Number of waypoints before delete. + local N=#self.waypoints + + -- Remove waypoint. + table.remove(self.waypoints, wpindex) + + -- Number of waypoints after delete. + local n=#self.waypoints + + self:I(self.lid..string.format("Removing waypoint %d. N %d-->%d", wpindex, N, n)) + + -- Shift all waypoint tasks after the removed waypoint. + for _,_task in pairs(self.taskqueue) do + local task=_task --#FLIGHTGROUP.Task + if task.type==FLIGHTGROUP.TaskType.WAYPOINT and task.waypoint and task.waypoint>wpindex then + task.waypoint=task.waypoint-1 + end + end + + -- Shift all mission waypoints after the removerd waypoint. + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + -- Get mission waypoint index. + local wpidx=mission:GetFlightWaypointIndex(self) + + -- Reduce number if this waypoint lies in the future. + if wpidx and wpidx>wpindex then + mission:SetFlightWaypointIndex(self, wpidx-1) + end + end + + + -- Waypoint was not reached yet. + if wpindex > self.currentwp then + + -- Could be that we just removed the only remaining waypoint ==> passedfinalwp=true so we RTB or wait. + if self.currentwp>=n then + self.passedfinalwp=true + end + + env.info("FF update route -1 after waypoint removed") + self:_CheckFlightDone() + + else + + -- If an already passed waypoint was deleted, we do not need to update the route. + + -- TODO: But what about the self.currentwp number. This is now incorrect! + self.currentwp=self.currentwp-1 + + end + + end + + return self +end + + +--- Check if a unit is an element of the flightgroup. +-- @param #FLIGHTGROUP self +-- @param #string unitname Name of unit. +-- @return #boolean If true, unit is element of the flight group or false if otherwise. +function FLIGHTGROUP:_IsElement(unitname) + + for _,_element in pairs(self.elements) do + local element=_element --#FLIGHTGROUP.Element + + if element.name==unitname then + return true + end + + end + + return false +end + +--- Check if all elements of the flight group have the same status (or are dead). +-- @param #FLIGHTGROUP self +-- @param #string unitname Name of unit. +function FLIGHTGROUP:_AllSameStatus(status) + + for _,_element in pairs(self.elements) do + local element=_element --#FLIGHTGROUP.Element + + if element.status==FLIGHTGROUP.ElementStatus.DEAD then + -- Do nothing. Element is already dead and does not count. + elseif element.status~=status then + -- At least this element has a different status. + return false + end + + end + + return true +end + +--- Check if all elements of the flight group have the same status (or are dead). +-- @param #FLIGHTGROUP self +-- @param #string status Status to check. +-- @return #boolean If true, all elements have a similar status. +function FLIGHTGROUP:_AllSimilarStatus(status) + + -- Check if all are dead. + if status==FLIGHTGROUP.ElementStatus.DEAD then + for _,_element in pairs(self.elements) do + local element=_element --#FLIGHTGROUP.Element + if element.status~=FLIGHTGROUP.ElementStatus.DEAD then + -- At least one is still alive. + return false + end + end + return true + end + + for _,_element in pairs(self.elements) do + local element=_element --#FLIGHTGROUP.Element + + self:T3(self.lid..string.format("Status=%s, element %s status=%s", status, element.name, element.status)) + + -- Dead units dont count ==> We wont return false for those. + if element.status~=FLIGHTGROUP.ElementStatus.DEAD then + + ---------- + -- ALIVE + ---------- + + if status==FLIGHTGROUP.ElementStatus.SPAWNED then + + -- Element SPAWNED: Check that others are not still IN UTERO + if element.status~=status and + element.status==FLIGHTGROUP.ElementStatus.INUTERO then + return false + end + + elseif status==FLIGHTGROUP.ElementStatus.PARKING then + + -- Element PARKING: Check that the other are not still SPAWNED + if element.status~=status or + (element.status==FLIGHTGROUP.ElementStatus.INUTERO or + element.status==FLIGHTGROUP.ElementStatus.SPAWNED) then + return false + end + + elseif status==FLIGHTGROUP.ElementStatus.ENGINEON then + + -- Element TAXIING: Check that the other are not still SPAWNED or PARKING + if element.status~=status and + (element.status==FLIGHTGROUP.ElementStatus.INUTERO or + element.status==FLIGHTGROUP.ElementStatus.SPAWNED or + element.status==FLIGHTGROUP.ElementStatus.PARKING) then + return false + end + + elseif status==FLIGHTGROUP.ElementStatus.TAXIING then + + -- Element TAXIING: Check that the other are not still SPAWNED or PARKING + if element.status~=status and + (element.status==FLIGHTGROUP.ElementStatus.INUTERO or + element.status==FLIGHTGROUP.ElementStatus.SPAWNED or + element.status==FLIGHTGROUP.ElementStatus.PARKING or + element.status==FLIGHTGROUP.ElementStatus.ENGINEON) then + return false + end + + elseif status==FLIGHTGROUP.ElementStatus.TAKEOFF then + + -- Element TAKEOFF: Check that the other are not still SPAWNED, PARKING or TAXIING + if element.status~=status and + (element.status==FLIGHTGROUP.ElementStatus.INUTERO or + element.status==FLIGHTGROUP.ElementStatus.SPAWNED or + element.status==FLIGHTGROUP.ElementStatus.PARKING or + element.status==FLIGHTGROUP.ElementStatus.ENGINEON or + element.status==FLIGHTGROUP.ElementStatus.TAXIING) then + return false + end + + elseif status==FLIGHTGROUP.ElementStatus.AIRBORNE then + + -- Element AIRBORNE: Check that the other are not still SPAWNED, PARKING, TAXIING or TAKEOFF + if element.status~=status and + (element.status==FLIGHTGROUP.ElementStatus.INUTERO or + element.status==FLIGHTGROUP.ElementStatus.SPAWNED or + element.status==FLIGHTGROUP.ElementStatus.PARKING or + element.status==FLIGHTGROUP.ElementStatus.ENGINEON or + element.status==FLIGHTGROUP.ElementStatus.TAXIING or + element.status==FLIGHTGROUP.ElementStatus.TAKEOFF) then + return false + end + + elseif status==FLIGHTGROUP.ElementStatus.LANDED then + + -- Element LANDED: check that the others are not still AIRBORNE or LANDING + if element.status~=status and + (element.status==FLIGHTGROUP.ElementStatus.AIRBORNE or + element.status==FLIGHTGROUP.ElementStatus.LANDING) then + return false + end + + elseif status==FLIGHTGROUP.ElementStatus.ARRIVED then + + -- Element ARRIVED: check that the others are not still AIRBORNE, LANDING, or LANDED (taxiing). + if element.status~=status and + (element.status==FLIGHTGROUP.ElementStatus.AIRBORNE or + element.status==FLIGHTGROUP.ElementStatus.LANDING or + element.status==FLIGHTGROUP.ElementStatus.LANDED) then + return false + end + + end + + else + -- Element is dead. We don't care unless all are dead. + end --DEAD + + end + + -- Debug info. + self:T3(self.lid..string.format("All %d elements have similar status %s ==> returning TRUE", #self.elements, status)) + + return true +end + +--- Check if all elements of the flight group have the same status or are dead. +-- @param #FLIGHTGROUP self +-- @param #FLIGHTGROUP.Element element Element. +-- @param #string newstatus New status of element +-- @param Wrapper.Airbase#AIRBASE airbase Airbase if applicable. +function FLIGHTGROUP:_UpdateStatus(element, newstatus, airbase) + + -- Old status. + local oldstatus=element.status + + -- Update status of element. + element.status=newstatus + + -- Debug + self:T3(self.lid..string.format("UpdateStatus element=%s: %s --> %s", element.name, oldstatus, newstatus)) + for _,_element in pairs(self.elements) do + local Element=_element -- #FLIGHTGROUP.Element + self:T3(self.lid..string.format("Element %s: %s", Element.name, Element.status)) + end + + if newstatus==FLIGHTGROUP.ElementStatus.SPAWNED then + --- + -- SPAWNED + --- + + if self:_AllSimilarStatus(newstatus) then + self:__FlightSpawned(-0.5) + end + + elseif newstatus==FLIGHTGROUP.ElementStatus.PARKING then + --- + -- PARKING + --- + + if self:_AllSimilarStatus(newstatus) then + self:__FlightParking(-0.5) + end + + elseif newstatus==FLIGHTGROUP.ElementStatus.ENGINEON then + --- + -- ENGINEON + --- + + -- No FLIGHT status. Waiting for taxiing. + + elseif newstatus==FLIGHTGROUP.ElementStatus.TAXIING then + --- + -- TAXIING + --- + + if self:_AllSimilarStatus(newstatus) then + self:__FlightTaxiing(-0.5) + end + + elseif newstatus==FLIGHTGROUP.ElementStatus.TAKEOFF then + --- + -- TAKEOFF + --- + + if self:_AllSimilarStatus(newstatus) then + -- Trigger takeoff event. Also triggers airborne event. + self:__FlightTakeoff(-0.5, airbase) + end + + elseif newstatus==FLIGHTGROUP.ElementStatus.AIRBORNE then + --- + -- AIRBORNE + --- + + if self:_AllSimilarStatus(newstatus) then + self:__FlightAirborne(-0.5) + end + + elseif newstatus==FLIGHTGROUP.ElementStatus.LANDED then + --- + -- LANDED + --- + + if self:_AllSimilarStatus(newstatus) then + self:FlightLanded(airbase) + end + + elseif newstatus==FLIGHTGROUP.ElementStatus.ARRIVED then + --- + -- ARRIVED + --- + + if self:_AllSimilarStatus(newstatus) then + + if self:IsLanded() then + self:FlightArrived() + elseif self:IsAirborne() then + self:FlightLanded() + self:FlightArrived() + end + + end + + elseif newstatus==FLIGHTGROUP.ElementStatus.DEAD then + --- + -- DEAD + --- + + if self:_AllSimilarStatus(newstatus) then + self:FlightDead() + end + + end +end + +--- Set status for all elements (except dead ones). +-- @param #FLIGHTGROUP self +-- @param #string status Element status. +function FLIGHTGROUP:_SetElementStatusAll(status) + + for _,_element in pairs(self.elements) do + local element=_element --#FLIGHTGROUP.Element + if element.status~=FLIGHTGROUP.ElementStatus.DEAD then + element.status=status + end + end + +end + +--- Set parking spot of element. +-- @param #FLIGHTGROUP self +-- @param #FLIGHTGROUP.Element Element The element. +-- @param Wrapper.Airbase#AIRBASE.ParkingSpot Spot Parking Spot. +function FLIGHTGROUP:_SetElementParkingAt(Element, Spot) + + -- Element is parking here. + Element.parking=Spot + + if Spot then + + env.info(string.format("FF Element %s is parking on spot %d", Element.name, Spot.TerminalID)) + + if self.flightcontrol then + + -- Set parking spot to OCCUPIED. + self.flightcontrol:SetParkingOccupied(Element.parking, Element.name) + end + + end + +end + +--- Set parking spot of element to free +-- @param #FLIGHTGROUP self +-- @param #FLIGHTGROUP.Element Element The element. +function FLIGHTGROUP:_SetElementParkingFree(Element) + + if Element.parking then + + -- Set parking to FREE. + if self.flightcontrol then + self.flightcontrol:SetParkingFree(Element.parking) + end + + -- Not parking any more. + Element.parking=nil + + end + +end + + +--- Check if flight is in zones. +-- @param #FLIGHTGROUP self +function FLIGHTGROUP:_CheckInZones() + + if self.checkzones then + + local Ncheck=self.checkzones:Count() + local Ninside=self.inzones:Count() + + -- Debug info. + self:T(self.lid..string.format("Check if flight is in %d zones. Currently it is in %d zones.", self.checkzones:Count(), self.inzones:Count())) + + -- Firstly, check if group is still inside zone it was already in. If not, remove zones and trigger LeaveZone() event. + local leftzones={} + for inzonename, inzone in pairs(self.inzones:GetSet()) do + + -- Check if group is still inside the zone. + local isstillinzone=self.group:IsPartlyOrCompletelyInZone(inzone) + + -- If not, trigger, LeaveZone event. + if not isstillinzone then + table.insert(leftzones, inzone) + end + end + + -- Trigger leave zone event. + for _,leftzone in pairs(leftzones) do + self:LeaveZone(leftzone) + end + + + -- Now, run of all check zones and see if the group entered a zone. + local enterzones={} + for checkzonename,_checkzone in pairs(self.checkzones:GetSet()) do + local checkzone=_checkzone --Core.Zone#ZONE + + -- Is flight currtently in this check zone? + local isincheckzone=self.group:IsPartlyOrCompletelyInZone(checkzone) + + if isincheckzone and not self.inzones:_Find(checkzonename) then + table.insert(enterzones, checkzone) + end + end + + -- Trigger enter zone event. + for _,enterzone in pairs(enterzones) do + self:EnterZone(enterzone) + end + + + end + +end + +--- Check detected units. +-- @param #FLIGHTGROUP self +function FLIGHTGROUP:_CheckDetectedUnits() + + if self.group and not self:IsDead() then + + -- Get detected DCS units. + local detectedtargets=self.group:GetDetectedTargets() + + local detected={} + for DetectionObjectID, Detection in pairs(detectedtargets or {}) do + local DetectedObject=Detection.object -- DCS#Object + + if DetectedObject and DetectedObject:isExist() and DetectedObject.id_<50000000 then + local unit=UNIT:Find(DetectedObject) + + if unit and unit:IsAlive() then + + -- Name of detected unit + local unitname=unit:GetName() + + -- Add unit to detected table of this run. + table.insert(detected, unit) + + -- Trigger detected unit event. + self:DetectedUnit(unit) + + if self.detectedunits:FindUnit(unitname) then + -- Unit is already in the detected unit set ==> Trigger "DetectedUnitKnown" event. + self:DetectedUnitKnown(unit) + else + -- Unit is was not detected ==> Trigger "DetectedUnitNew" event. + self:DetectedUnitNew(unit) + end + + end + end + end + + -- Loop over units in detected set. + local lost={} + for _,_unit in pairs(self.detectedunits:GetSet()) do + local unit=_unit --Wrapper.Unit#UNIT + + -- Loop over detected units + local gotit=false + for _,_du in pairs(detected) do + local du=_du --Wrapper.Unit#UNIT + if unit:GetName()==du:GetName() then + gotit=true + end + end + + if not gotit then + table.insert(lost, unit:GetName()) + self:DetectedUnitLost(unit) + end + + end + + -- Remove lost units from detected set. + self.detectedunits:RemoveUnitsByName(lost) + + end + + +end + +--- Get onboard number. +-- @param #FLIGHTGROUP self +-- @param #string unitname Name of the unit. +-- @return #string Modex. +function FLIGHTGROUP:_GetOnboardNumber(unitname) + + local group=UNIT:FindByName(unitname):GetGroup() + + -- Units of template group. + local units=group:GetTemplate().units + + -- Get numbers. + local numbers={} + for _,unit in pairs(units) do + + if unitname==unit.name then + return tostring(unit.onboard_num) + end + + end + + return nil +end + +--- Get the number of shells a unit or group currently has. For a group the ammo count of all units is summed up. +-- @param #FLIGHTGROUP self +-- @param #FLIGHTGROUP.Element element The element. +-- @return #FLIGHTGROUP.Ammo Ammo data. +function FLIGHTGROUP:GetAmmoElement(element) + return self:GetAmmoUnit(element.unit) +end + +--- Get total amount of ammunition of the whole group. +-- @param #FLIGHTGROUP self +-- @return #FLIGHTGROUP.Ammo Ammo data. +function FLIGHTGROUP:GetAmmoTot() + + local units=self.group:GetUnits() + + local Ammo={} --#FLIGHTGROUP.Ammo + Ammo.Total=0 + Ammo.Guns=0 + Ammo.Rockets=0 + Ammo.Bombs=0 + Ammo.Missiles=0 + Ammo.MissilesAA=0 + Ammo.MissilesAG=0 + Ammo.MissilesAS=0 + + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + + if unit and unit:IsAlive()~=nil then + + -- Get ammo of the unit. + local ammo=self:GetAmmoUnit(unit) + + -- Add up total. + Ammo.Total=Ammo.Total+ammo.Total + Ammo.Guns=Ammo.Guns+ammo.Guns + Ammo.Rockets=Ammo.Rockets+ammo.Rockets + Ammo.Bombs=Ammo.Bombs+ammo.Bombs + Ammo.Missiles=Ammo.Missiles+ammo.Missiles + Ammo.MissilesAA=Ammo.MissilesAA+ammo.MissilesAA + Ammo.MissilesAG=Ammo.MissilesAG+ammo.MissilesAG + Ammo.MissilesAS=Ammo.MissilesAS+ammo.MissilesAS + + end + + end + + return Ammo +end + +--- Get the number of shells a unit or group currently has. For a group the ammo count of all units is summed up. +-- @param #FLIGHTGROUP self +-- @param Wrapper.Unit#UNIT unit The unit object. +-- @param #boolean display Display ammo table as message to all. Default false. +-- @return #FLIGHTGROUP.Ammo Ammo data. +function FLIGHTGROUP:GetAmmoUnit(unit, display) + + -- Default is display false. + if display==nil then + display=false + end + + -- Init counter. + local nammo=0 + local nshells=0 + local nrockets=0 + local nmissiles=0 + local nmissilesAA=0 + local nmissilesAG=0 + local nmissilesAS=0 + local nbombs=0 + + -- Output. + local text=string.format("FLIGHTGROUP group %s - unit %s:\n", self.groupname, unit:GetName()) + + -- Get ammo table. + local ammotable=unit:GetAmmo() + + if ammotable then + + local weapons=#ammotable + + -- Loop over all weapons. + for w=1,weapons do + + -- Number of current weapon. + local Nammo=ammotable[w]["count"] + + -- Type name of current weapon. + local Tammo=ammotable[w]["desc"]["typeName"] + + local _weaponString = UTILS.Split(Tammo,"%.") + local _weaponName = _weaponString[#_weaponString] + + -- Get the weapon category: shell=0, missile=1, rocket=2, bomb=3 + local Category=ammotable[w].desc.category + + -- Get missile category: Weapon.MissileCategory AAM=1, SAM=2, BM=3, ANTI_SHIP=4, CRUISE=5, OTHER=6 + local MissileCategory=nil + if Category==Weapon.Category.MISSILE then + MissileCategory=ammotable[w].desc.missileCategory + end + + -- We are specifically looking for shells or rockets here. + if Category==Weapon.Category.SHELL then + + -- Add up all shells. + nshells=nshells+Nammo + + -- Debug info. + text=text..string.format("- %d shells of type %s\n", Nammo, _weaponName) + + elseif Category==Weapon.Category.ROCKET then + + -- Add up all rockets. + nrockets=nrockets+Nammo + + -- Debug info. + text=text..string.format("- %d rockets of type %s\n", Nammo, _weaponName) + + elseif Category==Weapon.Category.BOMB then + + -- Add up all rockets. + nbombs=nbombs+Nammo + + -- Debug info. + text=text..string.format("- %d bombs of type %s\n", Nammo, _weaponName) + + elseif Category==Weapon.Category.MISSILE then + + -- Add up all cruise missiles (category 5) + if MissileCategory==Weapon.MissileCategory.AAM then + nmissiles=nmissiles+Nammo + nmissilesAA=nmissilesAA+Nammo + elseif MissileCategory==Weapon.MissileCategory.ANTI_SHIP then + nmissiles=nmissiles+Nammo + nmissilesAS=nmissilesAS+Nammo + elseif MissileCategory==Weapon.MissileCategory.BM then + nmissiles=nmissiles+Nammo + nmissilesAG=nmissilesAG+Nammo + elseif MissileCategory==Weapon.MissileCategory.OTHER then + nmissiles=nmissiles+Nammo + nmissilesAG=nmissilesAG+Nammo + end + + -- Debug info. + text=text..string.format("- %d %s missiles of type %s\n", Nammo, self:_MissileCategoryName(MissileCategory), _weaponName) + + else + + -- Debug info. + text=text..string.format("- %d unknown ammo of type %s (category=%d, missile category=%s)\n", Nammo, Tammo, Category, tostring(MissileCategory)) + + end + + end + end + + -- Debug text and send message. + if display then + self:I(self.lid..text) + else + self:T3(self.lid..text) + end + MESSAGE:New(text, 10):ToAllIf(display) + + -- Total amount of ammunition. + nammo=nshells+nrockets+nmissiles+nbombs + + local ammo={} --#FLIGHTGROUP.Ammo + ammo.Total=nammo + ammo.Guns=nshells + ammo.Rockets=nrockets + ammo.Bombs=nbombs + ammo.Missiles=nmissiles + ammo.MissilesAA=nmissilesAA + ammo.MissilesAG=nmissilesAG + ammo.MissilesAS=nmissilesAS + + return ammo +end + + +--- Returns a name of a missile category. +-- @param #FLIGHTGROUP self +-- @param #number categorynumber Number of missile category from weapon missile category enumerator. See https://wiki.hoggitworld.com/view/DCS_Class_Weapon +-- @return #string Missile category name. +function FLIGHTGROUP:_MissileCategoryName(categorynumber) + local cat="unknown" + if categorynumber==Weapon.MissileCategory.AAM then + cat="air-to-air" + elseif categorynumber==Weapon.MissileCategory.SAM then + cat="surface-to-air" + elseif categorynumber==Weapon.MissileCategory.BM then + cat="ballistic" + elseif categorynumber==Weapon.MissileCategory.ANTI_SHIP then + cat="anti-ship" + elseif categorynumber==Weapon.MissileCategory.CRUISE then + cat="cruise" + elseif categorynumber==Weapon.MissileCategory.OTHER then + cat="other" + end + return cat +end + +--- Checks if a human player sits in the unit. +-- @param #FLIGHTGROUP self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. +-- @return #boolean If true, human player inside the unit. +function FLIGHTGROUP:_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 #FLIGHTGROUP self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @return #boolean If true, human player inside group. +function FLIGHTGROUP:_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 + +--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. +-- @param #FLIGHTGROUP 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 FLIGHTGROUP:_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) + + 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 + +--- Returns the coalition side. +-- @param #FLIGHTGROUP self +-- @return #number Coalition side number. +function FLIGHTGROUP:GetCoalition() + return self.group:GetCoalition() +end + +--- Returns the absolute (average) life points of the group. +-- @param #FLIGHTGROUP self +-- @return #number Life points. If group contains more than one element, the average is given. +-- @return #number Initial life points. +function FLIGHTGROUP:GetLifePoints() + if self.group then + return self.group:GetLife(), self.group:GetLife0() + end +end + +--- Returns the parking spot of the element. +-- @param #FLIGHTGROUP self +-- @param #FLIGHTGROUP.Element element Element of the flight group. +-- @param #number maxdist Distance threshold in meters. Default 5 m. +-- @param Wrapper.Airbase#AIRBASE airbase (Optional) The airbase to check for parking. Default is closest airbase to the element. +-- @return Wrapper.Airbase#AIRBASE.ParkingSpot Parking spot or nil if no spot is within distance threshold. +function FLIGHTGROUP:GetParkingSpot(element, maxdist, airbase) + + local coord=element.unit:GetCoordinate() + + airbase=airbase or coord:GetClosestAirbase(nil, self:GetCoalition()) + + local spot=nil --Wrapper.Airbase#AIRBASE.ParkingSpot + local dist=nil + local distmin=math.huge + for _,_parking in pairs(airbase.parking) do + local parking=_parking --Wrapper.Airbase#AIRBASE.ParkingSpot + dist=coord:Get2DDistance(parking.Coordinate) + if dist safedist) + return safe + end + + -- Get client coordinates. + local function _clients() + local clients=_DATABASE.CLIENTS + local coords={} + for clientname, client in pairs(clients) do + local template=_DATABASE:GetGroupTemplateFromUnitName(clientname) + local units=template.units + for i,unit in pairs(units) do + local coord=COORDINATE:New(unit.x, unit.alt, unit.y) + coords[unit.name]=coord + end + end + return coords + end + + -- Get airbase category. + local airbasecategory=airbase:GetAirbaseCategory() + + -- 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! + -- The alternative would be to perform the scan once but with a much larger radius and store all data. + for _,_parkingspot in pairs(parkingdata) do + local parkingspot=_parkingspot --Wrapper.Airbase#AIRBASE.ParkingSpot + + -- Scan a radius of 100 meters around the spot. + local _,_,_,_units,_statics,_sceneries=parkingspot.Coordinate:ScanObjects(scanradius, scanunits, scanstatics, scanscenery) + + -- Check all units. + for _,_unit in pairs(_units) do + local unit=_unit --Wrapper.Unit#UNIT + local _coord=unit:GetCoordinate() + local _size=self:_GetObjectSize(unit:GetDCSObject()) + local _name=unit:GetName() + table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="unit"}) + end + + -- Check all clients. + local clientcoords=_clients() + for clientname,_coord in pairs(clientcoords) do + table.insert(obstacles, {coord=_coord, size=15, name=clientname, type="client"}) + end + + -- Check all statics. + for _,static in pairs(_statics) do + local _vec3=static:getPoint() + local _coord=COORDINATE:NewFromVec3(_vec3) + local _name=static:getName() + 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() + local _coord=COORDINATE:NewFromVec3(_vec3) + local _name=scenery:getTypeName() + local _size=self:_GetObjectSize(scenery) + table.insert(obstacles,{coord=_coord, size=_size, name=_name, type="scenery"}) + end + + end + + -- Parking data for all assets. + local parking={} + + -- Get terminal type. + local terminaltype=self:_GetTerminal(self.attribute, airbase:GetAirbaseCategory()) + + -- Loop over all units - each one needs a spot. + for i,_element in pairs(self.elements) do + local element=_element --#FLIGHTGROUP.Element + + -- Loop over all parking spots. + local gotit=false + 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 + + -- Assume free and no problematic obstacle. + local free=true + local problem=nil + + -- Safe parking using TO_AC from DCS result. + if verysafe and parkingspot.TOAC then + free=false + self:T2(self.lid..string.format("Parking spot %d is occupied by other aircraft taking off (TOAC).", parkingspot.TerminalID)) + end + + -- Loop over all obstacles. + for _,obstacle in pairs(obstacles) do + + -- Check if aircraft overlaps with any obstacle. + local dist=parkingspot.Coordinate:Get2DDistance(obstacle.coord) + local safe=_overlap(element.size, obstacle.size, dist) + + -- Spot is blocked. + if not safe then + free=false + problem=obstacle + problem.dist=dist + break + end + + end + + -- Check flightcontrol data. + if self.flightcontrol and self.flightcontrol.airbasename==airbase:GetName() then + local problem=self.flightcontrol:IsParkingReserved(parkingspot) or self.flightcontrol:IsParkingOccupied(parkingspot) + if problem then + free=false + end + end + + -- Check if spot is free + if free then + + -- Add parkingspot for this element. + table.insert(parking, parkingspot) + + self:T2(self.lid..string.format("Parking spot %d is free for element %s!", parkingspot.TerminalID, element.name)) + + -- Add the unit as obstacle so that this spot will not be available for the next unit. + table.insert(obstacles, {coord=parkingspot.Coordinate, size=element.size, name=element.name, type="element"}) + + gotit=true + break + + else + + -- Debug output for occupied spots. + self:T2(self.lid..string.format("Parking spot %d is occupied or not big enough!", parkingspot.TerminalID)) + --if self.Debug then + -- local coord=problem.coord --Core.Point#COORDINATE + -- 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:E(self.lid..string.format("WARNING: No free parking spot for element %s", element.name)) + return nil + end + + end -- loop over asset units + + return parking +end + +--- Size of the bounding box of a DCS object derived from the DCS descriptor table. If boundinb box is nil, a size of zero is returned. +-- @param #FLIGHTGROUP self +-- @param DCS#Object DCSobject The DCS object for which the size is needed. +-- @return #number Max size of object in meters (length (x) or width (z) components not including height (y)). +-- @return #number Length (x component) of size. +-- @return #number Height (y component) of size. +-- @return #number Width (z component) of size. +function FLIGHTGROUP:_GetObjectSize(DCSobject) + local DCSdesc=DCSobject:getDesc() + if DCSdesc.box then + local x=DCSdesc.box.max.x+math.abs(DCSdesc.box.min.x) --length + 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) --width + return math.max(x,z), x , y, z + end + return 0,0,0,0 +end + +--- Get the generalized attribute of a group. +-- @param #FLIGHTGROUP self +-- @return #string Generalized attribute of the group. +function FLIGHTGROUP:_GetAttribute() + + -- Default + local attribute=FLIGHTGROUP.Attribute.OTHER + + local group=self.group + + if group then + + --- 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 bomber=group:HasAttribute("Strategic bombers") + local tanker=group:HasAttribute("Tankers") + local uav=group:HasAttribute("UAVs") + --- Helicopters + local transporthelo=group:HasAttribute("Transport helicopters") + local attackhelicopter=group:HasAttribute("Attack helicopters") + + -- Define attribute. Order is important. + if transportplane then + attribute=FLIGHTGROUP.Attribute.AIR_TRANSPORTPLANE + elseif awacs then + attribute=FLIGHTGROUP.Attribute.AIR_AWACS + elseif fighter then + attribute=FLIGHTGROUP.Attribute.AIR_FIGHTER + elseif bomber then + attribute=FLIGHTGROUP.Attribute.AIR_BOMBER + elseif tanker then + attribute=FLIGHTGROUP.Attribute.AIR_TANKER + elseif transporthelo then + attribute=FLIGHTGROUP.Attribute.AIR_TRANSPORTHELO + elseif attackhelicopter then + attribute=FLIGHTGROUP.Attribute.AIR_ATTACKHELO + elseif uav then + attribute=FLIGHTGROUP.Attribute.AIR_UAV + end + + end + + return attribute +end + +--- Get the proper terminal type based on generalized attribute of the group. +--@param #FLIGHTGROUP self +--@param #FLIGHTGROUP.Attribute _attribute Generlized attibute of unit. +--@param #number _category Airbase category. +--@return Wrapper.Airbase#AIRBASE.TerminalType Terminal type for this group. +function FLIGHTGROUP:_GetTerminal(_attribute, _category) + + -- Default terminal is "large". + local _terminal=AIRBASE.TerminalType.OpenBig + + if _attribute==FLIGHTGROUP.Attribute.AIR_FIGHTER then + -- Fighter ==> small. + _terminal=AIRBASE.TerminalType.FighterAircraft + elseif _attribute==FLIGHTGROUP.Attribute.AIR_BOMBER or _attribute==FLIGHTGROUP.Attribute.AIR_TRANSPORTPLANE or _attribute==FLIGHTGROUP.Attribute.AIR_TANKER or _attribute==FLIGHTGROUP.Attribute.AIR_AWACS then + -- Bigger aircraft. + _terminal=AIRBASE.TerminalType.OpenBig + elseif _attribute==FLIGHTGROUP.Attribute.AIR_TRANSPORTHELO or _attribute==FLIGHTGROUP.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==FLIGHTGROUP.Attribute.AIR_TRANSPORTHELO or _attribute==FLIGHTGROUP.Attribute.AIR_ATTACKHELO) then + _terminal=AIRBASE.TerminalType.OpenMedOrBig + end + end + + return _terminal +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- OPTION FUNCTIONS +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set the default ROE for the group. This is the ROE state gets when the group is spawned or to which it defaults back after a mission. +-- @param #FLIGHTGROUP self +-- @param #number roe ROE of group. Default is `ENUMS.ROE.ReturnFire`. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetDefaultROE(roe) + self.roeDefault=roe or ENUMS.ROE.ReturnFire + return self +end + +--- Set current ROE for the group. +-- @param #FLIGHTGROUP self +-- @param #string roe ROE of group. Default is the value defined by :SetDefaultROE(). +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetOptionROE(roe) + + roe=roe or self.roeDefault + + if self:IsAlive() then + + self.group:OptionROE(roe) + + self.roe=roe + + self:T2(self.lid..string.format("Setting current ROE=%d (0=WeaponFree, 1=OpenFireWeaponFree, 2=OpenFire, 3=ReturnFire, 4=WeaponHold)", self.roe)) + else + -- TODO WARNING + end + + return self +end + +--- Set the default ROT for the group. This is the ROT state gets when the group is spawned or to which it defaults back after a mission. +-- @param #FLIGHTGROUP self +-- @param #number roe ROE of group. Default is ENUMS.ROT.PassiveDefense. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetDefaultROT(roe) + self.rotDefault=roe or ENUMS.ROT.PassiveDefense + return self +end + +--- Set ROT for the group. +-- @param #FLIGHTGROUP self +-- @param #string rot ROT of group. Default is the value defined by :SetDefaultROT(). +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetOptionROT(rot) + + rot=rot or self.rotDefault + + if self:IsAlive() then + + self.group:OptionROT(rot) + + self.rot=rot + + self:T2(self.lid..string.format("Setting current ROT=%d (0=NoReaction, 1=Passive, 2=Evade, 3=ByPass, 4=AllowAbort)", self.rot)) + else + -- TODO WARNING + end + + return self +end + +--- Set default TACAN parameters. AA TACANs are always on "Y" band. +-- @param #FLIGHTGROUP self +-- @param #number Channel TACAN channel. +-- @param #string Morse Morse code. Default "XXX". +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetDefaultTACAN(Channel, Morse) + + self.tacanChannelDefault=Channel + self.tacanMorseDefault=Morse or "XXX" + + return self +end + +--- Activate TACAN beacon. +-- @param #FLIGHTGROUP self +-- @param #number TACANChannel TACAN Channel. +-- @param #string TACANMorse TACAN morse code. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SwitchTACANOn(TACANChannel, TACANMorse) + + if self:IsAlive() then + + local unit=self.group:GetUnit(1) + + if unit and unit:IsAlive() then + + local Type=5 + local System=4 + local UnitID=unit:GetID() + local TACANMode="Y" + local Frequency=UTILS.TACANToFrequency(TACANChannel, TACANMode) + + unit:CommandActivateBeacon(Type, System, Frequency, UnitID, TACANChannel, TACANMode, true, TACANMorse, true) + + self.tacanBeacon=unit + self.tacanChannel=TACANChannel + self.tacanMorse=TACANMorse + + self.tacanOn=true + + self:I(self.lid..string.format("Switching TACAN to Channel %dY Morse %s", self.tacanChannel, tostring(self.tacanMorse))) + + end + + end + + return self +end + +--- Deactivate TACAN beacon. +-- @param #FLIGHTGROUP self +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SwitchTACANOff() + + if self.tacanBeacon and self.tacanBeacon:IsAlive() then + self.tacanBeacon:CommandDeactivateBeacon() + end + + self:I(self.lid..string.format("Switching TACAN OFF")) + + self.tacanOn=false + +end + +--- Set default Radio frequency and modulation. +-- @param #FLIGHTGROUP self +-- @param #number Frequency Radio frequency in MHz. Default 251 MHz. +-- @param #number Modulation Radio modulation. Default `radio.Modulation.AM`. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetDefaultRadio(Frequency, Modulation) + + self.radioFreqDefault=Frequency or 251 + self.radioModuDefault=Modulation or radio.modulation.AM + + self.radioOn=false + + self.radioUse=true + + return self +end + +--- Get current Radio frequency and modulation. +-- @param #FLIGHTGROUP self +-- @return #number Radio frequency in MHz or nil. +-- @return #number Radio modulation or nil. +function FLIGHTGROUP:GetRadio() + return self.radioFreq, self.radioModu +end + +--- Turn radio on. +-- @param #FLIGHTGROUP self +-- @param #number Frequency Radio frequency in MHz. +-- @param #number Modulation Radio modulation. Default `radio.Modulation.AM`. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SwitchRadioOn(Frequency, Modulation) + + if self:IsAlive() and Frequency then + + Modulation=Modulation or radio.Modulation.AM + + self.group:SetOption(AI.Option.Air.id.SILENCE, false) + + self.group:CommandSetFrequency(Frequency, Modulation) + + self.radioFreq=Frequency + self.radioModu=Modulation + self.radioOn=true + + self:I(self.lid..string.format("Switching radio to frequency %.3f MHz %s", self.radioFreq, UTILS.GetModulationName(self.radioModu))) + + end + + return self +end + +--- Turn radio off. +-- @param #FLIGHTGROUP self +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SwitchRadioOff() + + if self:IsAlive() then + + self.group:SetOption(AI.Option.Air.id.SILENCE, true) + + self.radioFreq=nil + self.radioModu=nil + self.radioOn=false + + self:I(self.lid..string.format("Switching radio OFF")) + + end + + return self +end + +--- Set default formation. +-- @param #FLIGHTGROUP self +-- @param #number Formation The formation the groups flies in. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetDefaultFormation(Formation) + + self.formationDefault=Formation + + return self +end + +--- Switch to a specific formation. +-- @param #FLIGHTGROUP self +-- @param #number Formation New formation the group will fly in. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SwitchFormation(Formation) + + if self:IsAlive() and Formation then + + self.group:SetOption(AI.Option.Air.id.FORMATION, Formation) + + self.formation=Formation + + self:I(self.lid..string.format("Switching formation to %d", self.formation)) + + end + + return self +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- MENU FUNCTIONS +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get the proper terminal type based on generalized attribute of the group. +--@param #FLIGHTGROUP self +--@param #number delay Delay in seconds. +function FLIGHTGROUP:_UpdateMenu(delay) + + if delay and delay>0 then + self:I(self.lid..string.format("FF updating menu in %.1f sec", delay)) + self:ScheduleOnce(delay, FLIGHTGROUP._UpdateMenu, self) + else + + self:I(self.lid.."FF updating menu NOW") + + -- Get current position of group. + local position=self.group:GetCoordinate() + + -- Get all FLIGHTCONTROLS + local fc={} + for airbasename,_flightcontrol in pairs(_DATABASE.FLIGHTCONTROLS) do + + local airbase=AIRBASE:FindByName(airbasename) + + local coord=airbase:GetCoordinate() + + local dist=coord:Get2DDistance(position) + + local fcitem={airbasename=airbasename, dist=dist} + + table.insert(fc, fcitem) + end + + -- Sort table wrt distance to airbases. + local function _sort(a,b) + return a.dist Event --> To State + self:AddTransition("Stopped", "Start", "Cruising") -- Status update. + self:AddTransition("*", "Status", "*") -- Status update. + + self:AddTransition("*", "PassingWaypoint", "*") -- Passing waypoint. + self:AddTransition("*", "UpdateRoute", "*") -- Passing waypoint. + + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the OPSGROUP. Initializes parameters and starts event handlers. + -- @function [parent=#OPSGROUP] Start + -- @param #OPSGROUP self + + --- Triggers the FSM event "Start" after a delay. Starts the OPSGROUP. Initializes parameters and starts event handlers. + -- @function [parent=#OPSGROUP] __Start + -- @param #OPSGROUP self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the OPSGROUP and all its event handlers. + -- @param #OPSGROUP self + + --- Triggers the FSM event "Stop" after a delay. Stops the OPSGROUP and all its event handlers. + -- @function [parent=#OPSGROUP] __Stop + -- @param #OPSGROUP self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Status". + -- @function [parent=#OPSGROUP] Status + -- @param #OPSGROUP self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#OPSGROUP] __Status + -- @param #OPSGROUP self + -- @param #number delay Delay in seconds. + + -- Debug trace. + if false then + self.Debug=true + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(1) + end + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get coalition. +-- @param #OPSGROUP self +-- @return #number Coalition side of carrier. +function OPSGROUP:GetCoalition() + return self.group:GetCoalition() +end + +--- Get coordinate. +-- @param #OPSGROUP self +-- @return Core.Point#COORDINATE Carrier coordinate. +function OPSGROUP:GetCoordinate() + return self.group:GetCoordinate() +end + +--- Set detection on or off. +-- @param #FLIGHTGROUP self +-- @param #boolean Switch If true, detection is on. If false or nil, detection is off. Default is off. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetDetection(Switch) + self.detectionOn=Switch + return self +end + +--- Define a SET of zones that trigger and event if the group enters or leaves any of the zones. +-- @param #FLIGHTGROUP self +-- @param Core.Set#SET_ZONE CheckZonesSet Set of zones. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetCheckZones(CheckZonesSet) + self.checkzones=CheckZonesSet + return self +end + +--- Add a zone that triggers and event if the group enters or leaves any of the zones. +-- @param #FLIGHTGROUP self +-- @param Core.Zone#ZONE CheckZone Zone to check. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:AddCheckZone(CheckZone) + if not self.checkzones then + self.checkzones=SET_ZONE:New() + end + self.checkzones:AddZone(CheckZone) + return self +end + +--- Get set of detected units. +-- @param #FLIGHTGROUP self +-- @return Core.Set#SET_UNIT Set of detected units. +function FLIGHTGROUP:GetDetectedUnits() + return self.detectedunits +end + +--- Get MOOSE group object. +-- @param #FLIGHTGROUP self +-- @return Wrapper.Group#GROUP Moose group object. +function FLIGHTGROUP:GetGroup() + return self.group +end + +--- Get flight group name. +-- @param #FLIGHTGROUP self +-- @return #string Group name. +function FLIGHTGROUP:GetName() + return self.groupname +end + +--- Get current coordinate of the group. +-- @param #FLIGHTGROUP self +-- @return Core.Point#COORDINATE The coordinate (of the first unit) of the group. +function FLIGHTGROUP:GetCoordinate() + if self:IsAlive()~=nil then + return self.group:GetCoordinate() + else + self:E(self.lid.."WARNING: Group is not alive. Cannot get coordinate!") + end + return nil +end + +--- Get waypoint. +-- @param #FLIGHTGROUP self +-- @param #number indx Waypoint index. +-- @return #table Waypoint table. +function FLIGHTGROUP:GetWaypoint(indx) + return self.waypoints[indx] +end + +--- Get final waypoint. +-- @param #FLIGHTGROUP self +-- @return #table Waypoint table. +function FLIGHTGROUP:GetWaypointFinal() + return self.waypoints[#self.waypoints] +end + +--- Get next waypoint. +-- @param #FLIGHTGROUP self +-- @return #table Waypoint table. +function FLIGHTGROUP:GetWaypointNext() + local n=math.min(self.currentwp+1, #self.waypoints) + return self.waypoints[n] +end + +--- Get current waypoint. +-- @param #FLIGHTGROUP self +-- @return #table Waypoint table. +function FLIGHTGROUP:GetWaypointCurrent() + return self.waypoints[self.currentwp] +end + +--- Activate a *late activated* group. +-- @param #FLIGHTGROUP self +-- @param #number delay (Optional) Delay in seconds before the group is activated. Default is immediately. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:Activate(delay) + + if delay and delay>0 then + self:T2(self.lid..string.format("Activating late activated group in %d seconds", delay)) + self:ScheduleOnce(delay, FLIGHTGROUP.Activate, self) + else + + if self:IsAlive()==false then + + self:T(self.lid.."Activating late activated group") + self.group:Activate() + self.isLateActivated=false + + elseif self:IsAlive()==true then + self:E(self.lid.."WARNING: Activating group that is already activated") + else + self:E(self.lid.."ERROR: Activating group that is does not exist!") + end + + end + + return self +end + +--- Self destruction of group. An explosion is created at the position of each element. +-- @param #FLIGHTGROUP self +-- @param #number Delay Delay in seconds. Default now. +-- @param #number ExplosionPower (Optional) Explosion power in kg TNT. Default 500 kg. +-- @return #number Relative fuel in percent. +function FLIGHTGROUP:SelfDestruction(Delay, ExplosionPower) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, FLIGHTGROUP.SelfDestruction, self, 0, ExplosionPower) + else + + -- Loop over all elements. + for i,_element in pairs(self.elements) do + local element=_element --#FLIGHTGROUP.Element + + local unit=element.unit + + if unit and unit:IsAlive() then + unit:Explode(ExplosionPower) + end + end + end + +end + +--- Route group along waypoints. +-- @param #FLIGHTGROUP self +-- @param #table waypoints Table of waypoints. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:Route(waypoints) + + if self:IsAlive() then + + -- DCS task combo. + local Tasks={} + + -- Route (Mission) task. + local TaskRoute=self.group:TaskRoute(waypoints) + table.insert(Tasks, TaskRoute) + + -- TaskCombo of enroute and mission tasks. + local TaskCombo=self.group:TaskCombo(Tasks) + + -- Set tasks. + if #Tasks>1 then + self:SetTask(TaskCombo) + else + self:SetTask(TaskRoute) + end + + else + self:E(self.lid.."ERROR: Group is not alive!") + end + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Task Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set DCS task. Enroute tasks are injected automatically. +-- @param #FLIGHTGROUP self +-- @param #table DCSTask DCS task structure. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetTask(DCSTask) + + if self:IsAlive() then + + if self.taskcurrent>0 then + + -- TODO: Why the hell did I do this? It breaks scheduled tasks. I comment it out for now to see where it fails. + --local task=self:GetTaskCurrent() + --self:RemoveTask(task) + --self.taskcurrent=0 + + end + + -- Inject enroute tasks. + if self.taskenroute and #self.taskenroute>0 then + if tostring(DCSTask.id)=="ComboTask" then + for _,task in pairs(self.taskenroute) do + table.insert(DCSTask.params.tasks, 1, task) + end + else + local tasks=UTILS.DeepCopy(self.taskenroute) + table.insert(tasks, DCSTask) + + DCSTask=self.group.TaskCombo(self, tasks) + end + end + + -- Set task. + self.group:SetTask(DCSTask) + + -- Debug info. + local text=string.format("SETTING Task %s", tostring(DCSTask.id)) + if tostring(DCSTask.id)=="ComboTask" then + for i,task in pairs(DCSTask.params.tasks) do + text=text..string.format("\n[%d] %s", i, tostring(task.id)) + end + end + self:I(self.lid..text) + end + + return self +end + +--- Push DCS task. +-- @param #FLIGHTGROUP self +-- @param #table DCSTask DCS task structure. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:PushTask(DCSTask) + + if self:IsAlive() then + + -- Push task. + self.group:PushTask(DCSTask) + + -- Debug info. + local text=string.format("PUSHING Task %s", tostring(DCSTask.id)) + if tostring(DCSTask.id)=="ComboTask" then + for i,task in pairs(DCSTask.params.tasks) do + text=text..string.format("\n[%d] %s", i, tostring(task.id)) + end + end + self:I(self.lid..text) + end + + return self +end + +--- Clear DCS tasks. +-- @param #FLIGHTGROUP self +-- @param #table DCSTask DCS task structure. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:ClearTasks() + if self:IsAlive() then + self.group:ClearTasks() + self:I(self.lid..string.format("CLEARING Tasks")) + end + return self +end + +--- Add a *scheduled* task. +-- @param #OPSGROUP self +-- @param #table task DCS task table structure. +-- @param #string clock Mission time when task is executed. Default in 5 seconds. If argument passed as #number, it defines a relative delay in seconds. +-- @param #string description Brief text describing the task, e.g. "Attack SAM". +-- @param #number prio Priority of the task. +-- @param #number duration Duration before task is cancelled in seconds counted after task started. Default never. +-- @return #OPSGROUP.Task The task structure. +function OPSGROUP:AddTask(task, clock, description, prio, duration) + + local newtask=self:NewTaskScheduled(task, clock, description, prio, duration) + + -- Add to table. + table.insert(self.taskqueue, newtask) + + -- Info. + self:I(self.lid..string.format("Adding SCHEDULED task %s starting at %s", newtask.description, UTILS.SecondsToClock(newtask.time, true))) + self:T3({newtask=newtask}) + + return newtask +end + +--- Create a *scheduled* task. +-- @param #OPSGROUP self +-- @param #table task DCS task table structure. +-- @param #string clock Mission time when task is executed. Default in 5 seconds. If argument passed as #number, it defines a relative delay in seconds. +-- @param #string description Brief text describing the task, e.g. "Attack SAM". +-- @param #number prio Priority of the task. +-- @param #number duration Duration before task is cancelled in seconds counted after task started. Default never. +-- @return #OPSGROUP.Task The task structure. +function OPSGROUP:NewTaskScheduled(task, clock, description, prio, duration) + + -- Increase counter. + self.taskcounter=self.taskcounter+1 + + -- Set time. + local time=timer.getAbsTime()+5 + if clock then + if type(clock)=="string" then + time=UTILS.ClockToSeconds(clock) + elseif type(clock)=="number" then + time=timer.getAbsTime()+clock + end + end + + -- Task data structure. + local newtask={} --#OPSGROUP.Task + newtask.status=OPSGROUP.TaskStatus.SCHEDULED + newtask.dcstask=task + newtask.description=description or task.id + newtask.prio=prio or 50 + newtask.time=time + newtask.id=self.taskcounter + newtask.duration=duration + newtask.waypoint=-1 + newtask.type=OPSGROUP.TaskType.SCHEDULED + newtask.stopflag=USERFLAG:New(string.format("%s StopTaskFlag %d", self.groupname, newtask.id)) + newtask.stopflag:Set(0) + + return newtask +end + +--- Add a *waypoint* task. +-- @param #FLIGHTGROUP self +-- @param #table task DCS task table structure. +-- @param #number waypointindex Number of waypoint. Counting starts at one! Default is the as *next* waypoint. +-- @param #string description Brief text describing the task, e.g. "Attack SAM". +-- @param #number prio Priority of the task. Number between 1 and 100. Default is 50. +-- @param #number duration Duration before task is cancelled in seconds counted after task started. Default never. +-- @return #FLIGHTGROUP.Task The task structure. +function FLIGHTGROUP:AddTaskWaypoint(task, waypointindex, description, prio, duration) + + -- Increase counter. + self.taskcounter=self.taskcounter+1 + + -- Task data structure. + local newtask={} --#FLIGHTGROUP.Task + newtask.description=description + newtask.status=FLIGHTGROUP.TaskStatus.SCHEDULED + newtask.dcstask=task + newtask.prio=prio or 50 + newtask.id=self.taskcounter + newtask.duration=duration + newtask.time=0 + newtask.waypoint=waypointindex or (self.currentwp and self.currentwp+1 or 2) + newtask.type=FLIGHTGROUP.TaskType.WAYPOINT + newtask.stopflag=USERFLAG:New(string.format("%s StopTaskFlag %d", self.groupname, newtask.id)) + newtask.stopflag:Set(0) + + -- Add to table. + table.insert(self.taskqueue, newtask) + + -- Info. + self:I(self.lid..string.format("Adding WAYPOINT task %s at WP %d", newtask.description, newtask.waypoint)) + self:T3({newtask=newtask}) + + -- Update route. + --self:_CheckFlightDone(1) + self:__UpdateRoute(-1) + + return newtask +end + +--- Add an *enroute* task. +-- @param #FLIGHTGROUP self +-- @param #table task DCS task table structure. +function FLIGHTGROUP:AddTaskEnroute(task) + + if not self.taskenroute then + self.taskenroute={} + end + + -- Check not to add the same task twice! + local gotit=false + for _,Task in pairs(self.taskenroute) do + if Task.id==task.id then + gotit=true + break + end + end + + if not gotit then + table.insert(self.taskenroute, task) + end + +end + +--- Remove task from task queue. +-- @param #FLIGHTGROUP self +-- @param #FLIGHTGROUP.Task Task The task to be removed from the queue. +-- @return #boolean True if task could be removed. +function FLIGHTGROUP:RemoveTask(Task) + + for i=#self.taskqueue,1,-1 do + local task=self.taskqueue[i] --#FLIGHTGROUP.Task + + if task.id==Task.id then + + -- Remove task from queue. + table.remove(self.taskqueue, i) + + -- Update route if this is a waypoint task. + if task.type==FLIGHTGROUP.TaskType.WAYPOINT and task.status==FLIGHTGROUP.TaskStatus.SCHEDULED then + self:_CheckFlightDone(1) + --self:__UpdateRoute(-1) + end + + return true + end + end + + return false +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Mission Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add mission to queue. +-- @param #FLIGHTGROUP self +-- @param Ops.Auftrag#AUFTRAG Mission Mission for this group. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:AddMission(Mission) + + -- Add flight group to mission. + Mission:AddFlightGroup(self) + + -- Set flight status to SCHEDULED.. + Mission:SetFlightStatus(self, AUFTRAG.FlightStatus.SCHEDULED) + + -- Set mission status to SCHEDULED. + Mission:Scheduled() + + -- Add mission to queue. + table.insert(self.missionqueue, Mission) + + -- Info text. + local text=string.format("Added %s mission %s starting at %s, stopping at %s", + tostring(Mission.type), tostring(Mission.name), UTILS.SecondsToClock(Mission.Tstart, true), Mission.Tstop and UTILS.SecondsToClock(Mission.Tstop, true) or "INF") + self:I(self.lid..text) + + return self +end + +--- Remove mission from queue. +-- @param #FLIGHTGROUP self +-- @param Ops.Auftrag#AUFTRAG Mission Mission to be removed. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:RemoveMission(Mission) + + for i,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission.auftragsnummer==Mission.auftragsnummer then + + -- Remove mission waypoint task. + local Task=Mission:GetFlightWaypointTask(self) + + if Task then + self:RemoveTask(Task) + end + + -- Remove mission from queue. + table.remove(self.missionqueue, i) + + return self + end + + end + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Events +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "PassingWaypoint" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number n Waypoint passed. +-- @param #number N Total number of waypoints. +function OPSGROUP:onafterPassingWaypoint(From, Event, To, n, N) + self:I(self.lid..string.format("Passed waypoint %d of %d", n, N)) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Routing +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set DCS task. Enroute tasks are injected automatically. +-- @param #OPSGROUP self +-- @param #table DCSTask DCS task structure. +-- @return #OPSGROUP self +function OPSGROUP:SetTask(DCSTask) + + if self:IsAlive() then + + -- Set task. + self.group:SetTask(DCSTask) + + -- Debug info. + local text=string.format("SETTING Task %s", tostring(DCSTask.id)) + if tostring(DCSTask.id)=="ComboTask" then + for i,task in pairs(DCSTask.params.tasks) do + text=text..string.format("\n[%d] %s", i, tostring(task.id)) + end + end + self:I(self.lid..text) + end + + return self +end + +--- Check if flight is alive. +-- @param #OPSGROUP self +-- @return #boolean *true* if group is exists and is activated, *false* if group is exist but is NOT activated. *nil* otherwise, e.g. the GROUP object is *nil* or the group is not spawned yet. +function OPSGROUP:IsAlive() + + if self.group then + return self.group:IsAlive() + end + + return nil +end + +--- Route group along waypoints. Enroute tasks are also applied. +-- @param #OPSGROUP self +-- @param #table waypoints Table of waypoints. +-- @return #OPSGROUP self +function OPSGROUP:Route(waypoints) + + if self:IsAlive() then + + -- DCS task combo. + local Tasks={} + + -- Route (Mission) task. + local TaskRoute=self.group:TaskRoute(waypoints) + table.insert(Tasks, TaskRoute) + + -- TaskCombo of enroute and mission tasks. + local TaskCombo=self.group:TaskCombo(Tasks) + + -- Set tasks. + if #Tasks>1 then + self:SetTask(TaskCombo) + else + self:SetTask(TaskRoute) + end + + else + self:E(self.lid.."ERROR: Group is not alive!") + end + + return self +end + +--- Initialize Mission Editor waypoints. +-- @param #OPSGROUP self +-- @param #table waypoints Table of waypoints. Default is from group template. +-- @return #OPSGROUP self +function OPSGROUP:InitWaypoints(waypoints) + + -- Template waypoints. + self.waypoints0=self.group:GetTemplateRoutePoints() + + -- Waypoints of group as defined in the ME. + self.waypoints=waypoints or UTILS.DeepCopy(self.waypoints0) + + -- Debug info. + self:T(self.lid..string.format("Initializing %d waypoints", #self.waypoints)) + + -- Update route. + if #self.waypoints>0 then + + -- Check if only 1 wp? + if #self.waypoints==1 then + self.passedfinalwp=true + end + + -- Update route (when airborne). + self:__UpdateRoute(-1) + end + + return self +end + +--- Initialize Mission Editor waypoints. +-- @param #OPSGROUP self +function OPSGROUP:_UpdateWaypointTasks() + + local waypoints=self.waypoints + local nwaypoints=#waypoints + + for i,wp in pairs(waypoints) do + + if i>self.currentwp or nwaypoints==1 then + + -- Debug info. + self:T2(self.lid..string.format("Updating waypoint task for waypoint %d/%d. Last waypoint passed %d.", i, nwaypoints, self.currentwp)) + + -- Tasks of this waypoint + local taskswp={} + + -- At each waypoint report passing. + local TaskPassingWaypoint=self.group:TaskFunction("OPSGROUP._PassingWaypoint", self, i) + table.insert(taskswp, TaskPassingWaypoint) + + -- Waypoint task combo. + wp.task=self.group:TaskCombo(taskswp) + + end + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Global Task Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Function called when a group is passing a waypoint. +--@param Wrapper.Group#GROUP group Group that passed the waypoint +--@param #OPSGROUP opsgroup Ops group object. +--@param #number i Waypoint number that has been reached. +function OPSGROUP._PassingWaypoint(group, opsgroup, i) + + local final=#opsgroup.waypoints or 1 + + -- Debug message. + local text=string.format("Group passing waypoint %d of %d", i, final) + opsgroup:T3(opsgroup.lid..text) + + -- Set current waypoint. + opsgroup.currentwp=i + + -- Trigger PassingWaypoint event. + opsgroup:PassingWaypoint(i, final) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------