--- **Ops** - (R2.5) - AI Flight Group for Ops. -- -- **Main Features:** -- -- * Nice stuff. -- -- === -- -- ### 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 #string sid Class id string for output to DCS log file. -- @field #string groupname Name of flight group. -- @field Wrapper.Group#GROUP flightgroup Flight group object. -- @field #string type Aircraft type of flight group. -- @field #table element Table of elements, i.e. units of the group. -- @field #table taskqueue Queue of tasks. -- @field Core.Set#SET_UNIT detectedunits Set of detected units. -- @extends Core.Fsm#FSM --- Be surprised! -- -- === -- -- ![Banner Image](..\Presentations\CarrierAirWing\FLIGHTGROUP_Main.jpg) -- -- # The FLIGHTGROUP Concept -- -- -- -- @field #FLIGHTGROUP FLIGHTGROUP = { ClassName = "FLIGHTGROUP", sid = nil, groupname = nil, flightgroup = nil, type = nil, element = {}, taskqueue = {}, detectedunits = nil, } --- Status of flight group element. -- @type FLIGHTGROUP.ElementStatus -- @field #string SPAWNED Element was spawned into the world. -- @field #string PARKING Element is parking after spawned on ramp. -- @field #string TAXIING Element is taxiing after engine startup. -- @field #string AIRBORNE Element is airborne after take off. -- @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={ SPAWNED="spawned", PARKING="parking", TAXIING="taxiing", AIRBORNE="airborne", LANDED="landed", ARRIVED="arrived", DEAD="dead", } --- Flight group element. -- @type FLIGHTGROUP.Element -- @field #string name Name of the element. -- @field #number modex Tail number. -- @field #boolean ai If true, element is AI. -- @field #string skill Skill level. -- @field Wrapper.Unit#UNIT unit Element unit object. -- @field #string status Status, i.e. born, parking, taxiing. --- Flight group tasks. -- @type FLIGHTGROUP.Task -- @param #string INTERCEPT Intercept task. -- @param #string CAP Combat Air Patrol task.s -- @param #string BAI Battlefield Air Interdiction task. -- @param #string SEAD Suppression/destruction of enemy air defences. -- @param #string STRIKE Strike task. -- @param #string AWACS AWACS task. -- @param #string TANKER Tanker task. FLIGHTGROUP.Task={ INTERCEPT="Intercept", CAP="CAP", BAI="BAI", SEAD="SEAD", STRIKE="Strike", CAS="CAS", AWACS="AWACS", TANKER="Tanker", } --- FLIGHTGROUP class version. -- @field #string version FLIGHTGROUP.version="0.0.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: A lot! ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Create a new FLIGHTGROUP object and start the FSM. -- @param #FLIGHTGROUP self -- @param #string groupname Name of the group. -- @return #FLIGHTGROUP self function FLIGHTGROUP:New(groupname) -- Inherit everything from WAREHOUSE class. local self=BASE:Inherit(self, FSM:New()) -- #FLIGHTGROUP --self.flightgroup=AIGroup self.groupname=tostring(groupname) -- Set some string id for output to DCS.log file. self.sid=string.format("FLIGHTGROUP %s | ", self.groupname) -- Start State. self:SetStartState("Stopped") -- Check if the group is already alive and if so, add its elements. local group=GROUP:FindByName(groupname) if group and group:IsAlive() then self.flightgroup=group local units=group:GetUnits() for _,_unit in pairs(units) do local unit=_unit --Wrapper.Unit#UNIT local element=self:AddElementByName(unit:GetName()) -- Trigger Spawned event. element.status=FLIGHTGROUP.ElementStatus.SPAWNED self:ElementSpawned(element) -- Trigger Airborne event if unit was spawned in air. if unit:InAir() then element.status=FLIGHTGROUP.ElementStatus.AIRBORNE self:ElementAirborne(element) end end end -- Init set of detected units. self.detectedunits=SET_UNIT:New() -- Add FSM transitions. -- From State --> Event --> To State self:AddTransition("Stopped", "Start", "Running") -- Start FSM. self:AddTransition("*", "FlightStatus", "*") -- FLIGHTGROUP status update. self:AddTransition("*", "AddDetectedUnit", "*") -- Add a newly detected unit to the detected units set. self:AddTransition("*", "ElementTakeoff", "*") -- An element took off. self:AddTransition("*", "ElementLanded", "*") -- An element landed. self:AddTransition("*", "ElementArrived", "*") -- An element arrived. self:AddTransition("*", "ElementDead", "*") -- An element crashed, ejected, or pilot dead. self:AddTransition("*", "FlightSpawned", "Spawned") -- The whole flight group is airborne. self:AddTransition("*", "FlightParking", "Parking") -- The whole flight group is airborne. self:AddTransition("*", "FlightTaxiing", "Taxiing") -- The whole flight group is airborne. self:AddTransition("*", "FlightAirborne", "Airborne") -- The whole flight group is airborne. self:AddTransition("*", "FlightLanded", "Landed") -- The whole flight group has landed. self:AddTransition("*", "FlightArrived", "Arrived") -- The whole flight group has arrived. self:AddTransition("*", "FlightDead", "*") -- 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 "SkipperStatus" after a delay. -- @function [parent=#FLIGHTGROUP] __FlightStatus -- @param #FLIGHTGROUP self -- @param #number delay Delay in seconds. -- Debug trace. if true then self.Debug=true BASE:TraceOnOff(true) BASE:TraceClass(self.ClassName) BASE:TraceLevel(1) end self:Start() return self end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- User functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Get set of decteded units. -- @param #FLIGHTGROUP self -- @return Core.Set#SET_UNIT Set of detected units. function FLIGHTGROUP:GetDetectedUnits() return self.detectedunits end --- Check if flight is airborne. -- @param #FLIGHTGROUP self -- @return #boolean function FLIGHTGROUP:IsAirborne() return self:Is("Airborne") end --- Check if flight is airborne. -- @param #FLIGHTGROUP self -- @return #boolean function FLIGHTGROUP:IsLanded() return self:Is("Landed") end --- Check if flight is arrived. -- @param #FLIGHTGROUP self -- @return #boolean function FLIGHTGROUP:IsArrived() return self:Is("Arrived") end --- Check if flight is dead. -- @param #FLIGHTGROUP self -- @return #boolean function FLIGHTGROUP:IsDead() return self:Is("Dead") end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Start & Status ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- On after Start event. Starts the FLIGHTGROUP FSM and event handlers. -- @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:onafterStart(From, Event, To) -- Short info. local text=string.format("Starting flight group %s.", self.groupname) self:I(self.sid..text) -- 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) -- Start the status monitoring. self:__FlightStatus(-1) end --- On after "FlightStatus" event. -- @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:onafterFlightStatus(From, Event, To) -- FSM state. local fsmstate=self:GetState() -- Short info. local text=string.format("Flight group FSM status %s.", fsmstate) self:I(self.sid..text) text="Elements:" for i,_element in pairs(self.element) 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 text=text..string.format("\n[%d] %s: status=%s, fuel=%.1f, life=%.1f", i, name, status, fuel*100, life*100) end if #self.element==0 then text=text.." none!" end self:I(text) -- Get detected DCS units. local detectedtargets=self.flightgroup:GetDetectedTargets() for DetectionObjectID, Detection in pairs(detectedtargets) 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 local unitname=unit:GetName() if self.detectedunits:FindUnit(unitname) then -- Unit is already in the detected unit set. self:I(self.sid..string.format("Detected unit %s is already known.", unitname)) else self:AddDetectedUnit(unit) end end end end -- Next check in ~30 seconds. self:__FlightStatus(-30) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Events ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Flightgroup event function, handling the birth of a unit. -- @param #FLIGHTGROUP self -- @param Core.Event#EVENTDATA EventData Event data. function FLIGHTGROUP:OnEventBirth(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 self.flightgroup=self.flightgroup or EventData.IniGroup if not self:_IsElement(unitname) then local element=self:AddElementByName(unitname) self:_UpdateStatus(element, FLIGHTGROUP.ElementStatus.SPAWNED) self:I(self.sid..string.format("Element %s spawned", element.name)) 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 self:_UpdateStatus(element, FLIGHTGROUP.ElementStatus.TAXIING) self:I(self.sid..string.format("Element %s started engines ==> taxiing", element.name)) 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:_UpdateStatus(element, FLIGHTGROUP.ElementStatus.AIRBORNE) self:ElementTakeoff(element) self:I(self.sid..string.format("Element %s took off ==> airborne", element.name)) 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) if element then self:_UpdateStatus(element, FLIGHTGROUP.ElementStatus.LANDED) self:ElementLanded(element) self:I(self.sid..string.format("Element %s landed", element.name)) 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 local coord=unit:GetCoordinate() local airbase=coord:GetClosestAirbase() local _,_,dist,parking=coord:GetClosestParkingSpot(airbase) if dist and dist<10 and unit:InAir()==false then self:_UpdateStatus(element, FLIGHTGROUP.ElementStatus.ARRIVED) self:ElementArrived(element) self:I(self.sid..string.format("Element %s shut down engines ==> arrived", element.name)) else element.status=FLIGHTGROUP.ElementStatus.DEAD self:I(self.sid..string.format("Element %s started engines (in air) ==> dead", element.name)) 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: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:_UpdateStatus(element, FLIGHTGROUP.ElementStatus.DEAD) self:ElementDead(element) self:I(self.sid..string.format("Element %s crashed ==> dead", element.name)) end end end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- FSM functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- 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 -- @parma #string assignment The (optional) assignment for the asset. function FLIGHTGROUP:onafterAddDetectedUnit(From, Event, To, Unit) self:I(self.sid..string.format("Detected unit %s.", Unit:GetName())) self.detectedunits:AddUnit(Unit) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Misc functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- 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="unknown" table.insert(self.element, 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.element) 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 -- @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.element) 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.element) 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) local similar=true for _,_element in pairs(self.element) do local element=_element --#FLIGHTGROUP.Element if element.status~=FLIGHTGROUP.ElementStatus.DEAD then if status==FLIGHTGROUP.ElementStatus.SPAWNED then if element.status~=status or element.status~=FLIGHTGROUP.ElementStatus.PARKING or element.status~=FLIGHTGROUP.ElementStatus.TAXIING or element.status~=FLIGHTGROUP.ElementStatus.AIRBORNE or element.status~=FLIGHTGROUP.ElementStatus.LANDED or element.status~=FLIGHTGROUP.ElementStatus.ARRIVED then return false end elseif status==FLIGHTGROUP.ElementStatus.PARKING then if element.status~=status or element.status~=FLIGHTGROUP.ElementStatus.TAXIING or element.status~=FLIGHTGROUP.ElementStatus.AIRBORNE or element.status~=FLIGHTGROUP.ElementStatus.LANDED or element.status~=FLIGHTGROUP.ElementStatus.ARRIVED then return false end elseif status==FLIGHTGROUP.ElementStatus.TAXIING then if element.status~=status or element.status~=FLIGHTGROUP.ElementStatus.AIRBORNE or element.status~=FLIGHTGROUP.ElementStatus.LANDED or element.status~=FLIGHTGROUP.ElementStatus.ARRIVED then return false end elseif status==FLIGHTGROUP.ElementStatus.LANDED then if element.status~=status or element.status~=FLIGHTGROUP.ElementStatus.ARRIVED then return false end elseif status==FLIGHTGROUP.ElementStatus.ARRIVED then if element.status~=status then return false end end 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 --DEAD end 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 function FLIGHTGROUP:_UpdateStatus(element, newstatus) -- Old status. local oldstatus=element.status -- Update status of element. element.status=newstatus local group=element.group if newstatus==FLIGHTGROUP.ElementStatus.SPAWNED then --- -- SPAWNED --- self:ElementSpawned(element) if self:_AllSimilarStatus(newstatus) then self:FlightSpawned(group) end elseif newstatus==FLIGHTGROUP.ElementStatus.PARKING then --- -- PARKING --- self:ElementParking(element) if self:_AllSimilarStatus(newstatus) then self:FlightParking(group) end elseif newstatus==FLIGHTGROUP.ElementStatus.TAXIING then --- -- TAXIING --- self:ElementParking(element) if self:_AllSimilarStatus(newstatus) then self:FlightParking(group) end elseif newstatus==FLIGHTGROUP.ElementStatus.AIRBORNE then --- -- AIRBORNE --- self:ElementAirborne(element) if self:_AllSimilarStatus(newstatus) then if self:IsTaxiing() then self:FlightAirborne(group) elseif self:IsParking() then self:FlightTaxiing(group) self:FlightAirborne(group) elseif self:IsSpawned() then self:FlightParking(group) self:FlightTaxiing(group) self:FlightAirborne(group) end end elseif newstatus==FLIGHTGROUP.ElementStatus.LANDED then --- -- LANDED --- self:ElementLanded(element) if self:_AllSimilarStatus(newstatus) then self:FlightLanded(group) end elseif newstatus==FLIGHTGROUP.ElementStatus.ARRIVED then --- -- ARRIVED --- self:ElementArrived(element) if self:_AllSimilarStatus(newstatus) then if self:IsLanded() then self:FlightArrived(group) elseif self:IsAirborne() then self:FlightLanded(group) self:FlightArrived(group) end end end 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:_CheckAllStatus(status) if self:_CheckAllSameStatus(FLIGHTGROUP.ElementStatus.DEAD) and not self:IsDead() then -- All elements are dead. self:FlightDead() elseif self:_CheckAllSameStatus(FLIGHTGROUP.ElementStatus.SPAWNED) then elseif self:_CheckAllSameStatus(FLIGHTGROUP.ElementStatus.PARKING) then elseif self:_CheckAllSameStatus(FLIGHTGROUP.ElementStatus.TAXIING) then elseif self:_CheckAllSameStatus(FLIGHTGROUP.ElementStatus.AIRBORNE) then elseif self:_CheckAllSameStatus(FLIGHTGROUP.ElementStatus.LANDED) then self:FlightArrived() elseif self:_CheckAllSameStatus(FLIGHTGROUP.ElementStatus.ARRIVED) and not self:IsArrived() then self:FlightArrived() end if status==FLIGHTGROUP.ElementStatus.DEAD then elseif status==FLIGHTGROUP.ElementStatus.ARRIVED then elseif status==FLIGHTGROUP.ElementStatus.AIRBORNE then self:FlightAirborne() end return true end --- Get onboard number. -- @param #AIRBOSS 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 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------