diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index 22f23bb75..f2d48f6a2 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -37,6 +37,7 @@ __Moose.Include( 'Scripts/Moose/Wrapper/Client.lua' ) __Moose.Include( 'Scripts/Moose/Wrapper/Static.lua' ) __Moose.Include( 'Scripts/Moose/Wrapper/Airbase.lua' ) __Moose.Include( 'Scripts/Moose/Wrapper/Scenery.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/Marker.lua' ) __Moose.Include( 'Scripts/Moose/Cargo/Cargo.lua' ) __Moose.Include( 'Scripts/Moose/Cargo/CargoUnit.lua' ) @@ -69,6 +70,11 @@ __Moose.Include( 'Scripts/Moose/Ops/Airboss.lua' ) __Moose.Include( 'Scripts/Moose/Ops/RecoveryTanker.lua' ) __Moose.Include( 'Scripts/Moose/Ops/RescueHelo.lua' ) __Moose.Include( 'Scripts/Moose/Ops/ATIS.lua' ) +__Moose.Include( 'Scripts/Moose/Ops/AirWing.lua' ) +__Moose.Include( 'Scripts/Moose/Ops/Auftrag.lua' ) +__Moose.Include( 'Scripts/Moose/Ops/FlightGroup.lua' ) +__Moose.Include( 'Scripts/Moose/Ops/NavyGroup.lua' ) +__Moose.Include( 'Scripts/Moose/Ops/Squadron.lua' ) __Moose.Include( 'Scripts/Moose/AI/AI_Balancer.lua' ) __Moose.Include( 'Scripts/Moose/AI/AI_Air.lua' ) diff --git a/Moose Development/Moose/Ops/AirWing.lua b/Moose Development/Moose/Ops/AirWing.lua new file mode 100644 index 000000000..e12e082c1 --- /dev/null +++ b/Moose Development/Moose/Ops/AirWing.lua @@ -0,0 +1,2274 @@ +--- **Ops** - Airwing Warehouse. +-- +-- **Main Features:** +-- +-- * Manage squadrons. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Ops.Airwing +-- @image OPS_AirWing.png + + +--- AIRWING class. +-- @type AIRWING +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. Messages to all about status. +-- @field #number verbose Verbosity of output. +-- @field #string lid Class id string for output to DCS log file. +-- @field #table menu Table of menu items. +-- @field #table squadrons Table of squadrons. +-- @field #table missionqueue Mission queue table. +-- @field #table payloads Playloads for specific aircraft and mission types. +-- @field #number payloadcounter Running index of payloads. +-- @field Core.Set#SET_ZONE zonesetCAP Set of CAP zones. +-- @field Core.Set#SET_ZONE zonesetTANKER Set of TANKER zones. +-- @field Core.Set#SET_ZONE zonesetAWACS Set of AWACS zones. +-- @field #number nflightsCAP Number of CAP flights constantly in the air. +-- @field #number nflightsAWACS Number of AWACS flights constantly in the air. +-- @field #number nflightsTANKERboom Number of TANKER flights with BOOM constantly in the air. +-- @field #number nflightsTANKERprobe Number of TANKER flights with PROBE constantly in the air. +-- @field #number nflightsRescueHelo Number of Rescue helo flights constantly in the air. +-- @field #table pointsCAP Table of CAP points. +-- @field #table pointsTANKER Table of Tanker points. +-- @field #table pointsAWACS Table of AWACS points. +-- @field Ops.WingCommander#WINGCOMMANDER wingcommander The wing commander responsible for this airwing. +-- +-- @field Ops.RescueHelo#RESCUEHELO rescuehelo The rescue helo. +-- @field Ops.RecoveryTanker#RECOVERYTANKER recoverytanker The recoverytanker. +-- +-- @extends Functional.Warehouse#WAREHOUSE + +--- Be surprised! +-- +-- === +-- +-- ![Banner Image](..\Presentations\AirWing\AIRWING_Main.jpg) +-- +-- # The AIRWING Concept +-- +-- An AIRWING consists of multiple SQUADRONS. These squadrons "live" in a WAREHOUSE, i.e. a physical structure that is connected to an airbase (airdrome, FRAP or ship). +-- For an airwing to be operational, it needs airframes, weapons/fuel and an airbase. +-- +-- # Create an Airwing +-- +-- ## Constructing the Airwing +-- +-- airwing=AIRWING:New("Warehouse Batumi", "8th Fighter Wing") +-- airwing:Start() +-- +-- The first parameter specified the warehouse, i.e. the static building housing the airwing (or the name of the aircraft carrier). The second parameter is optional +-- and sets an alias. +-- +-- ## Adding Squadrons +-- +-- At this point the airwing does not have any assets (aircraft). In order to add these, one needs to first define SQUADRONS. +-- +-- VFA151=SQUADRON:New("F-14 Group", 8, "VFA-151 (Vigilantes)") +-- VFA151:AddMissonCapability({AUFTRAG.Type.PATROL, AUFTRAG.Type.INTERCEPT}) +-- +-- airwing:AddSquadron(VFA151) +-- +-- This adds eight Tomcat groups beloning to VFA-151 to the airwing. This squadron has the ability to perform combat air patrols and intercepts. +-- +-- ## Adding Payloads +-- +-- Adding pure airframes is not enough. The aircraft also need weapons (and fuel) for certain missions. These must be given to the airwing from template groups +-- defined in the Mission Editor. +-- +-- -- F-14 payloads for CAP and INTERCEPT. Phoenix are first, sparrows are second choice. +-- airwing:NewPayload(GROUP:FindByName("F-14 Payload AIM-54C"), 2, {AUFTRAG.Type.INTERCEPT, AUFTRAG.Type.PATROL}, 80) +-- airwing:NewPayload(GROUP:FindByName("F-14 Payload AIM-7M"), 20, {AUFTRAG.Type.INTERCEPT, AUFTRAG.Type.PATROL}) +-- +-- This will add two AIM-54C and 20 AIM-7M payloads. +-- +-- If the airwing gets an intercept or patrol mission assigned, it will first use the AIM-54s. Once these are consumed, the AIM-7s are attached to the aircraft. +-- +-- When an airwing does not have a payload for a certain mission type, the mission cannot be carried out. +-- +-- You can set the number of payloads to "unlimited" by setting its quantity to -1. +-- +-- # Adding Missions +-- +-- Various mission types can be added easily via the AUFTRAG class. +-- +-- Once you created an AUFTRAG you can add it to the AIRWING with the :AddMission(mission) function. +-- +-- This mission will be put into the AIRWING queue. Once the mission start time is reached and all resources (airframes and pylons) are available, the mission is started. +-- If the mission stop time is over (and the mission is not finished), it will be cancelled and removed from the queue. This applies also to mission that were not even +-- started. +-- +-- # Command an Airwing +-- +-- An airwing can receive missions from a WINGCOMMANDER. See docs of that class for details. +-- +-- However, you are still free to add missions at anytime. +-- +-- +-- @field #AIRWING +AIRWING = { + ClassName = "AIRWING", + Debug = false, + verbose = 0, + lid = nil, + menu = nil, + squadrons = {}, + missionqueue = {}, + payloads = {}, + pointsCAP = {}, + pointsTANKER = {}, + pointsAWACS = {}, + wingcommander = nil, +} + +--- Squadron asset. +-- @type AIRWING.SquadronAsset +-- @field #AIRWING.Payload payload The payload of the asset. +-- @field Ops.FlightGroup#FLIGHTGROUP flightgroup The flightgroup object. +-- @field #string squadname Name of the squadron this asset belongs to. +-- @extends Functional.Warehouse#WAREHOUSE.Assetitem + +--- Payload data. +-- @type AIRWING.Payload +-- @field #string unitname Name of the unit this pylon was extracted from. +-- @field #string aircrafttype Type of aircraft, which can use this payload. +-- @field #table capabilities Mission types and performances for which this payload can be used. +-- @field #table pylons Pylon data extracted for the unit template. +-- @field #number navail Number of available payloads of this type. +-- @field #boolean unlimited If true, this payload is unlimited and does not get consumed. + +--- Patrol data. +-- @type AIRWING.PatrolData +-- @field #string type Type name. +-- @field Core.Point#COORDINATE coord Patrol coordinate. +-- @field #number altitude Altitude in feet. +-- @field #number heading Heading in degrees. +-- @field #number leg Leg length in NM. +-- @field #number speed Speed in knots. +-- @field #number noccupied Number of flights on this patrol point. +-- @field Wrapper.Marker#MARKER marker F10 marker. + +--- AIRWING class version. +-- @field #string version +AIRWING.version="0.2.1" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Spawn in air or hot ==> Needs WAREHOUSE update. +-- TODO: Make special request to transfer squadrons to anther airwing (or warehouse). +-- TODO: Check that airbase has enough parking spots if a request is BIG. Alternatively, split requests. +-- DONE: Add squadrons to warehouse. +-- DONE: Build mission queue. +-- DONE: Find way to start missions. +-- DONE: Check if missions are done/cancelled. +-- DONE: Payloads as resources. +-- DONE: Define CAP zones. +-- DONE: Define TANKER zones for refuelling. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new AIRWING class object for a specific aircraft carrier unit. +-- @param #AIRWING self +-- @param #string warehousename Name of the warehouse static or unit object representing the warehouse. +-- @param #string airwingname Name of the air wing, e.g. "AIRWING-8". +-- @return #AIRWING self +function AIRWING:New(warehousename, airwingname) + + -- Inherit everything from WAREHOUSE class. + local self=BASE:Inherit(self, WAREHOUSE:New(warehousename, airwingname)) -- #AIRWING + + -- Nil check. + if not self then + BASE:E(string.format("ERROR: Could not find warehouse %s!", warehousename)) + return nil + end + + -- Set some string id for output to DCS.log file. + self.lid=string.format("AIRWING %s | ", self.alias) + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("*", "MissionRequest", "*") -- Add a (mission) request to the warehouse. + self:AddTransition("*", "MissionCancel", "*") -- Cancel mission. + + -- Defaults: + self.nflightsCAP=0 + self.nflightsAWACS=0 + self.nflightsTANKERboom=0 + self.nflightsTANKERprobe=0 + self.nflightsRecoveryTanker=0 + self.nflightsRescueHelo=0 + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the AIRWING. Initializes parameters and starts event handlers. + -- @function [parent=#AIRWING] Start + -- @param #AIRWING self + + --- Triggers the FSM event "Start" after a delay. Starts the AIRWING. Initializes parameters and starts event handlers. + -- @function [parent=#AIRWING] __Start + -- @param #AIRWING self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the AIRWING and all its event handlers. + -- @param #AIRWING self + + --- Triggers the FSM event "Stop" after a delay. Stops the AIRWING and all its event handlers. + -- @function [parent=#AIRWING] __Stop + -- @param #AIRWING self + -- @param #number delay Delay in seconds. + + + -- Debug trace. + if false then + self.Debug=true + self:TraceOnOff(true) + self:TraceClass(self.ClassName) + self:TraceLevel(1) + end + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add a squadron to the air wing. +-- @param #AIRWING self +-- @param Ops.Squadron#SQUADRON Squadron The squadron object. +-- @return #AIRWING self +function AIRWING:AddSquadron(Squadron) + + -- Add squadron to airwing. + table.insert(self.squadrons, Squadron) + + -- Add assets to squadron. + self:AddAssetToSquadron(Squadron, Squadron.Ngroups) + + -- Tanker and AWACS get unlimited payloads. + if Squadron.attribute==GROUP.Attribute.AIR_AWACS then + self:NewPayload(Squadron.templategroup, -1, AUFTRAG.Type.AWACS) + elseif Squadron.attribute==GROUP.Attribute.AIR_TANKER then + self:NewPayload(Squadron.templategroup, -1, AUFTRAG.Type.TANKER) + end + + -- Set airwing to squadron. + Squadron:SetAirwing(self) + + -- Start squadron. + if Squadron:IsStopped() then + Squadron:Start() + end + + return self +end + +--- Add a **new** payload to the airwing resources. +-- @param #AIRWING self +-- @param Wrapper.Unit#UNIT Unit The unit, the payload is extracted from. Can also be given as *#string* name of the unit. +-- @param #number Npayloads Number of payloads to add to the airwing resources. Default 99 (which should be enough for most scenarios). Set to -1 for unlimited. +-- @param #table MissionTypes Mission types this payload can be used for. +-- @param #number Performance A number between 0 (worst) and 100 (best) to describe the performance of the loadout for the given mission types. Default is 50. +-- @return #AIRWING.Payload The payload table or nil if the unit does not exist. +function AIRWING:NewPayload(Unit, Npayloads, MissionTypes, Performance) + + -- Default performance. + Performance=Performance or 50 + + if type(Unit)=="string" then + Unit=UNIT:FindByName(Unit) + if not Unit then + Unit=GROUP:FindByName(Unit) + end + end + + -- If a GROUP object was given, get the first unit. + if Unit:IsInstanceOf("GROUP") then + Unit=Unit:GetUnit(1) + end + + -- Ensure Missiontypes is a table. + if MissionTypes and type(MissionTypes)~="table" then + MissionTypes={MissionTypes} + end + + if Unit then + + local payload={} --#AIRWING.Payload + + payload.unitname=Unit:GetName() + payload.aircrafttype=Unit:GetTypeName() + payload.pylons=Unit:GetTemplatePayload() + payload.unlimited=Npayloads<0 + if payload.unlimited then + payload.navail=1 + else + payload.navail=Npayloads or 99 + end + + payload.capabilities={} + for _,missiontype in pairs(MissionTypes) do + local capability={} --Ops.Auftrag#AUFTRAG.Capability + capability.MissionType=missiontype + capability.Performance=Performance + table.insert(payload.capabilities, capability) + end + + -- Add ORBIT for all. + if not self:CheckMissionType(AUFTRAG.Type.ORBIT, MissionTypes) then + local capability={} --Ops.Auftrag#AUFTRAG.Capability + capability.MissionType=AUFTRAG.Type.ORBIT + capability.Performance=50 + table.insert(payload.capabilities, capability) + end + + -- Info + self:I(self.lid..string.format("Adding new payload from unit %s for aircraft type %s: N=%d (unlimited=%s), performance=%d, missions: %s", + payload.unitname, payload.aircrafttype, payload.navail, tostring(payload.unlimited), Performance, table.concat(MissionTypes, ", "))) + + -- Add payload + table.insert(self.payloads, payload) + + return payload + end + + return nil +end + +--- Add a mission capability to an existing payload. +-- @param #AIRWING self +-- @param #AIRWING.Payload Payload The payload table to which the capability should be added. +-- @param #table MissionTypes Mission types to be added. +-- @param #number Performance A number between 0 (worst) and 100 (best) to describe the performance of the loadout for the given mission types. Default is 50. +-- @return #AIRWING self +function AIRWING:AddPayloadCapability(Payload, MissionTypes, Performance) + + -- Ensure Missiontypes is a table. + if MissionTypes and type(MissionTypes)~="table" then + MissionTypes={MissionTypes} + end + + Payload.capabilities=Payload.capabilities or {} + + for _,missiontype in pairs(MissionTypes) do + + local capability={} --Ops.Auftrag#AUFTRAG.Capability + capability.MissionType=missiontype + capability.Performance=Performance + + --TODO: check that capability does not already exist! + + table.insert(Payload.capabilities, capability) + end + + return self +end + +--- Fetch a payload from the airwing resources for a given unit and mission type. +-- The payload with the highest priority is preferred. +-- @param #AIRWING self +-- @param #string UnitType The type of the unit. +-- @param #string MissionType The mission type. +-- @return #AIRWING.Payload Payload table or *nil*. +function AIRWING:FetchPayloadFromStock(UnitType, MissionType) + + -- Quick check if we have any payloads. + if not self.payloads or #self.payloads==0 then + self:I(self.lid.."WARNING: No payloads in stock!") + return nil + end + + -- Debug. + if self.Debug then + self:I(self.lid..string.format("Looking for payload for unit type=%s and mission type=%s", UnitType, MissionType)) + for i,_payload in pairs(self.payloads) do + local payload=_payload --#AIRWING.Payload + local performance=self:GetPayloadPeformance(payload, MissionType) + self:I(self.lid..string.format("[%d] Payload type=%s navail=%d unlimited=%s", i, payload.aircrafttype, payload.navail, tostring(payload.unlimited))) + end + end + + --- Sort payload wrt the following criteria: + -- 1) Highest performance is the main selection criterion. + -- 2) If payloads have the same performance, unlimited payloads are preferred over limited ones. + -- 3) If payloads have the same performance _and_ are limited, the more abundant one is preferred. + local function sortpayloads(a,b) + local pA=a --#AIRWING.Payload + local pB=b --#AIRWING.Payload + if a and b then -- I had the case that a or b were nil even though the self.payloads table was looking okay. Very strange! Seems to be solved by pre-selecting valid payloads. + local performanceA=self:GetPayloadPeformance(a, MissionType) + local performanceB=self:GetPayloadPeformance(b, MissionType) + return (performanceA>performanceB) or (performanceA==performanceB and a.unlimited==true) or (performanceA==performanceB and a.unlimited==true and b.unlimited==true and a.navail>b.navail) + elseif not a then + self:I(self.lid..string.format("FF ERROR in sortpayloads: a is nil")) + return false + elseif not b then + self:I(self.lid..string.format("FF ERROR in sortpayloads: b is nil")) + return true + else + self:I(self.lid..string.format("FF ERROR in sortpayloads: a and b are nil")) + return false + end + end + + -- Pre-selection: filter out only those payloads that are valid for the airframe and mission type and are available. + local payloads={} + for _,_payload in pairs(self.payloads) do + local payload=_payload --#AIRWING.Payload + if payload.aircrafttype==UnitType and self:CheckMissionCapability(MissionType, payload.capabilities) and payload.navail>0 then + table.insert(payloads, payload) + end + end + + -- Debug. + if self.Debug then + self:I(self.lid..string.format("FF Sorted payloads for mission type X and aircraft type=Y:")) + for _,_payload in ipairs(self.payloads) do + local payload=_payload --#AIRWING.Payload + if payload.aircrafttype==UnitType and self:CheckMissionCapability(MissionType, payload.capabilities) then + local performace=self:GetPayloadPeformance(payload, MissionType) + self:I(self.lid..string.format("FF %s payload for %s: avail=%d performace=%d", MissionType, payload.aircrafttype, payload.navail, performace)) + end + end + end + + -- Cases: + if #payloads==0 then + -- No payload available. + self:T(self.lid.."Warning could not find a payload for airframe X mission type Y!") + return nil + elseif #payloads==1 then + -- Only one payload anyway. + return payloads[1] + else + -- Sort payloads. + table.sort(payloads, sortpayloads) + return payloads[1] + end + +end + +--- Return payload from asset back to stock. +-- @param #AIRWING self +-- @param #AIRWING.SquadronAsset asset The squadron asset. +function AIRWING:ReturnPayloadFromAsset(asset) + + local payload=asset.payload + + if payload then + + -- Increase count if not unlimited. + if not payload.unlimited then + payload.navail=payload.navail+1 + end + + -- Remove asset payload. + asset.payload=nil + + else + self:E(self.lid.."ERROR: asset had no payload attached!") + end + +end + + +--- Add asset group(s) to squadron. +-- @param #AIRWING self +-- @param Ops.Squadron#SQUADRON Squadron The squadron object. +-- @param #number Nassets Number of asset groups to add. +-- @return #AIRWING self +function AIRWING:AddAssetToSquadron(Squadron, Nassets) + + if Squadron then + + -- Get the template group of the squadron. + local Group=GROUP:FindByName(Squadron.templatename) + + if Group then + + -- Debug text. + local text=string.format("Adding asset %s to squadron %s", Group:GetName(), Squadron.name) + self:T(self.lid..text) + + -- Add assets to airwing warehouse. + self:AddAsset(Group, Nassets, nil, nil, nil, nil, Squadron.skill, Squadron.livery, Squadron.name) + + else + self:E(self.lid.."ERROR: Group does not exist!") + end + + else + self:E(self.lid.."ERROR: Squadron does not exit!") + end + + return self +end + +--- Get squadron by name. +-- @param #AIRWING self +-- @param #string SquadronName Name of the squadron, e.g. "VFA-37". +-- @return Ops.Squadron#SQUADRON The squadron object. +function AIRWING:GetSquadron(SquadronName) + + for _,_squadron in pairs(self.squadrons) do + local squadron=_squadron --Ops.Squadron#SQUADRON + + if squadron.name==SquadronName then + return squadron + end + + end + + return nil +end + +--- Get squadron of an asset. +-- @param #AIRWING self +-- @param #AIRWING.SquadronAsset Asset The squadron asset. +-- @return Ops.Squadron#SQUADRON The squadron object. +function AIRWING:GetSquadronOfAsset(Asset) + return self:GetSquadron(Asset.squadname) +end + +--- Remove asset from squadron. +-- @param #AIRWING self +-- @param #AIRWING.SquadronAsset Asset +function AIRWING:RemoveAssetFromSquadron(Asset) + local squad=self:GetSquadronOfAsset(Asset) + if squad then + squad:DelAsset(Asset) + end +end + +--- Add mission to queue. +-- @param #AIRWING self +-- @param Ops.Auftrag#AUFTRAG Mission for this group. +-- @return #AIRWING self +function AIRWING:AddMission(Mission) + + -- Set status to QUEUED. This also attaches the airwing to this mission. + Mission:Queued(self) + + -- Add mission to queue. + table.insert(self.missionqueue, Mission) + + -- Info text. + local text=string.format("Added mission %s (type=%s). Starting at %s. Stopping at %s", + tostring(Mission.name), tostring(Mission.type), 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 #AIRWING self +-- @param Ops.Auftrag#AUFTRAG Mission Mission to be removed. +-- @return #AIRWING self +function AIRWING:RemoveMission(Mission) + + for i,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission.auftragsnummer==Mission.auftragsnummer then + table.remove(self.missionqueue, i) + break + end + + end + + return self +end + +--- Set number of CAP flights constantly carried out. +-- @param #AIRWING self +-- @param #number n Number of flights. Default 1. +-- @return #AIRWING self +function AIRWING:SetNumberCAP(n) + self.nflightsCAP=n or 1 + return self +end + +--- Set number of TANKER flights constantly in the air. +-- @param #AIRWING self +-- @param #number Nboom Number of flights. Default 1. +-- @param #number Nprobe Number of flights. Default 1. +-- @return #AIRWING self +function AIRWING:SetNumberTANKER(Nboom, Nprobe) + self.nflightsTANKERboom=Nboom or 1 + self.nflightsTANKERprobe=Nprobe or 1 + return self +end + +--- Set number of AWACS flights constantly in the air. +-- @param #AIRWING self +-- @param #number n Number of flights. Default 1. +-- @return #AIRWING self +function AIRWING:SetNumberAWACS(n) + self.nflightsAWACS=n or 1 + return self +end + +--- Set number of Rescue helo flights constantly in the air. +-- @param #AIRWING self +-- @param #number n Number of flights. Default 1. +-- @return #AIRWING self +function AIRWING:SetNumberRescuehelo(n) + self.nflightsRescueHelo=n or 1 + return self +end + +--- +-- @param #AIRWING self +-- @param #AIRWING.PatrolData point Patrol point table. +-- @return #string Marker text. +function AIRWING:_PatrolPointMarkerText(point) + + local text=string.format("%s Occupied=%d, \nheading=%03d, leg=%d NM, alt=%d ft, speed=%d kts", + point.type, point.noccupied, point.heading, point.leg, point.altitude, point.speed) + + return text +end + +--- Update marker of the patrol point. +-- @param #AIRWING.PatrolData point Patrol point table. +function AIRWING.UpdatePatrolPointMarker(point) + + local text=string.format("%s Occupied=%d\nheading=%03d, leg=%d NM, alt=%d ft, speed=%d kts", + point.type, point.noccupied, point.heading, point.leg, point.altitude, point.speed) + + point.marker:UpdateText(text, 1) + +end + + +--- Create a new generic patrol point. +-- @param #AIRWING self +-- @param #string Type Patrol point type, e.g. "CAP" or "AWACS". Default "Unknown". +-- @param Core.Point#COORDINATE Coordinate Coordinate of the patrol point. Default 10-15 NM away from the location of the airwing. +-- @param #number Altitude Orbit altitude in feet. Default random between Angels 10 and 20. +-- @param #number Heading Heading in degrees. Default random (0, 360] degrees. +-- @param #number LegLength Length of race-track orbit in NM. Default 15 NM. +-- @param #number Speed Orbit speed in knots. Default 350 knots. +-- @return #AIRWING.PatrolData Patrol point table. +function AIRWING:NewPatrolPoint(Type, Coordinate, Altitude, Speed, Heading, LegLength) + + local patrolpoint={} --#AIRWING.PatrolData + patrolpoint.type=Type or "Unknown" + patrolpoint.coord=Coordinate or self:GetCoordinate():Translate(UTILS.NMToMeters(math.random(10, 15)), math.random(360)) + patrolpoint.heading=Heading or math.random(360) + patrolpoint.leg=LegLength or 15 + patrolpoint.altitude=Altitude or math.random(10,20)*1000 + patrolpoint.speed=Speed or 350 + patrolpoint.noccupied=0 + patrolpoint.marker=MARKER:New(Coordinate, "New Patrol Point"):ToAll() + + AIRWING.UpdatePatrolPointMarker(patrolpoint) + + return patrolpoint +end + +--- Add a patrol Point for CAP missions. +-- @param #AIRWING self +-- @param Core.Point#COORDINATE Coordinate Coordinate of the patrol point. +-- @param #number Altitude Orbit altitude in feet. +-- @param #number Speed Orbit speed in knots. +-- @param #number Heading Heading in degrees. +-- @param #number LegLength Length of race-track orbit in NM. +-- @return #AIRWING self +function AIRWING:AddPatrolPointCAP(Coordinate, Altitude, Speed, Heading, LegLength) + + local patrolpoint=self:NewPatrolPoint("CAP", Coordinate, Altitude, Speed, Heading, LegLength) + + table.insert(self.pointsCAP, patrolpoint) + + return self +end + +--- Add a patrol Point for TANKER missions. +-- @param #AIRWING self +-- @param Core.Point#COORDINATE Coordinate Coordinate of the patrol point. +-- @param #number Altitude Orbit altitude in feet. +-- @param #number Speed Orbit speed in knots. +-- @param #number Heading Heading in degrees. +-- @param #number LegLength Length of race-track orbit in NM. +-- @return #AIRWING self +function AIRWING:AddPatrolPointTANKER(Coordinate, Altitude, Speed, Heading, LegLength) + + local patrolpoint=self:NewPatrolPoint("Tanker", Coordinate, Altitude, Speed, Heading, LegLength) + + table.insert(self.pointsTANKER, patrolpoint) + + return self +end + +--- Add a patrol Point for AWACS missions. +-- @param #AIRWING self +-- @param Core.Point#COORDINATE Coordinate Coordinate of the patrol point. +-- @param #number Altitude Orbit altitude in feet. +-- @param #number Speed Orbit speed in knots. +-- @param #number Heading Heading in degrees. +-- @param #number LegLength Length of race-track orbit in NM. +-- @return #AIRWING self +function AIRWING:AddPatrolPointAWACS(Coordinate, Altitude, Speed, Heading, LegLength) + + local patrolpoint=self:NewPatrolPoint("AWACS", Coordinate, Altitude, Speed, Heading, LegLength) + + table.insert(self.pointsAWACS, patrolpoint) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Start & Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Start AIRWING FSM. +-- @param #AIRWING self +function AIRWING:onafterStart(From, Event, To) + + -- Start parent Warehouse. + self:GetParent(self).onafterStart(self, From, Event, To) + + -- Info. + self:I(self.lid..string.format("Starting AIRWING v%s", AIRWING.version)) + + -- Menu. + if false then + + -- Add F10 radio menu. + self:_SetMenuCoalition() + + for _,_squadron in pairs(self.squadrons) do + local squadron=_squadron --Ops.Squadron#SQUADRON + self:_AddSquadonMenu(squadron) + end + + end + +end + +--- Update status. +-- @param #AIRWING self +function AIRWING:onafterStatus(From, Event, To) + + -- Status of parent Warehouse. + self:GetParent(self).onafterStatus(self, From, Event, To) + + local fsmstate=self:GetState() + + -- Check CAP missions. + self:CheckCAP() + + -- Check TANKER missions. + self:CheckTANKER() + + -- Check AWACS missions. + self:CheckAWACS() + + -- Check Rescue Helo missions. + self:CheckRescuhelo() + + -- Count missions not over yet. + local nmissions=self:CountMissionsInQueue() + + -- Count ALL payloads in stock. If any payload is unlimited, this gives 999. + local Npayloads=self:CountPayloadsInStock(AUFTRAG.Type) + + -- General info: + -- TODO: assets total + local text=string.format("Status %s: missions=%d, payloads=%d (%d), squads=%d", fsmstate, nmissions, #self.payloads, Npayloads, #self.squadrons) + self:I(self.lid..text) + + ------------------ + -- Mission Info -- + ------------------ + local text=string.format("Missions Total=%d:", #self.missionqueue) + for i,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + text=text..string.format("\n[%d] %s: Status=%s, Nassets=%d, Prio=%d, ID=%d (%s)", i, mission.type, mission.status, mission.nassets, mission.prio, mission.auftragsnummer, mission.name) + end + self:I(self.lid..text) + + ------------------- + -- Squadron Info -- + ------------------- + local text="Squadrons:" + for i,_squadron in pairs(self.squadrons) do + local squadron=_squadron --Ops.Squadron#SQUADRON + + local callsign=squadron.callsignName and UTILS.GetCallsignName(squadron.callsignName) or "N/A" + local modex=squadron.modex and squadron.modex or -1 + local skill=squadron.skill and tostring(squadron.skill) or "N/A" + + -- Squadron text + text=text..string.format("\n* %s %s: %s*%d/%d, Callsign=%s, Modex=%d, Skill=%s", squadron.name, squadron:GetState(), squadron.aircrafttype, squadron:CountAssetsInStock(), #squadron.assets, callsign, modex, skill) + + -- Loop over all assets. + if self.verbose>0 then + for j,_asset in pairs(squadron.assets) do + local asset=_asset --#AIRWING.SquadronAsset + local assignment=asset.assignment or "none" + local name=asset.templatename + local spawned=tostring(asset.spawned) + local groupname=asset.spawngroupname + local typename=asset.unittype + + local mission=self:GetAssetCurrentMission(asset) + local missiontext="" + if mission then + local distance=asset.flightgroup and UTILS.MetersToNM(mission:GetTargetDistance(asset.flightgroup.group:GetCoordinate())) or 0 + missiontext=string.format(" [%s (%s): status=%s, distance=%.1f NM]", mission.type, mission.name, mission.status, distance) + end + + -- Mission info. + text=text..string.format("\n -[%d] %s*%d \"%s\": spawned=%s, mission=%s%s", j, typename, asset.nunits, asset.spawngroupname, spawned, tostring(self:IsAssetOnMission(asset)), missiontext) + + -- Payload info. + local payload=asset.payload and table.concat(self:GetPayloadMissionTypes(asset.payload), ", ") or "None" + text=text.." payload="..payload + + -- Flight status. + text=text..", flight: " + if asset.flightgroup and asset.flightgroup:IsAlive() then + local status=asset.flightgroup:GetState() + local fuelmin=asset.flightgroup:GetFuelMin() + local fuellow=asset.flightgroup:IsFuelLow() + local fuelcri=asset.flightgroup:IsFuelCritical() + + text=text..string.format("%s fuel=%d", status, fuelmin) + if fuelcri then + text=text.." (critical!)" + elseif fuellow then + text=text.." (low)" + end + + local lifept, lifept0=asset.flightgroup:GetLifePoints() + text=text..string.format(" life=%d/%d", lifept, lifept0) + + local ammo=asset.flightgroup:GetAmmoTot() + text=text..string.format(" ammo=[G=%d, R=%d, B=%d, M=%d]", ammo.Guns, ammo.Rockets, ammo.Bombs, ammo.Missiles) + else + text=text.."N/A" + end + end + + end + end + self:I(self.lid..text) + + -------------- + -- Mission --- + -------------- + + -- Check if any missions should be cancelled. + self:_CheckMissions() + + -- Get next mission. + local mission=self:_GetNextMission() + + -- Request mission execution. + if mission then + self:MissionRequest(mission) + end + +end + +--- Get patrol data +-- @param #AIRWING self +-- @param #table PatrolPoints Patrol data points. +-- @return #AIRWING.PatrolData +function AIRWING:_GetPatrolData(PatrolPoints) + + -- Sort wrt lowest number of flights on this point. + local function sort(a,b) + return a.noccupied0 then + + -- Sort data wrt number of flights at that point. + table.sort(PatrolPoints, sort) + return PatrolPoints[1] + + else + + return self:NewPatrolPoint() + + end + +end + +--- Check how many CAP missions are assigned and add number of missing missions. +-- @param #AIRWING self +-- @return #AIRWING self +function AIRWING:CheckCAP() + + local Ncap=self:CountMissionsInQueue({AUFTRAG.Type.PATROL, AUFTRAG.Type.INTERCEPT}) + + for i=1,self.nflightsCAP-Ncap do + + local patrol=self:_GetPatrolData(self.pointsCAP) + + local altitude=patrol.altitude+1000*patrol.noccupied + + local missionCAP=AUFTRAG:NewPATROL(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg) + + missionCAP.patroldata=patrol + + patrol.noccupied=patrol.noccupied+1 + + AIRWING.UpdatePatrolPointMarker(patrol) + + self:AddMission(missionCAP) + + end + + return self +end + +--- Check how many TANKER missions are assigned and add number of missing missions. +-- @param #AIRWING self +-- @return #AIRWING self +function AIRWING:CheckTANKER() + + local Nboom=0 + local Nprob=0 + + -- Count tanker mission. + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission:IsNotOver() and mission.type==AUFTRAG.Type.TANKER then + if mission.refuelSystem==0 then + Nboom=Nboom+1 + elseif mission.refuelSystem==1 then + Nprob=Nprob+1 + end + + end + + end + + for i=1,self.nflightsTANKERboom-Nboom do + + local patrol=self:_GetPatrolData(self.pointsTANKER) + + local altitude=patrol.altitude+1000*patrol.noccupied + + local mission=AUFTRAG:NewTANKER(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg, 0) + + mission.patroldata=patrol + + patrol.noccupied=patrol.noccupied+1 + + AIRWING.UpdatePatrolPointMarker(patrol) + + self:AddMission(mission) + + end + + for i=1,self.nflightsTANKERprobe-Nprob do + + local patrol=self:_GetPatrolData(self.pointsTANKER) + + local altitude=patrol.altitude+1000*patrol.noccupied + + local mission=AUFTRAG:NewTANKER(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg, 1) + + mission.patroldata=patrol + + patrol.noccupied=patrol.noccupied+1 + + AIRWING.UpdatePatrolPointMarker(patrol) + + self:AddMission(mission) + + end + + return self +end + +--- Check how many AWACS missions are assigned and add number of missing missions. +-- @param #AIRWING self +-- @return #AIRWING self +function AIRWING:CheckAWACS() + + local N=self:CountMissionsInQueue({AUFTRAG.Type.AWACS}) + + for i=1,self.nflightsAWACS-N do + + local patrol=self:_GetPatrolData(self.pointsAWACS) + + local altitude=patrol.altitude+1000*patrol.noccupied + + local mission=AUFTRAG:NewAWACS(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg) + + mission.patroldata=patrol + + patrol.noccupied=patrol.noccupied+1 + + AIRWING.UpdatePatrolPointMarker(patrol) + + self:AddMission(mission) + + end + + return self +end + +--- Check how many Rescue helos are currently in the air. +-- @param #AIRWING self +-- @return #AIRWING self +function AIRWING:CheckRescuhelo() + + local N=self:CountMissionsInQueue({AUFTRAG.Type.RESCUEHELO}) + + local carrier=UNIT:FindByName(self.airbase:GetName()) + + for i=1,self.nflightsRescueHelo-N do + + local mission=AUFTRAG:NewRESCUEHELO(carrier) + + self:AddMission(mission) + + end + + return self +end + +--- Check how many AWACS missions are assigned and add number of missing missions. +-- @param #AIRWING self +-- @param Ops.FlightGroup#FLIGHTGROUP flightgroup The flightgroup. +-- @return #AIRWING.SquadronAsset The tanker asset. +function AIRWING:GetTankerForFlight(flightgroup) + + local tankers=self:GetAssetsOnMission(AUFTRAG.Type.TANKER) + + if #tankers>0 then + + local tankeropt={} + for _,_tanker in pairs(tankers) do + local tanker=_tanker --#AIRWING.SquadronAsset + + -- Check that donor and acceptor use the same refuelling system. + if flightgroup.refueltype and flightgroup.refueltype==tanker.flightgroup.tankertype then + + local tankercoord=tanker.flightgroup.group:GetCoordinate() + local assetcoord=flightgroup.group:GetCoordinate() + + local dist=assetcoord:Get2DDistance(tankercoord) + + table.insert(tankeropt, {tanker=tanker, dist=dist}) + end + end + + -- Sort tankers wrt to distance. + table.sort(tankeropt, function(a,b) return a.dist0 then + self:E(self.lid..string.format("ERROR: mission %s of type %s has already assets attached!", mission.name, mission.type)) + end + mission.assets={} + + -- Assign assets to mission. + for i=1,mission.nassets do + local asset=assets[i] --#AIRWING.SquadronAsset + + -- Should not happen as we just checked! + if not asset.payload then + self:E(self.lid.."ERROR: No payload for asset! This should not happen!") + end + + -- Add asset to mission. + mission:AddAsset(asset) + end + + -- Now return the remaining payloads. + for i=mission.nassets+1,#assets do + local asset=assets[i] --#AIRWING.SquadronAsset + for _,uid in pairs(gotpayload) do + if uid==asset.uid then + self:ReturnPayloadFromAsset(asset) + break + end + end + end + + return mission + end + + end -- mission due? + end -- mission loop + + return nil +end + +--- Calculate the mission score of an asset. +-- @param #AIRWING self +-- @param #AIRWING.SquadronAsset asset Asset +-- @param Ops.Auftrag#AUFTRAG Mission Mission for which the best assets are desired. +-- @param #boolean includePayload If true, include the payload in the calulation if the asset has one attached. +-- @return #number Mission score. +function AIRWING:CalculateAssetMissionScore(asset, Mission, includePayload) + + local score=0 + + -- Prefer highly skilled assets. + if asset.skill==AI.Skill.GOOD then + score=score+10 + elseif asset.skill==AI.Skill.HIGH then + score=score+20 + elseif asset.skill==AI.Skill.EXCELLENT then + score=score+30 + end + + -- Add mission performance to score. + local squad=self:GetSquadronOfAsset(asset) + local missionperformance=squad:GetMissionPeformance(Mission.type) + score=score+missionperformance + + -- Add payload performance to score. + if includePayload and asset.payload then + score=score+self:GetPayloadPeformance(asset.payload, Mission.type) + end + + -- Intercepts need to be carried out quickly. We prefer spawned assets. + if Mission.type==AUFTRAG.Type.INTERCEPT then + if asset.spawned then + self:I("FF adding 25 to asset because it is spawned") + score=score+25 + end + end + + -- TODO: This could be vastly improved. Need to gather ideas during testing. + -- Calculate ETA? Assets on orbit missions should arrive faster even if they are further away. + -- Max speed of assets. + -- Fuel amount? + -- Range of assets? + + return score +end + +--- Optimize chosen assets for the mission at hand. +-- @param #AIRWING self +-- @param #table assets Table of (unoptimized) assets. +-- @param Ops.Auftrag#AUFTRAG Mission Mission for which the best assets are desired. +-- @param #boolean includePayload If true, include the payload in the calulation if the asset has one attached. +function AIRWING:_OptimizeAssetSelection(assets, Mission, includePayload) + + local TargetCoordinate=Mission:GetTargetCoordinate() + + local dStock=self:GetCoordinate():Get2DDistance(TargetCoordinate) + + -- Calculate distance to mission target. + local distmin=math.huge + local distmax=0 + for _,_asset in pairs(assets) do + local asset=_asset --#AIRWING.SquadronAsset + + if asset.spawned then + local group=GROUP:FindByName(asset.spawngroupname) + asset.dist=group:GetCoordinate():Get2DDistance(TargetCoordinate) + else + asset.dist=dStock + end + + if asset.distdistmax then + distmax=asset.dist + end + + end + + -- Calculate the mission score of all assets. + for _,_asset in pairs(assets) do + local asset=_asset --#AIRWING.SquadronAsset + --self:I(string.format("FF asset %s has payload %s", asset.spawngroupname, asset.payload and "yes" or "no!")) + asset.score=self:CalculateAssetMissionScore(asset, Mission, includePayload) + end + + --- Sort assets wrt to their mission score. Higher is better. + local function optimize(a, b) + local assetA=a --#AIRWING.SquadronAsset + local assetB=b --#AIRWING.SquadronAsset + + -- Higher score wins. If equal score ==> closer wins. + -- TODO: Need to include the distance in a smarter way! + return (assetA.score>assetB.score) or (assetA.score==assetB.score and assetA.dist0 then + + --local text=string.format("Requesting assets for mission %s:", Mission.name) + for i,_asset in pairs(Assetlist) do + local asset=_asset --#AIRWING.SquadronAsset + + -- Set asset to requested! Important so that new requests do not use this asset! + asset.requested=true + + if Mission.missionTask then + asset.missionTask=Mission.missionTask + end + + end + + -- Add request to airwing warehouse. + -- TODO: better Assignment string. + self:AddRequest(self, WAREHOUSE.Descriptor.ASSETLIST, Assetlist, #Assetlist, nil, nil, Mission.prio, tostring(Mission.auftragsnummer)) + + -- The queueid has been increased in the onafterAddRequest function. So we can simply use it here. + Mission.requestID=self.queueid + end + +end + +--- On after "MissionCancel" event. Cancels the missions of all flightgroups. Deletes request from warehouse queue. +-- @param #AIRWING 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 AIRWING:onafterMissionCancel(From, Event, To, Mission) + + self:I(self.lid..string.format("Cancel mission %s", Mission.name)) + + if Mission:IsPlanned() or Mission:IsQueued() or Mission:IsRequested() then + + Mission:Done() + + else + + for _,_asset in pairs(Mission.assets) do + local asset=_asset --#AIRWING.SquadronAsset + + local flightgroup=asset.flightgroup + + if flightgroup then + flightgroup:MissionCancel(Mission) + end + + -- Not requested any more (if it was). + asset.requested=nil + end + + end + + -- Remove queued request (if any). + if Mission.requestID then + self:_DeleteQueueItemByID(Mission.requestID, self.queue) + end + +end + +--- On after "NewAsset" event. Asset is added to the given squadron (asset assignment). +-- @param #AIRWING self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The asset that has just been added. +-- @param #string assignment The (optional) assignment for the asset. +function AIRWING:onafterNewAsset(From, Event, To, asset, assignment) + + -- Call parent warehouse function first. + self:GetParent(self).onafterNewAsset(self, From, Event, To, asset, assignment) + + -- Debug text. + local text=string.format("New asset %s with assignment %s and request assignment %s", asset.spawngroupname, tostring(asset.assignment), tostring(assignment)) + self:T3(self.lid..text) + + -- Get squadron. + local squad=self:GetSquadron(asset.assignment) + + -- Check if asset is already part of the squadron. If an asset returns, it will be added again! We check that asset.assignment is also assignment. + if squad then + + if asset.assignment==assignment then + + -- Debug text. + local text=string.format("Adding asset to squadron %s: assignment=%s, type=%s, attribute=%s", squad.name, assignment, asset.unittype, asset.attribute) + self:I(self.lid..text) + + -- Create callsign and modex. + squad:GetCallsign(asset) + squad:GetModex(asset) + + -- Add asset to squadron. + squad:AddAsset(asset) + + --asset.terminalType=AIRBASE.TerminalType.OpenBig + else + + self:I(self.lid..string.format("Asset %s from squadron %s returned! asset.assignment=\"%s\", assignment=\"%s\"", asset.spawngroupname, squad.name, tostring(asset.assignment), tostring(assignment))) + self:ReturnPayloadFromAsset(asset) + + end + + end +end + + + + +--- On after "AssetSpawned" event triggered when an asset group is spawned into the cruel world. +-- Creates a new flightgroup element and adds the mission to the flightgroup queue. +-- @param #AIRWING self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Group#GROUP group The group spawned. +-- @param #AIRWING.SquadronAsset asset The asset that was spawned. +-- @param Functional.Warehouse#WAREHOUSE.Pendingitem request The request of the dead asset. +function AIRWING:onafterAssetSpawned(From, Event, To, group, asset, request) + + -- Call parent warehouse function first. + self:GetParent(self).onafterAssetSpawned(self, From, Event, To, group, asset, request) + + -- Create a flight group. + local flightgroup=self:_CreateFlightGroup(asset) + + -- Set RTB on fuel critical. + flightgroup:SetFuelCriticalThreshold(nil, true) + + -- Set airwing. + flightgroup:SetAirwing(self) + + -- Set asset flightgroup. + asset.flightgroup=flightgroup + + -- Get the SQUADRON of the asset. + local squadron=self:GetSquadronOfAsset(asset) + + -- Set default TACAN channel. + local Tacan=squadron:GetTACAN() + if Tacan then + flightgroup:SetDefaultTACAN(Tacan) + end + + -- Set radio frequency and modulation + local radioFreq, radioModu=squadron:GetRadio() + if radioFreq then + flightgroup:SetDefaultRadio(radioFreq, radioModu) + end + + -- Not requested any more. + asset.requested=nil + + -- Get Mission (if any). + local mission=self:GetMissionByID(request.assignment) + + -- Add mission to flightgroup queue. + if mission then + + -- RTB on low fuel if on PATROL. + if mission.type==AUFTRAG.Type.PATROL then + flightgroup:SetFuelLowThreshold(nil, true) + end + + -- Add mission to flightgroup queue. + asset.flightgroup:AddMission(mission) + end + + -- Add group to the detection set of the WINGCOMMANDER. + if self.wingcommander then + self.wingcommander.detectionset:AddGroup(asset.flightgroup.group) + end + +end + +--- On after "AssetDead" event triggered when an asset group died. +-- @param #AIRWING self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #AIRWING.SquadronAsset asset The asset that is dead. +-- @param Functional.Warehouse#WAREHOUSE.Pendingitem request The request of the dead asset. +function AIRWING:onafterAssetDead(From, Event, To, asset, request) + + -- Call parent warehouse function first. + self:GetParent(self).onafterAssetDead(self, From, Event, To, asset, request) + + -- Add group to the detection set of the WINGCOMMANDER. + if self.wingcommander then + self.wingcommander.detectionset:RemoveGroupsByName({asset.spawngroupname}) + end + + -- Remove asset from mission is done via Mission:AssetDead() call from flightgroup onafterFlightDead function + -- Remove asset from squadron same +end + +--- On after "Destroyed" event. Remove assets from squadrons. Stop squadrons. Remove airwing from wingcommander. +-- @param #AIRWING self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AIRWING:onafterDestroyed(From, Event, To) + + self:I(self.lid.."Airwing warehouse destroyed!") + + -- Cancel all missions. + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + mission:Cancel() + end + + -- Remove all squadron assets. + for _,_squadron in pairs(self.squadrons) do + local squadron=_squadron --Ops.Squadron#SQUADRON + -- Stop Squadron. This also removes all assets. + squadron:Stop() + end + + -- Call parent warehouse function first. + self:GetParent(self).onafterDestroyed(self, From, Event, To) + +end + + +--- On after "Request" event. +-- @param #AIRWING self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Functional.Warehouse#WAREHOUSE.Queueitem Request Information table of the request. +function AIRWING:onafterRequest(From, Event, To, Request) + + -- Assets + local assets=Request.cargoassets + + -- Get Mission + local Mission=self:GetMissionByID(Request.assignment) + + if Mission and assets then + + for _,_asset in pairs(assets) do + local asset=_asset --#AIRWING.SquadronAsset + -- This would be the place to modify the asset table before the asset is spawned. + end + + end + + -- Call parent warehouse function after assets have been adjusted. + self:GetParent(self).onafterRequest(self, From, Event, To, Request) + +end + +--- On after "SelfRequest" event. +-- @param #AIRWING self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Set#SET_GROUP groupset The set of asset groups that was delivered to the warehouse itself. +-- @param Functional.Warehouse#WAREHOUSE.Pendingitem request Pending self request. +function AIRWING:onafterSelfRequest(From, Event, To, groupset, request) + + -- Call parent warehouse function first. + self:GetParent(self).onafterSelfRequest(self, From, Event, To, groupset, request) + + -- Get Mission + local mission=self:GetMissionByID(request.assignment) + + for _,_asset in pairs(request.assets) do + local asset=_asset --#AIRWING.SquadronAsset + end + + for _,_group in pairs(groupset:GetSet()) do + local group=_group --Wrapper.Group#GROUP + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new flight group after an asset was spawned. +-- @param #AIRWING self +-- @param #AIRWING.SquadronAsset asset The asset. +-- @return Ops.FlightGroup#FLIGHTGROUP The created flightgroup object. +function AIRWING:_CreateFlightGroup(asset) + + -- Create flightgroup. + local flightgroup=FLIGHTGROUP:New(asset.spawngroupname) + + -- Set airwing. + flightgroup:SetAirwing(self) + + --- Check if out of missiles. For A2A missions ==> RTB. + function flightgroup:OnAfterOutOfMissiles() + local airwing=flightgroup:GetAirWing() + + end + + --- Check if out of missiles. For A2G missions ==> RTB. But need to check A2G missiles, rockets as well. + function flightgroup:OnAfterOutOfBombs() + local airwing=flightgroup:GetAirWing() + + end + + --- Mission started. + function flightgroup:OnAfterMissionStart(From, Event, To, Mission) + local airwing=flightgroup:GetAirWing() + + end + + --- Flight is DEAD. + function flightgroup:OnAfterFlightDead(From, Event, To) + local airwing=flightgroup:GetAirWing() + + end + + return flightgroup +end + + +--- Check if an asset is currently on a mission (STARTED or EXECUTING). +-- @param #AIRWING self +-- @param #AIRWING.SquadronAsset asset The asset. +-- @param #table MissionTypes Types on mission to be checked. Default all. +-- @return #boolean If true, asset has at least one mission of that type in the queue. +function AIRWING:IsAssetOnMission(asset, MissionTypes) + + if MissionTypes then + if type(MissionTypes)~="table" then + MissionTypes={MissionTypes} + end + else + -- Check all possible types. + MissionTypes=AUFTRAG.Type + end + + if asset.flightgroup and asset.flightgroup:IsAlive() then + + -- Loop over mission queue. + for _,_mission in pairs(asset.flightgroup.missionqueue or {}) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission:IsNotOver() then + + -- Get flight status. + local status=mission:GetGroupStatus(asset.flightgroup) + + -- Only if mission is started or executing. + if (status==AUFTRAG.GroupStatus.STARTED or status==AUFTRAG.GroupStatus.EXECUTING) and self:CheckMissionType(mission.type, MissionTypes) then + return true + end + + end + + end + + end + + -- Alternative: run over all missions and compare to mission assets. + --[[ + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission:IsNotOver() then + for _,_asset in pairs(mission.assets) do + local sqasset=_asset --#AIRWING.SquadronAsset + + if sqasset.uid==asset.uid then + return true + end + + end + end + + end + ]] + + return false +end + +--- Get the current mission of the asset. +-- @param #AIRWING self +-- @param #AIRWING.SquadronAsset asset The asset. +-- @return Ops.Auftrag#AUFTRAG Current mission or *nil*. +function AIRWING:GetAssetCurrentMission(asset) + + if asset.flightgroup then + return asset.flightgroup:GetMissionCurrent() + end + + return nil +end + +--- Count payloads in stock. +-- @param #AIRWING self +-- @param #table MissionTypes Types on mission to be checked. Default *all* possible types `AUFTRAG.Type`. +-- @param #table UnitTypes Types of units. +-- @return #number Count of available payloads in stock. +function AIRWING:CountPayloadsInStock(MissionTypes, UnitTypes) + + if UnitTypes then + if type(UnitTypes)=="string" then + UnitTypes={UnitTypes} + end + end + + local function _checkUnitTypes(payload) + if UnitTypes then + for _,unittype in pairs(UnitTypes) do + if unittype==payload.aircrafttype then + return true + end + end + else + -- Unit type was not specified. + return true + end + return false + end + + local n=0 + for _,_payload in pairs(self.payloads) do + local payload=_payload --#AIRWING.Payload + + if self:CheckMissionCapability(MissionTypes, payload.capabilities) and _checkUnitTypes(payload) then + + if payload.unlimited then + -- Payload is unlimited. Return a BIG number. + return 999 + else + n=n+payload.navail + end + + end + + end + + return n +end + +--- Count missions in mission queue. +-- @param #AIRWING self +-- @param #table MissionTypes Types on mission to be checked. Default *all* possible types `AUFTRAG.Type`. +-- @return #number Number of missions that are not over yet. +function AIRWING:CountMissionsInQueue(MissionTypes) + + MissionTypes=MissionTypes or AUFTRAG.Type + + local N=0 + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + -- Check if this mission type is requested. + if mission:IsNotOver() and self:CheckMissionType(mission.type, MissionTypes) then + N=N+1 + end + + end + + return N +end + +--- Count assets on mission. +-- @param #AIRWING self +-- @param #table MissionTypes Types on mission to be checked. Default all. +-- @return #number Number of pending and queued assets. +-- @return #number Number of pending assets. +-- @return #number Number of queued assets. +function AIRWING:CountAssetsOnMission(MissionTypes) + + local Nq=0 + local Np=0 + + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + -- Check if this mission type is requested. + if self:CheckMissionType(mission.type, MissionTypes) then + + for _,_asset in pairs(mission.assets or {}) do + local asset=_asset --#AIRWING.SquadronAsset + + local request, isqueued=self:GetRequestByID(mission.requestID) + + if isqueued then + Nq=Nq+1 + else + Np=Np+1 + end + + end + end + end + + return Np+Nq, Np, Nq +end + +--- Count assets on mission. +-- @param #AIRWING self +-- @param #table MissionTypes Types on mission to be checked. Default all. +-- @return #table Assets on pending requests. +function AIRWING:GetAssetsOnMission(MissionTypes, IncludeQueued) + + local assets={} + local Np=0 + + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + -- Check if this mission type is requested. + if self:CheckMissionType(mission.type, MissionTypes) then + + for _,_asset in pairs(mission.assets or {}) do + local asset=_asset --#AIRWING.SquadronAsset + + table.insert(assets, asset) + + end + end + end + + return assets +end + +--- Check +-- @param #AIRWING self +-- @param #boolean onlyactive Count only the active ones. +-- @return #table Table of unit types. +function AIRWING:_CheckSquads(onlyactive) + +end + +--- Get the aircraft types of this airwing. +-- @param #AIRWING self +-- @param #boolean onlyactive Count only the active ones. +-- @param #table squadrons Table of squadrons. Default all. +-- @return #table Table of unit types. +function AIRWING:GetAircraftTypes(onlyactive, squadrons) + + -- Get all unit types that can do the job. + local unittypes={} + + -- Loop over all squadrons. + for _,_squadron in pairs(squadrons or self.squadrons) do + local squadron=_squadron --Ops.Squadron#SQUADRON + + if (not onlyactive) or squadron:IsOnDuty() then + + local gotit=false + for _,unittype in pairs(unittypes) do + if squadron.aircrafttype==unittype then + gotit=true + break + end + end + if not gotit then + table.insert(unittypes, squadron.aircrafttype) + end + + end + end + + return unittypes +end + +--- Check if assets for a given mission type are available. +-- @param #AIRWING self +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @return #boolean If true, enough assets are available. +-- @return #table Assets that can do the required mission. +function AIRWING:CanMission(Mission) + + -- Assume we CAN and NO assets are available. + local Can=true + local Assets={} + + -- Squadrons for the job. If user assigned to mission or simply all. + local squadrons=Mission.squadrons or self.squadrons + + -- Get aircraft unit types for the job. + local unittypes=self:GetAircraftTypes(true, squadrons) + + -- Count all payloads in stock. + local Npayloads=self:CountPayloadsInStock(Mission.type, unittypes) + + if Npayloads #Assets then + self:I(self.lid..string.format("INFO: Not enough assets available! Got %d but need at least %d", #Assets, Mission.nassets)) + Can=false + end + + return Can, Assets +end + +--- Check if assets for a given mission type are available. +-- @param #AIRWING self +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @return #table Assets that can do the required mission. +function AIRWING:RecruitAssets(Mission) + +end + + +--- Check if a mission type is contained in a list of possible types. +-- @param #AIRWING self +-- @param #string MissionType The requested mission type. +-- @param #table PossibleTypes A table with possible mission types. +-- @return #boolean If true, the requested mission type is part of the possible mission types. +function AIRWING:CheckMissionType(MissionType, PossibleTypes) + + if type(PossibleTypes)=="string" then + PossibleTypes={PossibleTypes} + end + + for _,canmission in pairs(PossibleTypes) do + if canmission==MissionType then + return true + end + end + + return false +end + +--- Check if a mission type is contained in a list of possible capabilities. +-- @param #AIRWING self +-- @param #string MissionType The requested mission type. +-- @param #table Capabilities A table with possible capabilities. +-- @return #boolean If true, the requested mission type is part of the possible mission types. +function AIRWING:CheckMissionCapability(MissionType, Capabilities) + + for _,cap in pairs(Capabilities) do + local capability=cap --Ops.Auftrag#AUFTRAG.Capability + if capability.MissionType==MissionType then + return true + end + end + + return false +end + +--- Get payload performance for a given type of misson type. +-- @param #AIRWING self +-- @param #AIRWING.Payload Payload The payload table. +-- @param #string MissionType Type of mission. +-- @return #number Performance or -1. +function AIRWING:GetPayloadPeformance(Payload, MissionType) + + if Payload then + + for _,Capability in pairs(Payload.capabilities) do + local capability=Capability --Ops.Auftrag#AUFTRAG.Capability + if capability.MissionType==MissionType then + return capability.Performance + end + end + + else + self:E(self.lid.."ERROR: Payload is nil!") + end + + return -1 +end + +--- Get mission types a payload can perform. +-- @param #AIRWING self +-- @param #AIRWING.Payload Payload The payload table. +-- @return #table Mission types. +function AIRWING:GetPayloadMissionTypes(Payload) + + local missiontypes={} + + for _,Capability in pairs(Payload.capabilities) do + local capability=Capability --Ops.Auftrag#AUFTRAG.Capability + table.insert(missiontypes, capability.MissionType) + end + + return missiontypes +end + +--- Returns the mission for a given mission ID (Autragsnummer). +-- @param #AIRWING self +-- @param #number mid Mission ID (Auftragsnummer). +-- @return Ops.Auftrag#AUFTRAG Mission table. +function AIRWING:GetMissionByID(mid) + + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission.auftragsnummer==tonumber(mid) then + return mission + end + + end + + return nil +end + +--- Returns the mission for a given request ID. +-- @param #AIRWING self +-- @param #number RequestID Unique ID of the request. +-- @return Ops.Auftrag#AUFTRAG Mission table or *nil*. +function AIRWING:GetMissionFromRequestID(RequestID) + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + if mission.requestID and mission.requestID==RequestID then + return mission + end + end + return nil +end + +--- Returns the mission for a given request. +-- @param #AIRWING self +-- @param Functional.Warehouse#WAREHOUSE.Queueitem Request The warehouse request. +-- @return Ops.Auftrag#AUFTRAG Mission table or *nil*. +function AIRWING:GetMissionFromRequest(Request) + return self:GetMissionFromRequestID(Request.uid) +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Menu Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Patrol carrier. +-- @param #AIRWING self +-- @return #AIRWING self +function AIRWING:_SetMenuCoalition() + + -- Get coalition. + local Coalition=self:GetCoalition() + + -- Init menu table. + self.menu=self.menu or {} + + -- Init menu coalition table. + self.menu[Coalition]=self.menu[Coalition] or {} + + -- Shortcut. + local menu=self.menu[Coalition] + + if self.menusingle then + -- F10/Skipper/... + if not menu.AIRWING then + menu.AIRWING=MENU_COALITION:New(Coalition, "AIRWING") + end + else + -- F10/Skipper//... + if not menu.Root then + menu.Root=MENU_COALITION:New(Coalition, "AIRWING") + end + menu.AIRWING=MENU_COALITION:New(Coalition, self.alias, menu.Root) + end + + ------------------- + -- Squadron Menu -- + ------------------- + + menu.Squadron={} + menu.Squadron.Main= MENU_COALITION:New(Coalition, "Squadrons", menu.AIRWING) + + menu.Warehouse={} + menu.Warehouse.Main = MENU_COALITION:New(Coalition, "Warehouse", menu.AIRWING) + menu.Warehouse.Reports = MENU_COALITION_COMMAND:New(Coalition, "Reports On/Off", menu.Warehouse.Main, self.WarehouseReportsToggle, self) + menu.Warehouse.Assets = MENU_COALITION_COMMAND:New(Coalition, "Report Assets", menu.Warehouse.Main, self.ReportWarehouseStock, self) + + menu.ReportSquadrons = MENU_COALITION_COMMAND:New(Coalition, "Report Squadrons", menu.AIRWING, self.ReportSquadrons, self) + +end + +--- Report squadron status. +-- @param #AIRWING self +function AIRWING:ReportSquadrons() + + local text="Squadron Report:" + + for i,_squadron in pairs(self.squadrons) do + local squadron=_squadron + + local name=squadron.name + + local nspawned=0 + local nstock=0 + for _,_asset in pairs(squadron.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + local n=asset.nunits + + if asset.spawned then + nspawned=nspawned+n + else + nstock=nstock+n + end + + end + + text=string.format("\n%s: AC on duty=%d, in stock=%d", name, nspawned, nstock) + + end + + self:I(self.lid..text) + MESSAGE:New(text, 10, "AIRWING", true):ToCoalition(self:GetCoalition()) + +end + + +--- Add sub menu for this intruder. +-- @param #AIRWING self +-- @param Ops.Squadron#SQUADRON squadron The squadron data. +function AIRWING:_AddSquadonMenu(squadron) + + local Coalition=self:GetCoalition() + + local root=self.menu[Coalition].Squadron.Main + + local menu=MENU_COALITION:New(Coalition, squadron.name, root) + + MENU_COALITION_COMMAND:New(Coalition, "Report", menu, self._ReportSq, self, squadron) + MENU_COALITION_COMMAND:New(Coalition, "Launch CAP", menu, self._LaunchCAP, self, squadron) + + -- Set menu. + squadron.menu=menu + +end + + +--- Report squadron status. +-- @param #AIRWING self +-- @param Ops.Squadron#SQUADRON squadron The squadron object. +function AIRWING:_ReportSq(squadron) + + local text=string.format("%s: %s assets:", squadron.name, tostring(squadron.categoryname)) + for i,_asset in pairs(squadron.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + text=text..string.format("%d.) ") + end +end + +--- Warehouse reports on/off. +-- @param #AIRWING self +function AIRWING:WarehouseReportsToggle() + self.Report=not self.Report + MESSAGE:New(string.format("Warehouse reports are now %s", tostring(self.Report)), 10, "AIRWING", true):ToCoalition(self:GetCoalition()) +end + + +--- Report warehouse stock. +-- @param #AIRWING self +function AIRWING:ReportWarehouseStock() + local text=self:_GetStockAssetsText(false) + MESSAGE:New(text, 10, "AIRWING", true):ToCoalition(self:GetCoalition()) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Auftrag.lua b/Moose Development/Moose/Ops/Auftrag.lua new file mode 100644 index 000000000..34b6cad08 --- /dev/null +++ b/Moose Development/Moose/Ops/Auftrag.lua @@ -0,0 +1,3273 @@ +--- **Ops** - Auftrag (mission) for Ops. +-- +-- **Main Features:** +-- +-- * Simplifies defining and executing DCS tasks. +-- * Additional useful events. +-- * Set mission start/stop times. +-- * Set mission priority and urgency (can cancel running missions). +-- * Specific mission options for ROE, ROT, formation, etc. +-- * Interface to FLIGHTGROUP, AIRWING and WINGCOMMANDER classes. +-- * FSM events when a mission is done, successful or failed. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Ops.Auftrag +-- @image OPS_Auftrag.png + + +--- AUFTRAG class. +-- @type AUFTRAG +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. Messages to all about status. +-- @field #string lid Class id string for output to DCS log file. +-- @field #number auftragsnummer Auftragsnummer. +-- @field #string type Mission type. +-- @field #string status Mission status. +-- @field #table groupdata Group specific data. +-- @field #string name Mission name. +-- @field #number prio Mission priority. +-- @field #boolean urgent Mission is urgent. Running missions with lower prio might be cancelled. +-- @field #number Tstart Mission start time in seconds. +-- @field #number Tstop Mission stop time in seconds. +-- @field #number duration Mission duration in seconds. +-- @field Wrapper.Marker#MARKER marker F10 map marker. +-- @field #table DCStask DCS task structure. +-- @field #number Ntargets Number of mission targets. +-- @field #number dTevaluate Time interval in seconds before the mission result is evaluated after mission is over. +-- @field #number Tover Mission abs. time stamp, when mission was over. +-- @field #table conditionStart Condition(s) that have to be true, before the mission will be started. +-- @field #table conditionSuccess If all stop conditions are true, the mission is cancelled. +-- @field #table conditionFailure If all stop conditions are true, the mission is cancelled. +-- +-- @field #number orbitSpeed Orbit speed in m/s. +-- @field #number orbitAltitude Orbit altitude in meters. +-- @field #number orbitHeading Orbit heading in degrees. +-- @field #number orbitLeg Length of orbit leg in meters. +-- @field Core.Point#COORDINATE orbitRaceTrack Race-track orbit coordinate. +-- +-- @field #AUFTRAG.TargetData engageTarget Target data to engage. +-- +-- @field Core.Zone#ZONE_RADIUS engageZone *Circular* engagement zone. +-- @field #table engageTargetTypes Table of target types that are engaged in the engagement zone. +-- @field #number engageAltitude Engagement altitude in meters. +-- @field #number engageDirection Engagement direction in degrees. +-- @field #number engageQuantity Number of times a target is engaged. +-- @field #number engageWeaponType Weapon type used. +-- @field #number engageWeaponExpend How many weapons are used. +-- @field #boolean engageAsGroup Group attack. +-- @field #number engageMaxDistance Max engage distance. +-- @field #number refuelSystem Refuel type (boom or probe) for TANKER missions. +-- +-- @field Wrapper.Group#GROUP escortGroup The group to be escorted. +-- @field DCS#Vec3 escortVec3 The 3D offset vector from the escorted group to the escort group. +-- +-- @field #number facDesignation FAC designation type. +-- @field #boolean facDatalink FAC datalink enabled. +-- @field #number facFreq FAC radio frequency in MHz. +-- @field #number facModu FAC radio modulation 0=AM 1=FM. +-- +-- @field Core.Set#SET_GROUP transportGroupSet Groups to be transported. +-- @field Core.Point#COORDINATE transportPickup Coordinate where to pickup the cargo. +-- @field Core.Point#COORDINATE transportDropoff Coordinate where to drop off the cargo. +-- +-- @field #number artyRadius Radius in meters. +-- @field #number artyShots Number of shots fired. +-- +-- @field Ops.WingCommander#WINGCOMMANDER wingcommander The WINGCOMMANDER managing this mission. +-- @field Ops.AirWing#AIRWING airwing The assigned airwing. +-- @field #table assets Airwing Assets assigned for this mission. +-- @field #number nassets Number of required assets by the Airwing. +-- @field #number requestID The ID of the queued warehouse request. Necessary to cancel the request if the mission was cancelled before the request is processed. +-- @field #boolean cancelContactLost If true, cancel mission if the contact is lost. +-- @field #table squadrons User specifed airwing squadrons assigned for this mission. Only these will be considered for the job! +-- @field Ops.AirWing#AIRWING.PatrolData patroldata Patrol data. +-- +-- @field #string missionTask Mission task. See `ENUMS.MissionTask`. +-- @field #number missionAltitude Mission altitude in meters. +-- @field #number missionFraction Mission coordiante fraction. Default is 0.5. +-- @field #number missionRange Mission range in meters. Used in AIRWING class. +-- +-- @field #table enrouteTasks Mission enroute tasks. +-- +-- @field #number radioFreq Mission radio frequency in MHz. +-- @field #number radioModu Mission radio modulation (0=AM and 1=FM). +-- @field #number tacanChannel Mission TACAN channel. +-- @field #number tacanMorse Mission TACAN morse code. +-- +-- @field #number missionRepeated Number of times mission was repeated. +-- @field #number missionRepeatMax Number of times mission is repeated if failed. +-- +-- @field #number optionROE ROE. +-- @field #number optionROT ROT. +-- @field #number optionCM Counter measures. +-- @field #number optionFormation Formation. +-- @field #number optionRTBammo RTB on out-of-ammo. +-- @field #number optionRTBfuel RTB on out-of-fuel. +-- @field #number optionECM ECM. +-- +-- @extends Core.Fsm#FSM + +--- *A warrior's mission is to foster the success of others.* --- Morihei Ueshiba +-- +-- === +-- +-- ![Banner Image](..\Presentations\CarrierAirWing\AUFTRAG_Main.jpg) +-- +-- # The AUFTRAG Concept +-- +-- As you probably know, setting tasks in DCS is often tedious. The AUFTRAG class significantly simplifies the necessary workflow by using optimized default parameters. +-- +-- You can think of an AUFTRAG as document, which contains the mission briefing, i.e. information about the target location, mission altitude, speed and various other parameters. +-- This document can be handed over directly to a pilot (or multiple pilots) via the FLIGHTGROUP class. The pilots will then execute the mission. +-- The AUFTRAG document can also be given to an AIRWING. The airwing will then determine the best assets (pilots and payloads) available for the job. +-- One more up the food chain, an AUFTRAG can be passed to a WINGCOMMANDER. The wing commander will find the best AIRWING and pass the job over to it. +-- +-- # Airborne Missions +-- +-- Several mission types are supported by this class. +-- +-- ## Anti-Ship +-- +-- An anti-ship mission can be created with the @{#AUFTRAG.NewANTISHIP}(*Target, Altitude*) function. +-- +-- ## AWACS +-- +-- An AWACS mission can be created with the @{#AUFTRAG.NewAWACS}() function. +-- +-- ## BAI +-- +-- A BAI mission can be created with the @{#AUFTRAG.NewBAI}() function. +-- +-- ## Bombing +-- +-- A bombing mission can be created with the @{#AUFTRAG.NewBOMBING}() function. +-- +-- ## Bombing Runway +-- +-- A bombing runway mission can be created with the @{#AUFTRAG.NewBOMBRUNWAY}() function. +-- +-- ## Bombing Carpet +-- +-- A carpet bombing mission can be created with the @{#AUFTRAG.NewBOMBCARPET}() function. +-- +-- ## CAP +-- +-- A CAP mission can be created with the @{#AUFTRAG.NewCAP}() function. +-- +-- ## CAS +-- +-- A CAS mission can be created with the @{#AUFTRAG.NewCAS}() function. +-- +-- ## Escort +-- +-- An escort mission can be created with the @{#AUFTRAG.NewESCORT}() function. +-- +-- ## FACA +-- +-- An FACA mission can be created with the @{#AUFTRAG.NewFACA}() function. +-- +-- ## Ferry +-- +-- Not implemented yet. +-- +-- ## Intercept +-- +-- An intercept mission can be created with the @{#AUFTRAG.NewINTERCEPT}() function. +-- +-- ## Orbit +-- +-- An orbit mission can be created with the @{#AUFTRAG.NewORBIT}() function. +-- +-- ## PATROL +-- +-- An patrol mission can be created with the @{#AUFTRAG.NewPATROL}() function. +-- +-- ## RECON +-- +-- Not implemented yet. +-- +-- ## RESCUE HELO +-- +-- An rescue helo mission can be created with the @{#AUFTRAG.NewRESCUEHELO}() function. +-- +-- ## SEAD +-- +-- An SEAD mission can be created with the @{#AUFTRAG.NewSEAD}() function. +-- +-- ## STRIKE +-- +-- An strike mission can be created with the @{#AUFTRAG.NewSTRIKE}() function. +-- +-- ## Tanker +-- +-- A refueling tanker mission can be created with the @{#AUFTRAG.NewTANKER}() function. +-- +-- ## TROOPTRANSPORT +-- +-- A troop transport mission can be created with the @{#AUFTRAG.NewTROOPTRANSPORT}() function. +-- +-- # Ground Missions +-- +-- ## ARTY +-- +-- An arty mission can be created with the @{#AUFTRAG.NewARTY}() function. +-- +-- # Options and Parameters +-- +-- +-- # Assigning Missions +-- +-- An AUFTRAG can be assigned to groups, airwings or wingcommanders +-- +-- ## Group Level +-- +-- ### Flight Group +-- +-- Assigning an AUFTRAG to a flight groups is done via the @{Ops.FlightGroup#FLIGHTGROUP.AddMission} function. See FLIGHTGROUP docs for details. +-- +-- ### Navy Group +-- +-- Assigning an AUFTRAG to a navy groups is done via the @{Ops.NavyGroup#NAVYGROUP.AddMission} function. See NAVYGROUP docs for details. +-- +-- ## Airwing Level +-- +-- Adding an AUFTRAG to an airwing is done via the @{Ops.AirWing#AIRWING.AddMission} function. See AIRWING docs for further details. +-- +-- ## Wing Commander Level +-- +-- Assigning an AUFTRAG to a wing commander is done via the @{Ops.WingCommander#WINGCOMMANDER.AddMission} function. See WINGCOMMADER docs for details. +-- +-- +-- # Events +-- +-- The AUFTRAG class creates many useful (FSM) events, which can be used in the mission designers script. +-- +-- +-- # Examples +-- +-- +-- +-- +-- +-- @field #AUFTRAG +AUFTRAG = { + ClassName = "AUFTRAG", + Debug = false, + lid = nil, + auftragsnummer = nil, + groupdata = {}, + assets = {}, + missionFraction = 0.5, + enrouteTasks = {}, + marker = nil, + conditionStart = {}, + conditionSuccess = {}, + conditionFailure = {}, +} + +--- Global mission counter. +_AUFTRAGSNR=0 + + +--- Mission types. +-- @type AUFTRAG.Type +-- @field #string ANTISHIP Anti-ship mission. +-- @field #string AWACS AWACS mission. +-- @field #string BAI Battlefield Air Interdiction. +-- @field #string BOMBING Bombing mission. +-- @field #string BOMBRUNWAY Bomb runway of an airbase. +-- @field #string BOMBCARPET Carpet bombing. +-- @field #string CAP Combat Air Patrol. +-- @field #string CAS Close Air Support. +-- @field #string ESCORT Escort mission. +-- @field #string FACA Forward AirController airborne mission. +-- @field #string FERRY Ferry flight mission. +-- @field #string INTERCEPT Intercept mission. +-- @field #string ORBIT Orbit mission. +-- @field #string PATROL Similar to CAP but no auto engage targets. +-- @field #string RECON Recon mission. +-- @field #string RECOVERYTANKER Recovery tanker mission. Not implemented yet. +-- @field #string RESCUEHELO Rescue helo. +-- @field #string SEAD Suppression/destruction of enemy air defences. +-- @field #string STRIKE Strike mission. +-- @field #string TANKER Tanker mission. +-- @field #string TROOPTRANSPORT Troop transport mission. +-- @field #string ARTY Fire at point. +AUFTRAG.Type={ + ANTISHIP="Anti Ship", + AWACS="AWACS", + BAI="BAI", + BOMBING="Bombing", + BOMBRUNWAY="Bomb Runway", + BOMBCARPET="Carpet Bombing", + CAP="CAP", + CAS="CAS", + ESCORT="Escort", + FACA="FAC-A", + FERRY="Ferry Flight", + INTERCEPT="Intercept", + ORBIT="Orbit", + PATROL="Patrol", + RECON="Recon", + RECOVERYTANKER="Recovery Tanker", + RESCUEHELO="Rescue Helo", + SEAD="SEAD", + STRIKE="Strike", + TANKER="Tanker", + TROOPTRANSPORT="Troop Transport", + ARTY="Fire At Point", +} + +--- Mission status. +-- @type AUFTRAG.Status +-- @field #string PLANNED Mission is at the early planning stage. +-- @field #string QUEUED Mission is queued at an airwing. +-- @field #string REQUESTED Mission assets were requested from the warehouse. +-- @field #string SCHEDULED Mission is scheduled in a FLIGHGROUP queue waiting to be started. +-- @field #string STARTED Mission has started but is not executed yet. +-- @field #string EXECUTING Mission is being executed. +-- @field #string DONE Mission is over. +-- @field #string CANCELLED Mission was cancelled. +-- @field #string SUCCESS Mission was a success. +-- @field #string FAILED Mission failed. +AUFTRAG.Status={ + PLANNED="planned", + QUEUED="queued", + REQUESTED="requested", + SCHEDULED="scheduled", + STARTED="started", + EXECUTING="executing", + DONE="done", + CANCELLED="cancelled", + SUCCESS="success", + FAILED="failed", +} + +--- Mission status of an assigned group. +-- @type AUFTRAG.GroupStatus +-- @field #string SCHEDULED Mission is scheduled in a FLIGHGROUP queue waiting to be started. +-- @field #string STARTED Ops group started this mission but it is not executed yet. +-- @field #string EXECUTING Ops group is executing this mission. +-- @field #string PAUSED Ops group has paused this mission, e.g. for refuelling. +-- @field #string DONE Mission task of the Ops group is done. +-- @field #string CANCELLED Mission was cancelled. +AUFTRAG.GroupStatus={ + SCHEDULED="scheduled", + STARTED="started", + EXECUTING="executing", + PAUSED="paused", + DONE="done", + CANCELLED="cancelled", +} + +--- Target type. +-- @type AUFTRAG.TargetType +-- @field #string GROUP Target is a GROUP object. +-- @field #string UNIT Target is a UNIT object. +-- @field #string STATIC Target is a STATIC object. +-- @field #string COORDINATE Target is a COORDINATE. +-- @field #string AIRBASE Target is an AIRBASE. +-- @field #string SETGROUP Target is a SET of GROUPs. +-- @field #string SETUNIT Target is a SET of UNITs. +AUFTRAG.TargetType={ + GROUP="Group", + UNIT="Unit", + STATIC="Static", + COORDINATE="Coordinate", + AIRBASE="Airbase", + SETGROUP="SetGroup", + SETUNIT="SetUnit", +} + +--- Target data. +-- @type AUFTRAG.TargetData +-- @field Wrapper.Positionable#POSITIONABLE Target Target Object. +-- @field #string Type Target type: "Group", "Unit", "Static", "Coordinate", "Airbase", "SetGroup", "SetUnit". +-- @field #string Name Target name. +-- @field #number Ninital Number of initial targets. +-- @field #number Lifepoints Total life points. +-- @field #number Lifepoints0 Inital life points. + +--- Mission capability. +-- @type AUFTRAG.Capability +-- @field #string MissionType Type of mission. +-- @field #number Performance Number describing the performance level. The higher the better. + +--- Mission success. +-- @type AUFTRAG.Success +-- @field #string SURVIVED Group did survive. +-- @field #string ENGAGED Target was engaged. +-- @field #string DAMAGED Target was damaged. +-- @field #string DESTROYED Target was destroyed. + +--- Generic mission condition. +-- @type AUFTRAG.Condition +-- @field #function func Callback function to check for a condition. Should return a #boolean. +-- @field #table arg Optional arguments passed to the condition callback function. + +--- Group specific data. Each ops group subscribed to this mission has different data for this. +-- @type AUFTRAG.GroupData +-- @field Ops.OpsGroup#OPSGROUP opsgroup The OPS group. +-- @field Core.Point#COORDINATE waypointcoordinate Waypoint coordinate. +-- @field #number waypointindex Waypoint index. +-- @field Ops.OpsGroup#OPSGROUP.Task waypointtask Waypoint task. +-- @field #string status Group mission status. +-- @field Ops.AirWing#AIRWING.SquadronAsset asset The squadron asset. + + +--- AUFTRAG class version. +-- @field #string version +AUFTRAG.version="0.3.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Clone mission. How? Deepcopy? +-- DONE: Option to assign mission to specific squadrons (requires an AIRWING). +-- TODO: Option to assign a specific payload for the mission (requires an AIRWING). +-- DONE: Add mission start conditions. +-- TODO: Add recovery tanker mission for boat ops. +-- DONE: Add rescue helo mission for boat ops. +-- TODO: Mission success options damaged, destroyed. +-- DONE: Mission ROE and ROT. +-- DONE: Mission frequency and TACAN. +-- TODO: Mission formation, etc. +-- DONE: FSM events. +-- DONE: F10 marker functions that are updated on Status event. +-- TODO: F10 marker to create new missions. +-- DONE: Evaluate mission result ==> SUCCESS/FAILURE +-- DONE: NewAUTO() NewA2G NewA2A +-- DONE: Transport mission. +-- TODO: Recon mission. What input? Set of coordinates? +-- TODO: Set mission coalition, e.g. for F10 markers. Could be derived from target if target has a coalition. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new generic AUFTRAG object. +-- @param #AUFTRAG self +-- @param #string Type Mission type. +-- @return #AUFTRAG self +function AUFTRAG:New(Type) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #AUFTRAG + + -- Increase global counter. + _AUFTRAGSNR=_AUFTRAGSNR+1 + + -- Mission type. + self.type=Type + + -- Auftragsnummer. + self.auftragsnummer=_AUFTRAGSNR + + -- Log id. + self:_SetLogID() + + -- State is planned. + self.status=AUFTRAG.Status.PLANNED + + -- Defaults + self:SetName() + self:SetPriority() + self:SetTime() + self.engageAsGroup=true + self.missionRepeated=0 + self.missionRepeatMax=0 + self.nassets=1 + self.dTevaluate=0 + + -- FMS start state is PLANNED. + self:SetStartState(self.status) + + -- PLANNED --> (QUEUED) --> (REQUESTED) --> SCHEDULED --> STARTED --> EXECUTING --> DONE + + self:AddTransition(AUFTRAG.Status.PLANNED, "Queued", AUFTRAG.Status.QUEUED) -- Mission is in queue of an AIRWING. + self:AddTransition(AUFTRAG.Status.QUEUED, "Requested", AUFTRAG.Status.REQUESTED) -- Mission assets have been requested from the warehouse. + self:AddTransition(AUFTRAG.Status.REQUESTED, "Scheduled", AUFTRAG.Status.SCHEDULED) -- Mission added to the first ops group queue. + + self:AddTransition(AUFTRAG.Status.PLANNED, "Scheduled", AUFTRAG.Status.SCHEDULED) -- From planned directly to scheduled. + + self:AddTransition(AUFTRAG.Status.SCHEDULED, "Started", AUFTRAG.Status.STARTED) -- First asset has started the mission + self:AddTransition(AUFTRAG.Status.STARTED, "Executing", AUFTRAG.Status.EXECUTING) -- First asset is executing the mission. + + self:AddTransition("*", "Done", AUFTRAG.Status.DONE) -- All assets have reported that mission is done. + + self:AddTransition("*", "Cancel", "*") -- Command to cancel the mission. + + self:AddTransition("*", "Success", AUFTRAG.Status.SUCCESS) + self:AddTransition("*", "Failed", AUFTRAG.Status.FAILED) + + self:AddTransition("*", "Status", "*") + self:AddTransition("*", "Stop", "*") + + self:AddTransition("*", "Repeat", AUFTRAG.Status.PLANNED) + + self:AddTransition("*", "GroupDead", "*") + self:AddTransition("*", "AssetDead", "*") + + + -- Init status update. + self:__Status(-1) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Create Missions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create an ANTI-SHIP mission. +-- @param #AUFTRAG self +-- @param Wrapper.Positionable#POSITIONABLE Target The target to attack. Can be passed as a @{Wrapper.Group#GROUP} or @{Wrapper.Unit#UNIT} object. +-- @param #number Altitude Engage altitude in feet. Default 2000 ft. +-- @return #AUFTRAG self +function AUFTRAG:NewANTISHIP(Target, Altitude) + + local mission=AUFTRAG:New(AUFTRAG.Type.ANTISHIP) + + mission:_TargetFromObject(Target) + + -- DCS task parameters: + mission.engageWeaponType=ENUMS.WeaponFlag.Auto + mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL + mission.engageAltitude=Altitude or UTILS.FeetToMeters(2000) + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.ANTISHIPSTRIKE + mission.missionAltitude=mission.engageAltitude + mission.missionFraction=0.4 + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionROT=ENUMS.ROT.EvadeFire + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- Create an ORBIT mission, which can be either a circular orbit or a race-track pattern. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Coordinate Where to orbit. +-- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`. +-- @param #number Speed Orbit speed in knots. Default 350 KIAS. +-- @param #number Heading Heading of race-track pattern in degrees. If not specified, a circular orbit is performed. +-- @param #number Leg Length of race-track in NM. If not specified, a circular orbit is performed. +-- @return #AUFTRAG self +function AUFTRAG:NewORBIT(Coordinate, Altitude, Speed, Heading, Leg) + + local mission=AUFTRAG:New(AUFTRAG.Type.ORBIT) + + -- Altitude. + if Altitude then + mission.orbitAltitude=UTILS.FeetToMeters(Altitude) + else + mission.orbitAltitude=Coordinate.y + end + Coordinate.y=mission.orbitAltitude + + mission:_TargetFromObject(Coordinate) + + mission.orbitSpeed = UTILS.KnotsToMps(Speed or 350) + + if Heading and Leg then + mission.orbitHeading=Heading + mission.orbitLeg=UTILS.NMToMeters(Leg) + mission.orbitRaceTrack=Coordinate:Translate(mission.orbitLeg, mission.orbitHeading, true) + end + + + -- Mission options: + mission.missionAltitude=mission.orbitAltitude*0.9 + mission.missionFraction=0.9 + mission.optionROE=ENUMS.ROE.ReturnFire + mission.optionROT=ENUMS.ROT.PassiveDefense + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- Create an ORBIT mission, where the aircraft will go in a circle around the specified coordinate. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Coordinate Position where to orbit around. +-- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`. +-- @param #number Speed Orbit speed in knots. Default 350 KIAS. +-- @return #AUFTRAG self +function AUFTRAG:NewORBIT_CIRCLE(Coordinate, Altitude, Speed) + + local mission=AUFTRAG:NewORBIT(Coordinate, Altitude, Speed) + + return mission +end + +--- Create an ORBIT mission, where the aircraft will fly a race-track pattern. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Coordinate Where to orbit. +-- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`. +-- @param #number Speed Orbit speed in knots. Default 350 KIAS. +-- @param #number Heading Heading of race-track pattern in degrees. Default random in [0, 360) degrees. +-- @param #number Leg Length of race-track in NM. Default 10 NM. +-- @return #AUFTRAG self +function AUFTRAG:NewORBIT_RACETRACK(Coordinate, Altitude, Speed, Heading, Leg) + + Heading = Heading or math.random(360) + Leg = Leg or 10 + + local mission=AUFTRAG:NewORBIT(Coordinate, Altitude, Speed, Heading, Leg) + + return mission +end + +--- Create a PATROL mission. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Coordinate Where to orbit. +-- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`. +-- @param #number Speed Orbit speed in knots. Default 350 kts. +-- @param #number Heading Heading of race-track pattern in degrees. Default random in [0, 360) degrees. +-- @param #number Leg Length of race-track in NM. Default 10 NM. +-- @return #AUFTRAG self +function AUFTRAG:NewPATROL(Coordinate, Altitude, Speed, Heading, Leg) + + -- Create ORBIT first. + local mission=AUFTRAG:NewORBIT_RACETRACK(Coordinate, Altitude, Speed, Heading, Leg) + + -- Mission type PATROL. + mission.type=AUFTRAG.Type.PATROL + + mission:_SetLogID() + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.INTERCEPT + mission.optionROT=ENUMS.ROT.PassiveDefense + + return mission +end + +--- Create a TANKER mission. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Coordinate Where to orbit. +-- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`. +-- @param #number Speed Orbit speed in knots. Default 350 kts. +-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West). +-- @param #number Leg Length of race-track in NM. Default 10 NM. +-- @param #number RefuelSystem Refueling system. +-- @return #AUFTRAG self +function AUFTRAG:NewTANKER(Coordinate, Altitude, Speed, Heading, Leg, RefuelSystem) + + -- Create ORBIT first. + local mission=AUFTRAG:NewORBIT_RACETRACK(Coordinate, Altitude, Speed, Heading, Leg) + + -- Mission type PATROL. + mission.type=AUFTRAG.Type.TANKER + + mission:_SetLogID() + + mission.refuelSystem=RefuelSystem + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.REFUELING + mission.optionROE=ENUMS.ROE.WeaponHold + mission.optionROT=ENUMS.ROT.PassiveDefense + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- Create a AWACS mission. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Coordinate Where to orbit. Altitude is also taken from the coordinate. +-- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`. +-- @param #number Speed Orbit speed in knots. Default 350 kts. +-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West). +-- @param #number Leg Length of race-track in NM. Default 10 NM. +-- @return #AUFTRAG self +function AUFTRAG:NewAWACS(Coordinate, Altitude, Speed, Heading, Leg) + + -- Create ORBIT first. + local mission=AUFTRAG:NewORBIT_RACETRACK(Coordinate, Altitude, Speed, Heading, Leg) + + -- Mission type PATROL. + mission.type=AUFTRAG.Type.AWACS + + mission:_SetLogID() + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.AWACS + mission.optionROE=ENUMS.ROE.WeaponHold + mission.optionROT=ENUMS.ROT.PassiveDefense + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + + + +--- Create an INTERCEPT mission. +-- @param #AUFTRAG self +-- @param Wrapper.Positionable#POSITIONABLE Target The target to intercept. Can also be passed as simple @{Wrapper.Group#GROUP} or @{Wrapper.Unit#UNIT} object. +-- @return #AUFTRAG self +function AUFTRAG:NewINTERCEPT(Target) + + local mission=AUFTRAG:New(AUFTRAG.Type.INTERCEPT) + + mission:_TargetFromObject(Target) + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.INTERCEPT + mission.missionFraction=0.1 + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionROT=ENUMS.ROT.EvadeFire + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- Create a CAP mission. +-- @param #AUFTRAG self +-- @param Core.Zone#ZONE_RADIUS ZoneCAP Circular CAP zone. Detected targets in this zone will be engaged. +-- @param #number Altitude Altitude at which to orbit in feet. Default is 10,000 ft. +-- @param #number Speed Orbit speed in knots. Default 350 kts. +-- @param Core.Point#COORDINATE Coordinate Where to orbit. Default is the center of the CAP zone. +-- @param #number Heading Heading of race-track pattern in degrees. If not specified, a simple circular orbit is performed. +-- @param #number Leg Length of race-track in NM. If not specified, a simple circular orbit is performed. +-- @param #table TargetTypes Table of target types. Default {"Air"}. +-- @return #AUFTRAG self +function AUFTRAG:NewCAP(ZoneCAP, Altitude, Speed, Coordinate, Heading, Leg, TargetTypes) + + -- Ensure given TargetTypes parameter is a table. + if TargetTypes then + if type(TargetTypes)~="table" then + TargetTypes={TargetTypes} + end + end + + -- Create ORBIT first. + local mission=AUFTRAG:NewORBIT(Coordinate or ZoneCAP:GetCoordinate(), Altitude or 10000, Speed, Heading, Leg) + + -- Mission type CAP. + mission.type=AUFTRAG.Type.CAP + mission:_SetLogID() + + -- DCS task parameters: + mission.engageZone=ZoneCAP + mission.engageTargetTypes=TargetTypes or {"Air"} + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.CAP + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionROT=ENUMS.ROT.EvadeFire + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- Create a CAS mission. +-- @param #AUFTRAG self +-- @param Core.Zone#ZONE_RADIUS ZoneCAS Circular CAS zone. Detected targets in this zone will be engaged. +-- @param #number Altitude Altitude at which to orbit. Default is 10,000 ft. +-- @param #number Speed Orbit speed in knots. Default 350 KIAS. +-- @param Core.Point#COORDINATE Coordinate Where to orbit. Default is the center of the CAS zone. +-- @param #number Heading Heading of race-track pattern in degrees. If not specified, a simple circular orbit is performed. +-- @param #number Leg Length of race-track in NM. If not specified, a simple circular orbit is performed. +-- @param #table TargetTypes (Optional) Table of target types. Default {"Helicopters", "Ground Units", "Light armed ships"}. +-- @return #AUFTRAG self +function AUFTRAG:NewCAS(ZoneCAS, Altitude, Speed, Coordinate, Heading, Leg, TargetTypes) + + -- Ensure given TargetTypes parameter is a table. + if TargetTypes then + if type(TargetTypes)~="table" then + TargetTypes={TargetTypes} + end + end + + -- Create ORBIT first. + local mission=AUFTRAG:NewORBIT(Coordinate or ZoneCAS:GetCoordinate(), Altitude or 10000, Speed, Heading, Leg) + + -- Mission type CAS. + mission.type=AUFTRAG.Type.CAS + mission:_SetLogID() + + -- DCS Task options: + mission.engageZone=ZoneCAS + mission.engageTargetTypes=TargetTypes or {"Helicopters", "Ground Units", "Light armed ships"} + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.CAS + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionROT=ENUMS.ROT.EvadeFire + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- Create a FACA mission. +-- @param #AUFTRAG self +-- @param Wrapper.Group#GROUP Target Target group. Must be a GROUP object. +-- @param #string Designation Designation of target. See `AI.Task.Designation`. Default `AI.Task.Designation.AUTO`. +-- @param #boolean DataLink Enable data link. Default `true`. +-- @param #number Frequency Radio frequency in MHz the FAC uses for communication. Default is 133 MHz. +-- @param #number Modulation Radio modulation band. Default 0=AM. Use 1 for FM. See radio.modulation.AM or radio.modulaton.FM. +-- @return #AUFTRAG self +function AUFTRAG:NewFACA(Target, Designation, DataLink, Frequency, Modulation) + + local mission=AUFTRAG:New(AUFTRAG.Type.FACA) + + mission:_TargetFromObject(Target) + + -- TODO: check that target is really a group object! + + -- DCS Task options: + mission.facDesignation=Designation --or AI.Task.Designation.AUTO + mission.facDatalink=true + mission.facFreq=Frequency or 133 + mission.facModu=Modulation or radio.modulation.AM + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.AFAC + mission.missionAltitude=nil + mission.missionFraction=0.5 + mission.optionROE=ENUMS.ROE.ReturnFire + mission.optionROT=ENUMS.ROT.PassiveDefense + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + + +--- Create a BAI mission. +-- @param #AUFTRAG self +-- @param Wrapper.Positionable#POSITIONABLE Target The target to attack. Can be a GROUP, UNIT or STATIC object. +-- @param #number Altitude Engage altitude in feet. Default 2000 ft. +-- @return #AUFTRAG self +function AUFTRAG:NewBAI(Target, Altitude) + + local mission=AUFTRAG:New(AUFTRAG.Type.BAI) + + mission:_TargetFromObject(Target) + + -- DCS Task options: + mission.engageWeaponType=ENUMS.WeaponFlag.AnyAG + mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL + mission.engageAltitude=Altitude or UTILS.FeetToMeters(2000) + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.GROUNDATTACK + mission.missionAltitude=mission.engageAltitude + mission.missionFraction=0.75 + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionROT=ENUMS.ROT.PassiveDefense + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- Create a SEAD mission. +-- @param #AUFTRAG self +-- @param Wrapper.Positionable#POSITIONABLE Target The target to attack. Can be a GROUP or UNIT object. +-- @param #number Altitude Engage altitude in feet. Default 2000 ft. +-- @return #AUFTRAG self +function AUFTRAG:NewSEAD(Target, Altitude) + + local mission=AUFTRAG:New(AUFTRAG.Type.SEAD) + + mission:_TargetFromObject(Target) + + -- DCS Task options: + mission.engageWeaponType=ENUMS.WeaponFlag.AnyAG --ENUMS.WeaponFlag.Cannons + mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL + mission.engageAltitude=Altitude or UTILS.FeetToMeters(2000) + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.SEAD + mission.missionAltitude=mission.engageAltitude + mission.missionFraction=0.2 + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionROT=ENUMS.ROT.EvadeFire + --mission.optionROT=ENUMS.ROT.AllowAbortMission + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- Create a STRIKE mission. Flight will attack the closest map object to the specified coordinate. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Target The target coordinate. Can also be given as a GROUP, UNIT or STATIC object. +-- @param #number Altitude Engage altitude in feet. Default 2000 ft. +-- @return #AUFTRAG self +function AUFTRAG:NewSTRIKE(Target, Altitude) + + local mission=AUFTRAG:New(AUFTRAG.Type.STRIKE) + + mission:_TargetFromObject(Target) + + -- DCS Task options: + mission.engageWeaponType=ENUMS.WeaponFlag.AnyAG + mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL + mission.engageAltitude=UTILS.FeetToMeters(Altitude or 2000) + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.GROUNDATTACK + mission.missionAltitude=mission.engageAltitude + mission.missionFraction=0.75 + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionROT=ENUMS.ROT.PassiveDefense + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- Create a BOMBING mission. Flight will drop bombs a specified coordinate. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Target Target coordinate. Can also be specified as a GROUP, UNIT or STATIC object. +-- @param #number Altitude Engage altitude in feet. Default 25000 ft. +-- @return #AUFTRAG self +function AUFTRAG:NewBOMBING(Target, Altitude) + + local mission=AUFTRAG:New(AUFTRAG.Type.BOMBING) + + mission:_TargetFromObject(Target) + + -- DCS task options: + mission.engageWeaponType=ENUMS.WeaponFlag.AnyBomb + mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL + mission.engageAltitude=UTILS.FeetToMeters(Altitude or 25000) + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.GROUNDATTACK + mission.missionAltitude=mission.engageAltitude*0.8 + mission.missionFraction=0.5 + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionROT=ENUMS.ROT.NoReaction -- No reaction is better. + + -- Evaluate result after 5 min. We might need time until the bombs have dropped and targets have been detroyed. + mission.dTevaluate=5*60 + + -- Get DCS task. + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- Create a BOMBRUNWAY mission. +-- @param #AUFTRAG self +-- @param Wrapper.Airbase#AIRBASE Airdrome The airbase to bomb. This must be an airdrome (not a FARP or ship) as these to not have a runway. +-- @param #number Altitude Engage altitude in feet. Default 25000 ft. +-- @return #AUFTRAG self +function AUFTRAG:NewBOMBRUNWAY(Airdrome, Altitude) + + if type(Airdrome)=="string" then + Airdrome=AIRBASE:FindByName(Airdrome) + end + + local mission=AUFTRAG:New(AUFTRAG.Type.BOMBRUNWAY) + + mission:_TargetFromObject(Airdrome) + + -- DCS task options: + mission.engageWeaponType=ENUMS.WeaponFlag.AnyBomb + mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL + mission.engageAltitude=UTILS.FeetToMeters(Altitude or 25000) + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.RUNWAYATTACK + mission.missionAltitude=mission.engageAltitude*0.8 + mission.missionFraction=0.2 + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionROT=ENUMS.ROT.PassiveDefense + + -- Evaluate result after 5 min. + mission.dTevaluate=5*60 + + -- Get DCS task. + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- Create a CARPET BOMBING mission. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Target Target coordinate. Can also be specified as a GROUP, UNIT or STATIC object. +-- @param #number Altitude Engage altitude in feet. Default 25000 ft. +-- @param #number CarpetLength Length of bombing carpet in meters. Default 500 m. +-- @return #AUFTRAG self +function AUFTRAG:NewBOMBCARPET(Target, Altitude, CarpetLength) + + local mission=AUFTRAG:New(AUFTRAG.Type.BOMBCARPET) + + mission:_TargetFromObject(Target) + + -- DCS task options: + mission.engageWeaponType=ENUMS.WeaponFlag.AnyBomb + mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL + mission.engageAltitude=UTILS.FeetToMeters(Altitude or 25000) + mission.engageCarpetLength=CarpetLength or 500 + mission.engageAsGroup=false -- Looks like this must be false or the task is not executed. It is not available in the ME anyway but in the task of the mission file. + mission.engageDirection=nil -- This is also not available in the ME. + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.GROUNDATTACK + mission.missionAltitude=mission.engageAltitude*0.8 + mission.missionFraction=0.5 + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionROT=ENUMS.ROT.NoReaction + + -- Evaluate result after 5 min. + mission.dTevaluate=5*60 + + -- Get DCS task. + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + + +--- Create an ESCORT (or FOLLOW) mission. Flight will escort another group and automatically engage certain target types. +-- @param #AUFTRAG self +-- @param Wrapper.Group#GROUP EscortGroup The group to escort. +-- @param DCS#Vec3 OffsetVector A table with x, y and z components specifying the offset of the flight to the escorted group. Default {x=200, y=0, z=-100} for 200 meters to the right, same alitude, 100 meters behind. +-- @param #number EngageMaxDistance Max engage distance of targets in meters. Default auto (*nil*). +-- @param #table TargetTypes Types of targets to engage automatically. Default is {"Air"}, i.e. all enemy airborne units. Use an empty set {} for a simple "FOLLOW" mission. +-- @return #AUFTRAG self +function AUFTRAG:NewESCORT(EscortGroup, OffsetVector, EngageMaxDistance, TargetTypes) + + local mission=AUFTRAG:New(AUFTRAG.Type.ESCORT) + + mission:_TargetFromObject(EscortGroup) + + -- DCS task parameters: + mission.escortVec3=OffsetVector or {x=200, y=0, z=-100} + mission.engageMaxDistance=EngageMaxDistance + mission.engageTargetTypes=TargetTypes or {"Air"} + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.ESCORT + mission.missionFraction=0.1 + mission.optionROE=ENUMS.ROE.OpenFire -- TODO: what's the best ROE here? Make dependent on ESCORT or FOLLOW! + mission.optionROT=ENUMS.ROT.PassiveDefense + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- Create a RESCUE HELO mission. +-- @param #AUFTRAG self +-- @param Wrapper.Unit#UNIT Carrier The carrier unit. +-- @return #AUFTRAG self +function AUFTRAG:NewRESCUEHELO(Carrier) + + local mission=AUFTRAG:New(AUFTRAG.Type.RESCUEHELO) + + mission:_TargetFromObject(Carrier) + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.NOTHING + mission.missionFraction=0.5 + mission.optionROE=ENUMS.ROE.WeaponHold + mission.optionROT=ENUMS.ROT.NoReaction + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + + +--- Create a TROOP TRANSPORT mission. +-- @param #AUFTRAG self +-- @param Core.Set#SET_GROUP TransportGroupSet The set group(s) to be transported. +-- @param Core.Point#COORDINATE DropoffCoordinate Coordinate where the helo will land drop off the the troops. +-- @param Core.Point#COORDINATE PickupCoordinate Coordinate where the helo will land to pick up the the cargo. Default is the fist transport group. +-- @return #AUFTRAG self +function AUFTRAG:NewTROOPTRANSPORT(TransportGroupSet, DropoffCoordinate, PickupCoordinate) + + local mission=AUFTRAG:New(AUFTRAG.Type.TROOPTRANSPORT) + + if TransportGroupSet:IsInstanceOf("GROUP") then + mission.transportGroupSet=SET_GROUP:New() + mission.transportGroupSet:AddGroup(TransportGroupSet) + elseif TransportGroupSet:IsInstanceOf("SET_GROUP") then + mission.transportGroupSet=TransportGroupSet + else + mission:E(mission.lid.."ERROR: TransportGroupSet must be a GROUP or SET_GROUP object!") + return nil + end + + mission:_TargetFromObject(mission.transportGroupSet) + + mission.transportPickup=PickupCoordinate or mission:GetTargetCoordinate() + mission.transportDropoff=DropoffCoordinate + + -- Debug. + mission.transportPickup:MarkToAll("Pickup") + mission.transportDropoff:MarkToAll("Drop off") + + -- TODO: what's the best ROE here? + mission.optionROE=ENUMS.ROE.ReturnFire + mission.optionROT=ENUMS.ROT.PassiveDefense + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- Create an ARTY mission. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Target Center of the firing solution. +-- @param #number Nshots Number of shots to be fired. Default 3. +-- @param #number Radius Radius of the shells in meters. Default 100 meters. +-- @return #AUFTRAG self +function AUFTRAG:NewARTY(Target, Nshots, Radius) + + local mission=AUFTRAG:New(AUFTRAG.Type.ARTY) + + mission:_TargetFromObject(Target) + + mission.artyShots=Nshots or 3 + mission.artyRadius=Radius or 100 + + mission.optionROE=ENUMS.ROE.OpenFire + mission.missionFraction=0.1 + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + + +--- Create a mission to attack a group. Mission type is automatically chosen from the group category. +-- @param #AUFTRAG self +-- @param Wrapper.Group#GROUP EngageGroup Group to be engaged. +-- @return #AUFTRAG self +function AUFTRAG:NewAUTO(EngageGroup) + + local mission=nil --#AUFTRAG + + local group=EngageGroup + + if group and group:IsAlive() then + + local category=group:GetCategory() + local attribute=group:GetAttribute() + local threatlevel=group:GetThreatLevel() + + if category==Group.Category.AIRPLANE or category==Group.Category.HELICOPTER then + + --- + -- AIR + --- + + mission=AUFTRAG:NewINTERCEPT(group) + + elseif category==Group.Category.GROUND then + + --- + -- GROUND + --- + + --TODO: action depends on type + -- AA/SAM ==> SEAD + -- Tanks ==> + -- Artillery ==> + -- Infantry ==> + -- + + if attribute==GROUP.Attribute.GROUND_AAA or attribute==GROUP.Attribute.GROUND_SAM then + + -- SEAD/DEAD + + -- TODO: Attack radars first? Attack launchers? + + mission=AUFTRAG:NewSEAD(group) + + elseif attribute==GROUP.Attribute.GROUND_ARTILLERY then + + mission=AUFTRAG:NewBAI(group) + + elseif attribute==GROUP.Attribute.GROUND_INFANTRY then + + mission=AUFTRAG:NewBAI(group) + + else + + mission=AUFTRAG:NewBAI(group) + + end + + elseif category==Group.Category.SHIP then + + --- + -- NAVAL + --- + + mission=AUFTRAG:NewANTISHIP(group) + + end + end + + if mission then + mission:SetPriority(10, true) + end + + return mission +end + + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User API Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set mission start and stop time. +-- @param #AUFTRAG self +-- @param #string ClockStart Time the mission is started, e.g. "05:00" for 5 am. If specified as a #number, it will be relative (in seconds) to the current mission time. Default is 5 seconds after mission was added. +-- @param #string ClockStop (Optional) Time the mission is stopped, e.g. "13:00" for 1 pm. If mission could not be started at that time, it will be removed from the queue. If specified as a #number it will be relative (in seconds) to the current mission time. +-- @return #AUFTRAG self +function AUFTRAG:SetTime(ClockStart, ClockStop) + + -- Current mission time. + local Tnow=timer.getAbsTime() + + -- Set start time. Default in 5 sec. + local Tstart=Tnow+5 + if ClockStart and type(ClockStart)=="number" then + Tstart=Tnow+ClockStart + elseif ClockStart and type(ClockStart)=="string" then + Tstart=UTILS.ClockToSeconds(ClockStart) + end + + -- Set stop time. Default nil. + local Tstop=nil + if ClockStop and type(ClockStop)=="number" then + Tstop=Tnow+ClockStop + elseif ClockStop and type(ClockStop)=="string" then + Tstop=UTILS.ClockToSeconds(ClockStop) + end + + self.Tstart=Tstart + self.Tstop=Tstop + + if Tstop then + self.duration=self.Tstop-self.Tstart + end + + return self +end + +--- Set mission priority and (optional) urgency. Urgent missions can cancel other running missions. +-- @param #AUFTRAG self +-- @param #number Prio Priority 1=high, 100=low. Default 50. +-- @param #boolean Urgent If *true*, another running mission might be cancelled if it has a lower priority. +-- @return #AUFTRAG self +function AUFTRAG:SetPriority(Prio, Urgent) + self.prio=Prio or 50 + self.urgent=Urgent + return self +end + +--- Set how many times the mission is repeated if it fails. +-- @param #AUFTRAG self +-- @param #number Nrepeat Number of repeats. Default 0. +-- @return #AUFTRAG self +function AUFTRAG:SetRepeatOnFailure(Nrepeat) + self.missionRepeatMax=Nrepeat or 0 + return self +end + +--- Define how many assets are required to do the job. +-- @param #AUFTRAG self +-- @param #number Nassets Number of asset groups. Default 1. +-- @return #AUFTRAG self +function AUFTRAG:SetRequiredAssets(Nassets) + self.nassets=Nassets or 1 + return self +end + +--- Set mission name. +-- @param #AUFTRAG self +-- @param #string Name Name of the mission. Default is "Auftrag Nr. X", where X is a running number, which is automatically increased. +-- @return #AUFTRAG self +function AUFTRAG:SetName(Name) + self.name=Name or string.format("Auftrag Nr. %d", self.auftragsnummer) + return self +end + +--- Set weapon type used for the engagement. +-- @param #AUFTRAG self +-- @param #number WeaponType Weapon type. Default is ENUMS.WeaponFlag.Auto +-- @return #AUFTRAG self +function AUFTRAG:SetWeaponType(WeaponType) + + self.engageWeaponType=WeaponType or ENUMS.WeaponFlag.Auto + + -- Update the DCS task parameter. + self.DCStask=self:GetDCSMissionTask() + + return self +end + +--- Set number of weapons to expend. +-- @param #AUFTRAG self +-- @param #number WeaponExpend How much of the weapon load is expended during the attack, e.g. `AI.Task.WeaponExpend.ALL`. Default "Auto". +-- @return #AUFTRAG self +function AUFTRAG:SetWeaponExpend(WeaponExpend) + + self.engageWeaponExpend=WeaponExpend or "Auto" + + -- Update the DCS task parameter. + self.DCStask=self:GetDCSMissionTask() + + return self +end + +--- Set whether target will be attack as group. +-- @param #AUFTRAG self +-- @param #boolean Switch If true or nil, engage as group. If false, not. +-- @return #AUFTRAG self +function AUFTRAG:SetEngageAsGroup(Switch) + + if Switch==nil then + Switch=true + end + + self.engageAsGroup=Switch + + -- Update the DCS task parameter. + self.DCStask=self:GetDCSMissionTask() + + return self +end + +--- Set engage altitude. +-- @param #AUFTRAG self +-- @param #string Altitude Altitude in feet. Default 6000 ft. +-- @return #AUFTRAG self +function AUFTRAG:SetEngageAltitude(Altitude) + + self.engageAltitude=UTILS.FeetToMeters(Altitude or 6000) + + -- Update the DCS task parameter. + self.DCStask=self:GetDCSMissionTask() + + return self +end + +--- Set mission altitude. +-- @param #AUFTRAG self +-- @param #string Altitude Altitude in feet. +-- @return #AUFTRAG self +function AUFTRAG:SetMissionAltitude(Altitude) + self.missionAltitude=UTILS.FeetToMeters(Altitude) + return self +end + +--- Set max engage range. +-- @param #AUFTRAG self +-- @param #number Range Max range in NM. Default 100 NM. +-- @return #AUFTRAG self +function AUFTRAG:SetEngageRange(Range) + self.engageRange=UTILS.NMToMeters(Range or 100) + return self +end + +--- Set Rules of Engagement (ROE) for this mission. +-- @param #AUFTRAG self +-- @param #string roe Mission ROE. +-- @return #AUFTRAG self +function AUFTRAG:SetROE(roe) + + self.optionROE=roe + + return self +end + + +--- Set Reaction on Threat (ROT) for this mission. +-- @param #AUFTRAG self +-- @param #string roe Mission ROT. +-- @return #AUFTRAG self +function AUFTRAG:SetROT(rot) + + self.optionROT=rot + + return self +end + +--- Set formation for this mission. +-- @param #AUFTRAG self +-- @param #number Formation Formation. +-- @return #AUFTRAG self +function AUFTRAG:SetFormation(Formation) + + self.optionFormation=Formation + + return self +end + +--- Set radio frequency and modulation for this mission. +-- @param #AUFTRAG self +-- @param #number Frequency Frequency in MHz. +-- @param #number Modulation Radio modulation. Default 0=AM. +-- @return #AUFTRAG self +function AUFTRAG:SetRadio(Frequency, Modulation) + + self.radioFreq=Frequency + self.radioModu=Modulation or 0 + + return self +end + +--- Set TACAN beacon channel and Morse code for this mission. +-- @param #AUFTRAG self +-- @param #number Channel TACAN channel. +-- @param #string Morse Morse code. Default "XXX". +-- @return #AUFTRAG self +function AUFTRAG:SetTACAN(Channel, Morse) + + self.tacanChannel=Channel + self.tacanMorse=Morse or "XXX" + + return self +end + + +--- Add start condition. +-- @param #AUFTRAG self +-- @param #function ConditionFunction Function that needs to be true before the mission can be started. Must return a #boolean. +-- @param ... Condition function arguments if any. +-- @return #AUFTRAG self +function AUFTRAG:AddConditionStart(ConditionFunction, ...) + + local condition={} --#AUFTRAG.Condition + + condition.func=ConditionFunction + condition.arg={} + if arg then + condition.arg=arg + end + + table.insert(self.conditionStart, condition) + + return self +end + +--- Add success condition. +-- @param #AUFTRAG self +-- @param #function ConditionFunction If this function returns `true`, the mission is cancelled. +-- @param ... Condition function arguments if any. +-- @return #AUFTRAG self +function AUFTRAG:AddConditionSuccess(ConditionFunction, ...) + + local condition={} --#AUFTRAG.Condition + + condition.func=ConditionFunction + condition.arg={} + if arg then + condition.arg=arg + end + + table.insert(self.conditionSuccess, condition) + + return self +end + +--- Add failure condition. +-- @param #AUFTRAG self +-- @param #function ConditionFunction If this function returns `true`, the mission is cancelled. +-- @param ... Condition function arguments if any. +-- @return #AUFTRAG self +function AUFTRAG:AddConditionFailure(ConditionFunction, ...) + + local condition={} --#AUFTRAG.Condition + + condition.func=ConditionFunction + condition.arg={} + if arg then + condition.arg=arg + end + + table.insert(self.conditionFailure, condition) + + return self +end + + +--- Assign airwing squadron(s) to the mission. Only these squads will be considered for the job. +-- @param #AUFTRAG self +-- @param #table Squadrons A table of SQUADRONs or a single SQUADRON object. +-- @return #AUFTRAG self +function AUFTRAG:AssignSquadrons(Squadrons) + + if Squadrons:IsInstanceOf("SQUADRON") then + Squadrons={Squadrons} + end + + for _,_squad in pairs(Squadrons) do + local squadron=_squad --Ops.Squadron#SQUADRON + self:I(self.lid..string.format("Assigning squadron %s", tostring(squadron.name))) + end + + self.squadrons=Squadrons +end + + +--- Add a Ops group to the mission. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPSGROUP object. +function AUFTRAG:AddOpsGroup(OpsGroup) + self:I(self.lid..string.format("Adding Ops group %s", OpsGroup.groupname)) + + local groupdata={} --#AUFTRAG.GroupData + groupdata.opsgroup=OpsGroup + groupdata.status=AUFTRAG.GroupStatus.SCHEDULED + groupdata.waypointcoordinate=nil + groupdata.waypointindex=nil + groupdata.waypointtask=nil + + self.groupdata[OpsGroup.groupname]=groupdata + +end + +--- Remove an Ops group from the mission. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPSGROUP object. +function AUFTRAG:DelOpsGroup(OpsGroup) + self:I(self.lid..string.format("Removing OPS group %s", OpsGroup and OpsGroup.groupname or "nil (ERROR)!")) + + if OpsGroup then + + -- Remove mission form queue. + OpsGroup:RemoveMission(self) + + self.groupdata[OpsGroup.groupname]=nil + + end + +end + +--- Check if mission is PLANNED. +-- @param #AUFTRAG self +-- @return #boolean If true, mission is in the planning state. +function AUFTRAG:IsPlanned() + return self.status==AUFTRAG.Status.PLANNED +end + +--- Check if mission is QUEUED at an AIRWING mission queue. +-- @param #AUFTRAG self +-- @return #boolean If true, mission is queued. +function AUFTRAG:IsQueued() + return self.status==AUFTRAG.Status.QUEUED +end + +--- Check if mission is REQUESTED, i.e. request for WAREHOUSE assets is done. +-- @param #AUFTRAG self +-- @return #boolean If true, mission is requested. +function AUFTRAG:IsRequested() + return self.status==AUFTRAG.Status.REQUESTED +end + +--- Check if mission is SCHEDULED, i.e. request for WAREHOUSE assets is done. +-- @param #AUFTRAG self +-- @return #boolean If true, mission is queued. +function AUFTRAG:IsScheduled() + return self.status==AUFTRAG.Status.SCHEDULED +end + +--- Check if mission is STARTED, i.e. group is on its way to the mission execution waypoint. +-- @param #AUFTRAG self +-- @return #boolean If true, mission is started. +function AUFTRAG:IsStarted() + return self.status==AUFTRAG.Status.STARTED +end + +--- Check if mission is executing. +-- @param #AUFTRAG self +-- @return #boolean If true, mission is currently executing. +function AUFTRAG:IsExecuting() + return self.status==AUFTRAG.Status.EXECUTING +end + +--- Check if mission was cancelled. +-- @param #AUFTRAG self +-- @return #boolean If true, mission was cancelled. +function AUFTRAG:IsCancelled() + return self.status==AUFTRAG.Status.CANCELLED +end + +--- Check if mission is done. +-- @param #AUFTRAG self +-- @return #boolean If true, mission is done. +function AUFTRAG:IsDone() + return self.status==AUFTRAG.Status.DONE +end + +--- Check if mission was a success. +-- @param #AUFTRAG self +-- @return #boolean If true, mission was successful. +function AUFTRAG:IsSuccess() + return self.status==AUFTRAG.Status.SUCCESS +end + +--- Check if mission is over. This could be state DONE or CANCELLED. +-- @param #AUFTRAG self +-- @return #boolean If true, mission is currently executing. +function AUFTRAG:IsOver() + local over = self.status==AUFTRAG.Status.DONE or self.status==AUFTRAG.Status.CANCELLED or self.status==AUFTRAG.Status.SUCCESS or self.status==AUFTRAG.Status.FAILED + return over +end + +--- Check if mission is NOT over. +-- @param #AUFTRAG self +-- @return #boolean If true, mission is NOT over yet. +function AUFTRAG:IsNotOver() + return not self:IsOver() +end + +--- Check if mission is ready to be started. +-- * Mission start time passed. +-- * Mission stop time did not pass already. +-- * All start conditions are true. +-- @param #AUFTRAG self +-- @return #boolean If true, mission can be started. +function AUFTRAG:IsReadyToGo() + + local Tnow=timer.getAbsTime() + + -- Start time did not pass yet. + if self.Tstart and Tnowself.Tstop or false then + return false + end + + -- All start conditions true? + local startme=self:EvalConditionsAll(self.conditionStart) + + if not startme then + return false + end + + + -- We're good to go! + return true +end + +--- Check if mission is ready to be started. +-- * Mission stop already passed. +-- * Any stop condition is true. +-- @param #AUFTRAG self +-- @return #boolean If true, mission should be cancelled. +function AUFTRAG:IsReadyToCancel() + + local Tnow=timer.getAbsTime() + + -- Stop time already passed. + if self.Tstop and Tnow>self.Tstop then + return true + end + + + local failure=self:EvalConditionsAny(self.conditionFailure) + + if failure then + self.failurecondition=true + return true + end + + local success=self:EvalConditionsAny(self.conditionSuccess) + + if success then + self.successcondition=true + return true + end + + -- No criterion matched. + return false +end + +--- Check if all given condition are true. +-- @param #AUFTRAG self +-- @param #table Conditions Table of conditions. +-- @return #boolean If true, all conditions were true. Returns false if at least one condition returned false. +function AUFTRAG:EvalConditionsAll(Conditions) + + -- Any stop condition must be true. + for _,_condition in pairs(Conditions or {}) do + local condition=_condition --#AUFTRAG.Condition + + -- Call function. + local istrue=condition.func(unpack(condition.arg)) + + -- Any false will return false. + if not istrue then + return false + end + + end + + -- All conditions were true. + return true +end + + +--- Check if any of the given conditions is true. +-- @param #AUFTRAG self +-- @param #table Conditions Table of conditions. +-- @return #boolean If true, at least one condition is true. +function AUFTRAG:EvalConditionsAny(Conditions) + + -- Any stop condition must be true. + for _,_condition in pairs(Conditions or {}) do + local condition=_condition --#AUFTRAG.Condition + + -- Call function. + local istrue=condition.func(unpack(condition.arg)) + + -- Any true will return true. + if istrue then + return true + end + + end + + -- No condition was true. + return false +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Mission Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "Status" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AUFTRAG:onafterStatus(From, Event, To) + + -- Number of alive mission targets. + local Ntargets=self:CountMissionTargets() + + -- Number of alive groups attached to this mission. + local Ngroups=self:CountOpsGroups() + + -- Check if mission is not OVER yet. + if self:IsNotOver() then + + if self:CheckGroupsDone() then + + -- All groups have reported MISSON DONE. + self:Done() + + elseif (self.Tstop and timer.getAbsTime()>self.Tstop+10) or (self.Ntargets>0 and Ntargets==0) then + + -- Cancel mission if stop time passed. + self:Cancel() + + end + + end + + + -- Current FSM state. + local fsmstate=self:GetState() + local Tnow=timer.getAbsTime() + + -- Mission start stop time. + local Cstart=UTILS.SecondsToClock(self.Tstart, true) + local Cstop=self.Tstop and UTILS.SecondsToClock(self.Tstop, true) or "INF" + + local targetname=self:GetTargetName() or "unknown" + + local airwing=self.airwing and self.airwing.alias or "N/A" + local commander=self.wingcommander and tostring(self.wingcommander.coalition) or "N/A" + + -- Info message. + self:I(self.lid..string.format("Status %s: Target=%s, T=%s-%s, assets=%d, groups=%d, targets=%d, wing=%s, commander=%s", self.status, targetname, Cstart, Cstop, #self.assets, Ngroups, Ntargets, airwing, commander)) + + -- Check for error. + if fsmstate~=self.status then + self:E(self.lid..string.format("ERROR: FSM state %s != %s mission status!", fsmstate, self.status)) + end + + local text="Group data:" + for groupname,_groupdata in pairs(self.groupdata) do + local groupdata=_groupdata --#AUFTRAG.GroupData + text=text..string.format("\n- %s: status mission=%s opsgroup=%s", groupname, groupdata.status, groupdata.opsgroup and groupdata.opsgroup:GetState() or "N/A") + end + self:I(self.lid..text) + + local ready2evaluate=self.Tover and Tnow-self.Tover>=self.dTevaluate or false + + -- Check if mission is OVER (done or cancelled) and enough time passed to evaluate the result. + if self:IsOver() and ready2evaluate then + -- Evaluate success or failure of the mission. + self:Evaluate() + else + self:__Status(-30) + end + + -- Update F10 marker. + self:UpdateMarker() +end + +--- Evaluate mission outcome - success or failure. +-- @param #AUFTRAG self +-- @return #AUFTRAG self +function AUFTRAG:Evaluate() + + -- Assume success and check if any failed condition applies. + local failed=false + + -- Any success condition true? + local successCondition=self:EvalConditionsAny(self.conditionSuccess) + + -- Any failure condition true? + local failureCondition=self:EvalConditionsAny(self.conditionFailure) + + -- Target damage in %. + local targetdamage=self:GetTargetDamage() + + -- Current number of mission targets. + local Ntargets=self:CountMissionTargets() + + -- Number of current targets is still >0 ==> Not everything was destroyed. + if self.type==AUFTRAG.Type.TROOPTRANSPORT then + + if self.Ntargets>0 and Ntargets0 and Ntargets>0 then + failed=true + end + + end + + --TODO: all assets dead? Is this a FAILED criterion even if all targets have been destroyed? What if there are no initial targets (e.g. when ORBIT, PATROL, RECON missions). + + --self:I(self.lid..string.format("Evaluating mission: Initial Targets=%d, current targets=%d ==> success=%s", self.Ntargets, Ntargets, tostring(not failed))) + + if failureCondition then + failed=true + elseif successCondition then + failed=false + end + + local text=string.format("Evaluating mission:\n") + text=text..string.format("Targets = %d/%d\n", self.Ntargets, Ntargets) + text=text..string.format("Damage = %.1f %%\n", targetdamage) + text=text..string.format("Success Cond = %s\n", tostring(successCondition)) + text=text..string.format("Failure Cond = %s", tostring(failureCondition)) + self:I(self.lid..text) + + + if failed then + self:Failed() + else + self:Success() + end + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Asset Data +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get asset data table. +-- @param #AUFTRAG self +-- @param #string AssetName Name of the asset. +-- @return #AUFTRAG.GroupData Group data or *nil* if OPS group does not exist. +function AUFTRAG:GetAssetDataByName(AssetName) + return self.groupdata[tostring(AssetName)] +end + +--- Get flight data table. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. +-- @return #AUFTRAG.GroupData Flight data or nil if opsgroup does not exist. +function AUFTRAG:GetGroupData(opsgroup) + if opsgroup and self.groupdata then + return self.groupdata[opsgroup.groupname] + end + return nil +end + +--- Set opsgroup mission status. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. +-- @param #string status New status. +function AUFTRAG:SetGroupStatus(opsgroup, status) + self:I(self.lid..string.format("Setting flight %s to status %s", opsgroup and opsgroup.groupname or "nil", tostring(status))) + + --env.info("FF trying to get flight status in AUFTRAG:GetGroupStatus") + if self:GetGroupStatus(opsgroup)==AUFTRAG.GroupStatus.CANCELLED and status==AUFTRAG.GroupStatus.DONE then + -- Do not overwrite a CANCELLED status with a DONE status. + else + local groupdata=self:GetGroupData(opsgroup) + if groupdata then + groupdata.status=status + else + self:E(self.lid.."WARNING: Could not SET flight data for flight group. Setting status to DONE") + end + end + + -- Debug info. + self:I(self.lid..string.format("Setting flight %s status to %s. IsNotOver=%s CheckGroupsDone=%s", opsgroup.groupname, self:GetGroupStatus(opsgroup), tostring(self:IsNotOver()), tostring(self:CheckGroupsDone()))) + + -- Check if ALL flights are done with their mission. + if self:IsNotOver() and self:CheckGroupsDone() then + self:I(self.lid.."All flights done ==> mission DONE!") + self:Done() + else + self:T3(self.lid.."Mission NOT DONE yet!") + end + +end + +--- Get ops group mission status. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. +function AUFTRAG:GetGroupStatus(opsgroup) + self:T3(self.lid..string.format("Trying to get Flight status for flight group %s", opsgroup and opsgroup.groupname or "nil")) + + local groupdata=self:GetGroupData(opsgroup) + + if groupdata then + return groupdata.status + else + + self:E(self.lid..string.format("WARNING: Could not GET groupdata for opsgroup %s. Returning status DONE.", opsgroup and opsgroup.groupname or "nil")) + return AUFTRAG.GroupStatus.DONE + + end +end + + +--- Set Ops group waypoint coordinate. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. +-- @param Core.Point#COORDINATE coordinate Waypoint Coordinate. +function AUFTRAG:SetGroupWaypointCoordinate(opsgroup, coordinate) + local groupdata=self:GetGroupData(opsgroup) + if groupdata then + groupdata.waypointcoordinate=coordinate + end +end + +--- Get opsgroup waypoint coordinate. +-- @param #AUFTRAG self +-- @return Core.Point#COORDINATE Waypoint Coordinate. +function AUFTRAG:GetGroupWaypointCoordinate(opsgroup) + local groupdata=self:GetGroupData(opsgroup) + if groupdata then + return groupdata.waypointcoordinate + end +end + + +--- Set Ops group waypoint task. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. +-- @param Ops.OpsGroup#OPSGROUP.Task task Waypoint task. +function AUFTRAG:SetGroupWaypointTask(opsgroup, task) + self:I(self.lid..string.format("Setting waypoint task %s", task and task.description or "WTF")) + local groupdata=self:GetGroupData(opsgroup) + if groupdata then + groupdata.waypointtask=task + end +end + +--- Get opsgroup waypoint task. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. +-- @return Ops.OpsGroup#OPSGROUP.Task task Waypoint task. Waypoint task. +function AUFTRAG:GetGroupWaypointTask(opsgroup) + local groupdata=self:GetGroupData(opsgroup) + if groupdata then + return groupdata.waypointtask + end +end + +--- Set opsgroup waypoint index. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. +-- @param #number waypointindex Waypoint index. +function AUFTRAG:SetGroupWaypointIndex(opsgroup, waypointindex) + self:I(self.lid..string.format("Setting waypoint index %d", waypointindex)) + local groupdata=self:GetGroupData(opsgroup) + if groupdata then + groupdata.waypointindex=waypointindex + end +end + +--- Get opsgroup waypoint index. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. +-- @return #number Waypoint index +function AUFTRAG:GetGroupWaypointIndex(opsgroup) + local groupdata=self:GetGroupData(opsgroup) + if groupdata then + return groupdata.waypointindex + end +end + + +--- Check if all flights are done with their mission (or dead). +-- @param #AUFTRAG self +-- @return #boolean If true, all flights are done with the mission. +function AUFTRAG:CheckGroupsDone() + + -- These are early stages, where we might not even have a opsgroup defined to be checked. + if self:IsPlanned() or self:IsQueued() or self:IsRequested() then + return false + end + + -- It could be that all flights were destroyed on the way to the mission execution waypoint. + -- TODO: would be better to check if everybody is dead by now. + if self:IsStarted() and self:CountOpsGroups()==0 then + return true + end + + -- Check status of all flight groups. + for groupname,data in pairs(self.groupdata) do + local groupdata=data --#AUFTRAG.GroupData + if groupdata then + if groupdata.status==AUFTRAG.GroupStatus.DONE or groupdata.status==AUFTRAG.GroupStatus.CANCELLED then + -- This one is done or cancelled. + else + -- At least this flight is not DONE or CANCELLED. + return false + end + end + end + + return true +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- EVENT Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Unit lost event. +-- @param #AUFTRAG self +-- @param Core.Event#EVENTDATA EventData Event data. +function AUFTRAG:OnEventUnitLost(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit then + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + for _,_groupdata in pairs(self.groupdata) do + local groupdata=_groupdata --#AUFTRAG.GroupData + if groupdata and groupdata.opsgroup and groupdata.opsgroup.groupname==EventData.IniGroupName then + self:I(self.lid..string.format("UNIT LOST event for opsgroup %s unit %s", groupdata.opsgroup.groupname, EventData.IniUnitName)) + end + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "Queue" event. Mission is added to the mission queue of an AIRWING. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.AirWing#AIRWING Airwing The airwing. +function AUFTRAG:onafterQueued(From, Event, To, Airwing) + self.status=AUFTRAG.Status.QUEUED + self.airwing=Airwing + self:I(self.lid..string.format("New mission status=%s at airwing %s", self.status, tostring(Airwing.alias))) +end + + +--- On after "Requested" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AUFTRAG:onafterRequested(From, Event, To) + self.status=AUFTRAG.Status.REQUESTED + self:I(self.lid..string.format("New mission status=%s", self.status)) +end + +--- On after "Assign" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AUFTRAG:onafterAssign(From, Event, To) + self.status=AUFTRAG.Status.ASSIGNED + self:I(self.lid..string.format("New mission status=%s", self.status)) +end + +--- On after "Schedule" event. Mission is added to the mission queue of a FLIGHTGROUP. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP FlightGroup +function AUFTRAG:onafterScheduled(From, Event, To, FlightGroup) + self.status=AUFTRAG.Status.SCHEDULED + self:I(self.lid..string.format("New mission status=%s", self.status)) +end + +--- On after "Start" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AUFTRAG:onafterStarted(From, Event, To) + self.status=AUFTRAG.Status.STARTED + self:I(self.lid..string.format("New mission status=%s", self.status)) +end + +--- On after "Execute" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AUFTRAG:onafterExecuting(From, Event, To) + self.status=AUFTRAG.Status.EXECUTING + self:I(self.lid..string.format("New mission status=%s", self.status)) +end + +--- On after "Done" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AUFTRAG:onafterDone(From, Event, To) + self.status=AUFTRAG.Status.DONE + self:I(self.lid..string.format("New mission status=%s", self.status)) + + -- Set time stamp. + self.Tover=timer.getAbsTime() + +end + + +--- On after "GroupDead" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP OpsGroup The ops group that is dead now. +function AUFTRAG:onafterGroupDead(From, Event, To, OpsGroup) + + local asset=self:GetAssetByName(OpsGroup.groupname) + if asset then + self:AssetDead(asset) + end + +end + +--- On after "AssetDead" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.AirWing#AIRWING.SquadronAsset Asset The asset. +function AUFTRAG:onafterAssetDead(From, Event, To, Asset) + + -- Remove opsgroup from mission. + --self:DelOpsGroup(Asset.opsgroup) + + local N=self:CountOpsGroups() + + -- All assets dead? + if N==0 then + + if self:IsNotOver() then + + -- Cancel mission. Wait for next mission update to evaluate SUCCESS or FAILURE. + self:Cancel() + + else + + self:E(self.lid.."ERROR: All assets are dead not but mission was already over... Investigate!") + -- Now this can happen, because when a opsgroup dies (sometimes!), the mission is DONE + + end + end + + -- Remove asset from airwing. + if self.airwing then + self.airwing:RemoveAssetFromSquadron(Asset) + end + + -- Delete asset from mission. + self:DelAsset(Asset) + +end + +--- On after "Success" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AUFTRAG:onafterSuccess(From, Event, To) + + self.status=AUFTRAG.Status.SUCCESS + self:I(self.lid..string.format("New mission status=%s", self.status)) + + -- Stop mission. + self:Stop() + +end + +--- On after "Cancel" event. Cancells the mission. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AUFTRAG:onafterCancel(From, Event, To) + + -- Debug info. + self:I(self.lid..string.format("CANCELLING mission in status %s. Will wait for groups to report mission DONE before evaluation.", self.status)) + + -- Time stamp. + self.Tover=timer.getAbsTime() + + -- No more repeats. + self.missionRepeatMax=self.missionRepeated + + -- Not necessary to delay the evaluaton?! + self.dTevaluate=0 + + if self.wingcommander then + + self:I(self.lid..string.format("Wingcommander will cancel the mission. Will wait for mission DONE before evaluation!")) + + self.wingcommander:CancelMission(self) + + elseif self.airwing then + + self:I(self.lid..string.format("Airwing %s will cancel the mission. Will wait for mission DONE before evaluation!", self.airwing.alias)) + + -- Airwing will cancel all flight missions and remove queued request from warehouse queue. + self.airwing:MissionCancel(self) + + else + + self:I(self.lid..string.format("No airwing or wingcommander. Attached flights will cancel the mission on their own. Will wait for mission DONE before evaluation!")) + + for _,_groupdata in pairs(self.groupdata) do + local groupdata=_groupdata --#AUFTRAG.GroupData + groupdata.opsgroup:MissionCancel(self) + end + + end + + -- Special mission states. + if self.status==AUFTRAG.Status.PLANNED then + self:I(self.lid..string.format("Cancelled mission was in planned stage. Call it done!")) + self:Done() + end + +end + +--- On after "Failed" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AUFTRAG:onafterFailed(From, Event, To) + + self.status=AUFTRAG.Status.FAILED + self:I(self.lid..string.format("New mission status=%s", self.status)) + + if self.missionRepeated>=self.missionRepeatMax then + + self:I(self.lid..string.format("Mission FAILED! Number of max repeats reached [%d>=%d] ==> Stopping mission!", self.missionRepeated, self.missionRepeatMax)) + self:Stop() + + else + + -- Repeat mission. + self:I(self.lid..string.format("Mission failed! Repeating mission for the %d time (max %d times) ==> Repeat mission!", self.missionRepeated+1, self.missionRepeatMax)) + self:Repeat() + + end + +end + + +--- On after "Repeat" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AUFTRAG:onafterRepeat(From, Event, To) + + -- Set mission status to PLANNED. + self.status=AUFTRAG.Status.PLANNED + + self:I(self.lid..string.format("New mission status=%s (on Repeat)", self.status)) + + -- Increase repeat counter. + self.missionRepeated=self.missionRepeated+1 + + if self.wingcommander then + + elseif self.airwing then + + -- Already at the airwing ==> Queued() + self:Queued(self.airwing) + + else + + end + + + -- No mission assets. + self.assets={} + + for _,_groupdata in pairs(self.groupdata) do + local groupdata=_groupdata --#AUFTRAG.GroupData + local opsgroup=groupdata.opsgroup + if opsgroup then + self:DelOpsGroup(opsgroup) + end + + end + -- No flight data. + self.groupdata={} + + -- Call status again. + self:__Status(-30) + +end + + + +--- On after "Stop" event. Remove mission from AIRWING and FLIGHTGROUP mission queues. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AUFTRAG:onafterStop(From, Event, To) + + self:I(self.lid..string.format("STOPPED mission in status=%s. Removing missions from queues. Stopping CallScheduler!", self.status)) + + -- TODO: remove missions from queues in WINGCOMMANDER, AIRWING and FLIGHGROUPS! + -- TODO: Mission should be OVER! we dont want to remove running missions from any queues. + + if self.wingcommander then + self.wingcommander:RemoveMission(self) + end + + if self.airwing then + self.airwing:RemoveMission(self) + end + + for _,_groupdata in pairs(self.groupdata) do + local groupdata=_groupdata --#AUFTRAG.GroupData + groupdata.opsgroup:RemoveMission(self) + end + + -- No mission assets. + self.assets={} + + -- No flight data. + self.groupdata={} + + -- Clear pending scheduler calls. + self.CallScheduler:Clear() + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add asset to mission. +-- @param #AUFTRAG self +-- @param Ops.AirWing#AIRWING.SquadronAsset Asset The asset to be added to the mission. +-- @return #AUFTRAG self +function AUFTRAG:AddAsset(Asset) + + self.assets=self.assets or {} + + table.insert(self.assets, Asset) + + return self +end + +--- Delete asset from mission. +-- @param #AUFTRAG self +-- @param Ops.AirWing#AIRWING.SquadronAsset Asset The asset to be removed. +-- @return #AUFTRAG self +function AUFTRAG:DelAsset(Asset) + + for i,_asset in pairs(self.assets or {}) do + local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset + + if asset.uid==Asset.uid then + self:I(self.lid..string.format("Removing asset \"%s\" from mission", tostring(asset.spawngroupname))) + table.remove(self.assets, i) + return self + end + + end + + return self +end + +--- Get asset by its spawn group name. +-- @param #AUFTRAG self +-- @param #string Name Asset spawn group name. +-- @return Ops.AirWing#AIRWING.SquadronAsset +function AUFTRAG:GetAssetByName(Name) + + for i,_asset in pairs(self.assets or {}) do + local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset + + if asset.spawngroupname==Name then + return asset + end + + end + + return nil +end + + +--- Count alive mission targets. +-- @param #AUFTRAG self +-- @param #AUFTRAG.TargetData Target (Optional) The target object. +-- @return #number Number of alive target units. +function AUFTRAG:CountMissionTargets(Target) + + local N=0 + + Target=Target or self:GetTargetData() + + if Target then + + if Target.Type==AUFTRAG.TargetType.GROUP then + + local target=Target.Target --Wrapper.Group#GROUP + + local units=target:GetUnits() + + for _,_unit in pairs(units or {}) do + local unit=_unit --Wrapper.Unit#UNIT + + -- We check that unit is "alive" and has health >1. Somtimes units get heavily damanged but are still alive. + -- TODO: here I could introduce and count that if units have only health < 50% if mission objective is to just "damage" the units. + if unit and unit:IsAlive() and unit:GetLife()>1 then + N=N+1 + end + end + + elseif Target.Type==AUFTRAG.TargetType.UNIT then + + local target=Target.Target --Wrapper.Unit#UNIT + + if target and target:IsAlive() and target:GetLife()>1 then + N=N+1 + end + + elseif Target.Type==AUFTRAG.TargetType.STATIC then + + local target=Target.Target --Wrapper.Static#STATIC + + if target and target:IsAlive() then + N=N+1 + end + + elseif Target.Type==AUFTRAG.TargetType.AIRBASE then + + -- TODO: any (good) way to tell whether an airbase was "destroyed" or at least damaged? Is :GetLive() working? + + elseif Target.Type==AUFTRAG.TargetType.COORDINATE then + + -- No target! + + elseif Target.Type==AUFTRAG.TargetType.SETGROUP then + + for _,_group in pairs(Target.Target.Set or {}) do + local group=_group --Wrapper.Group#GROUP + + local units=group:GetUnits() + + for _,_unit in pairs(units or {}) do + local unit=_unit --Wrapper.Unit#UNIT + + -- We check that unit is "alive". + if unit and unit:IsAlive() and unit:GetLife()>1 then + N=N+1 + end + end + + end + + elseif Target.Type==AUFTRAG.TargetType.SETUNIT then + + for _,_unit in pairs(Target.Target.Set or {}) do + local unit=_unit --Wrapper.Unit#UNIT + + -- We check that unit is "alive". + if unit and unit:IsAlive() and unit:GetLife()>1 then + N=N+1 + end + + end + + else + self:E("ERROR unknown target type") + end + end + + return N +end + +--- Get target life points. +-- @param #AUFTRAG self +-- @return #number Number of initial life points when mission was planned. +function AUFTRAG:GetTargetInitialLife() + return self:GetTargetData().Lifepoints +end + +--- Get target damage. +-- @param #AUFTRAG self +-- @return #number Damage in percent. +function AUFTRAG:GetTargetDamage() + local target=self:GetTargetData() + local life=self:GetTargetLife()/self:GetTargetInitialLife() + local damage=1-life + return damage*100 +end + + +--- Get target life points. +-- @param #AUFTRAG self +-- @return #number Life points of target. +function AUFTRAG:GetTargetLife() + return self:_GetTargetLife(nil, false) +end + +--- Get target life points. +-- @param #AUFTRAG self +-- @param #AUFTRAG.TargetData Target (Optional) The target object. +-- @param #boolean Healthy Get the life points of the healthy target. +-- @return #number Life points of target. +function AUFTRAG:_GetTargetLife(Target, Healthy) + + local N=0 + + Target=Target or self:GetTargetData() + + local function _GetLife(unit) + local unit=unit --Wrapper.Unit#UNIT + if Healthy then + local life=unit:GetLife() + local life0=unit:GetLife0() + + return math.max(life, life0) + else + return unit:GetLife() + end + end + + if Target then + + if Target.Type==AUFTRAG.TargetType.GROUP then + + local target=Target.Target --Wrapper.Group#GROUP + + local units=target:GetUnits() + + for _,_unit in pairs(units or {}) do + local unit=_unit --Wrapper.Unit#UNIT + + -- We check that unit is "alive". + if unit and unit:IsAlive() then + N=N+_GetLife(unit) + end + end + + elseif Target.Type==AUFTRAG.TargetType.UNIT then + + local target=Target.Target --Wrapper.Unit#UNIT + + if target and target:IsAlive() then + N=N+_GetLife(target) + end + + elseif Target.Type==AUFTRAG.TargetType.STATIC then + + local target=Target.Target --Wrapper.Static#STATIC + + -- Statics are alive or not. + if target and target:IsAlive() then + N=N+1 --_GetLife(target) + else + N=N+0 + end + + elseif Target.Type==AUFTRAG.TargetType.AIRBASE then + + -- TODO: any (good) way to tell whether an airbase was "destroyed" or at least damaged? Is :GetLive() working? + N=N+1 + + elseif Target.Type==AUFTRAG.TargetType.COORDINATE then + + -- A coordinate does not live. + N=N+1 + + elseif Target.Type==AUFTRAG.TargetType.SETGROUP then + + for _,_group in pairs(Target.Target.Set or {}) do + local group=_group --Wrapper.Group#GROUP + + local units=group:GetUnits() + + for _,_unit in pairs(units or {}) do + local unit=_unit --Wrapper.Unit#UNIT + + -- We check that unit is "alive". + if unit and unit:IsAlive() then + N=N+_GetLife(unit) + end + end + + end + + elseif Target.Type==AUFTRAG.TargetType.SETUNIT then + + for _,_unit in pairs(Target.Target.Set or {}) do + local unit=_unit --Wrapper.Unit#UNIT + + -- We check that unit is "alive". + if unit and unit:IsAlive() then + N=N+_GetLife(unit) + end + + end + + else + self:E(self.lid.."ERROR unknown target type") + end + end + + return N +end + +--- Count alive flight groups assigned for this mission. +-- @param #AUFTRAG self +-- @return #number Number of alive flight groups. +function AUFTRAG:CountOpsGroups() + local N=0 + for _,_groupdata in pairs(self.groupdata) do + local groupdata=_groupdata --#AUFTRAG.GroupData + if groupdata and groupdata.opsgroup and groupdata.opsgroup:IsAlive() then + N=N+1 + end + end + return N +end + +--- Get coordinate of target. +-- @param #AUFTRAG self +-- @return #AUFTRAG.TargetData The target object. Could be many things. +function AUFTRAG:GetTargetData() + return self.engageTarget +end + +--- Get mission objective object. Could be many things depending on the mission type. +-- @param #AUFTRAG self +-- @return Wrapper.Positionable#POSITIONABLE The target object. Could be many things. +function AUFTRAG:GetObjective() + return self:GetTargetData().Target +end + +--- Get type of target. +-- @param #AUFTRAG self +-- @return #string The target type. +function AUFTRAG:GetTargetType() + return self:GetTargetData().Type +end + +--- Get 2D vector of target. +-- @param #AUFTRAG self +-- @return DCS#VEC2 The target 2D vector or *nil*. +function AUFTRAG:GetTargetVec2() + local coord=self:GetTargetCoordinate() + if coord then + return coord:GetVec2() + end + return nil +end + +--- Get coordinate of target. +-- @param #AUFTRAG self +-- @return Core.Point#COORDINATE The target coordinate or *nil*. +function AUFTRAG:GetTargetCoordinate() + + if self.transportPickup then + + -- Special case where we defined a + return self.transportPickup + + else + + local target + + if self:GetTargetType()==AUFTRAG.TargetType.COORDINATE then + + -- Here the objective itself is a COORDINATE. + return self:GetObjective() + + elseif self:GetTargetType()==AUFTRAG.TargetType.SETGROUP then + + -- Return the first group in the set. + -- TODO: does this only return ALIVE groups?! + return self:GetObjective():GetFirst():GetCoordinate() + + elseif self:GetTargetType()==AUFTRAG.TargetType.SETUNIT then + + -- Return the first unit in the set. + -- TODO: does this only return ALIVE units?! + return self:GetObjective():GetFirst():GetCoordinate() + + else + + -- In all other cases the GetCoordinate() function should work. + return self:GetObjective():GetCoordinate() + + end + end + + return nil +end + +--- Get name of the target. +-- @param #AUFTRAG self +-- @return #string Name of the target or "N/A". +function AUFTRAG:GetTargetName() + + if self.engageTarget.Target then + return self.engageTarget.Name + end + + return "N/A" +end + + +--- Get distance to target. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE FromCoord The coordinate from which the distance is measured. +-- @return #number Distance in meters or 0. +function AUFTRAG:GetTargetDistance(FromCoord) + + local TargetCoord=self:GetTargetCoordinate() + + if TargetCoord and FromCoord then + return TargetCoord:Get2DDistance(FromCoord) + else + self:E(self.lid.."ERROR: TargetCoord or FromCoord does not exist in AUFTRAG:GetTargetDistance() function! Returning 0") + end + + return 0 +end + +--- Get coordinate of target. First unit/group of the set is used. +-- @param #AUFTRAG self +-- @return #string +function AUFTRAG:GetMissionTypesText(MissionTypes) + + local text="" + for _,missiontype in pairs(MissionTypes) do + text=text..string.format("%s, ", missiontype) + end + + return text +end + +--- Get coordinate of target. First unit/group of the set is used. +-- @param #AUFTRAG self +-- @param Wrapper.Group#GROUP group Group. +-- @return Core.Point#COORDINATE Coordinate where the mission is executed. +function AUFTRAG:GetMissionWaypointCoord(group) + + -- Create waypoint coordinate half way between us and the target. + local waypointcoord=group:GetCoordinate():GetIntermediateCoordinate(self:GetTargetCoordinate(), self.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 self.missionAltitude then + waypointcoord:SetAltitude(self.missionAltitude, true) + end + env.info(string.format("FF mission alt=%d meters", waypointcoord.y)) + + return waypointcoord +end + + +--- Set log ID string. +-- @param #AUFTRAG self +-- @return #AUFTRAG self +function AUFTRAG:_SetLogID() + self.lid=string.format("Auftrag #%d %s | ", self.auftragsnummer, tostring(self.type)) + return self +end + +--- Update mission F10 map marker. +-- @param #AUFTRAG self +-- @return #AUFTRAG self +function AUFTRAG:UpdateMarker() + + -- Marker text. + local text=string.format("%s %s: %s", self.name, self.type:upper(), self.status:upper()) + text=text..string.format("\n%s", self:GetTargetName()) + text=text..string.format("\nTargets %d/%d, Life Points=%d/%d", self:CountMissionTargets(), self.Ntargets, self:GetTargetLife(), self:GetTargetInitialLife()) + text=text..string.format("\nFlights %d/%d", self:CountOpsGroups(), self.nassets) + + if not self.marker then + + -- Get target coordinates. Can be nil! + local targetcoord=self:GetTargetCoordinate() + + self.marker=MARKER:New(targetcoord, text):ReadOnly():ToAll() + + else + + if self.marker:GetText()~=text then + self.marker:UpdateText(text) + end + + end + + return self +end + +--- Get DCS task table for the given mission. +-- @param #AUFTRAG self +-- @param Wrapper.Controllable#CONTROLLABLE TaskControllable The controllable for which this task is set. Most tasks don't need it. +-- @return DCS#Task The DCS task table. If multiple tasks are necessary, this is returned as a combo task. +function AUFTRAG:GetDCSMissionTask(TaskControllable) + + local DCStasks={} + + -- Create DCS task based on current self. + if self.type==AUFTRAG.Type.ANTISHIP then + + ---------------------- + -- ANTISHIP Mission -- + ---------------------- + + self:_GetDCSAttackTask(self.engageTarget, DCStasks) + + elseif self.type==AUFTRAG.Type.AWACS then + + ------------------- + -- AWACS Mission -- + ------------------- + + local DCStask=CONTROLLABLE.EnRouteTaskAWACS(nil) + + table.insert(self.enrouteTasks, DCStask) + + elseif self.type==AUFTRAG.Type.BAI then + + ----------------- + -- BAI Mission -- + ----------------- + + self:_GetDCSAttackTask(self.engageTarget, DCStasks) + + elseif self.type==AUFTRAG.Type.BOMBING then + + --------------------- + -- BOMBING Mission -- + --------------------- + + local DCStask=CONTROLLABLE.TaskBombing(nil, self:GetTargetVec2(), self.engageAsGroup, self.engageWeaponExpend, self.engageQuantity, self.engageDirection, self.engageAltitude, self.engageWeaponType, Divebomb) + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.BOMBRUNWAY then + + ------------------------ + -- BOMBRUNWAY Mission -- + ------------------------ + + local DCStask=CONTROLLABLE.TaskBombingRunway(nil, self.engageTarget.Target, self.engageWeaponType, self.engageWeaponExpend, self.engageQuantity, self.engageDirection, self.engageAsGroup) + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.BOMBCARPET then + + ------------------------ + -- BOMBCARPET Mission -- + ------------------------ + + local DCStask=CONTROLLABLE.TaskCarpetBombing(nil, self:GetTargetVec2(), self.engageAsGroup, self.engageWeaponExpend, self.engageQuantity, self.engageDirection, self.engageAltitude, self.engageWeaponType, self.engageCarpetLength) + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.CAP then + + ----------------- + -- CAP Mission -- + ----------------- + + local DCStask=CONTROLLABLE.EnRouteTaskEngageTargetsInZone(nil, self.engageZone:GetVec2(), self.engageZone:GetRadius(), self.engageTargetTypes, Priority) + + table.insert(self.enrouteTasks, DCStask) + + elseif self.type==AUFTRAG.Type.CAS then + + ----------------- + -- CAS Mission -- + ----------------- + + local DCStask=CONTROLLABLE.EnRouteTaskEngageTargetsInZone(nil, self.engageZone:GetVec2(), self.engageZone:GetRadius(), self.engageTargetTypes, Priority) + + table.insert(self.enrouteTasks, DCStask) + + elseif self.type==AUFTRAG.Type.ESCORT then + + -------------------- + -- ESCORT Mission -- + -------------------- + + local DCStask=CONTROLLABLE.TaskEscort(nil, self.engageTarget.Target, self.escortVec3, LastWaypointIndex, self.engageMaxDistance, self.engageTargetTypes) + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.FACA then + + ----------------- + -- FAC Mission -- + ----------------- + + local DCStask=CONTROLLABLE.TaskFAC_AttackGroup(nil, self.engageTarget.Target, self.engageWeaponType, self.facDesignation, self.facDatalink, self.facFrequency, self.facModulation, CallsignName, CallsignNumber) + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.FERRY then + + ------------------- + -- FERRY Mission -- + ------------------- + + -- TODO: Ferry mission type. How? + + elseif self.type==AUFTRAG.Type.INTERCEPT then + + ----------------------- + -- INTERCEPT Mission -- + ----------------------- + + self:_GetDCSAttackTask(self.engageTarget, DCStasks) + + elseif self.type==AUFTRAG.Type.ORBIT then + + ------------------- + -- ORBIT Mission -- + ------------------- + + -- Done below as also other mission types use the orbit task. + + elseif self.type==AUFTRAG.Type.PATROL then + + -------------------- + -- PATROL Mission -- + -------------------- + + -- Done below as also other mission types use the orbit task. + + elseif self.type==AUFTRAG.Type.RECON then + + ------------------- + -- RECON Mission -- + ------------------- + + -- TODO: What? Table of coordinates? + + elseif self.type==AUFTRAG.Type.SEAD then + + ------------------ + -- SEAD Mission -- + ------------------ + + --[[ + local DCStask=CONTROLLABLE.EnRouteTaskEngageTargets(nil, nil ,{"Air Defence"} , 0) + table.insert(self.enrouteTasks, DCStask) + DCStask.key="SEAD" + ]] + + self:_GetDCSAttackTask(self.engageTarget, DCStasks) + + elseif self.type==AUFTRAG.Type.STRIKE then + + -------------------- + -- STRIKE Mission -- + -------------------- + + local DCStask=CONTROLLABLE.TaskAttackMapObject(nil, self:GetTargetVec2(), self.engageAsGroup, self.engageWeaponExpend, self.engageQuantity, self.engageDirection, self.engageAltitude, self.engageWeaponType) + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.TANKER then + + -------------------- + -- TANKER Mission -- + -------------------- + + local DCStask=CONTROLLABLE.EnRouteTaskTanker(nil) + + table.insert(self.enrouteTasks, DCStask) + + elseif self.type==AUFTRAG.Type.TROOPTRANSPORT then + + ---------------------------- + -- TROOPTRANSPORT Mission -- + ---------------------------- + + -- Task to embark the troops at the pick up point. + local TaskEmbark=CONTROLLABLE.TaskEmbarking(TaskControllable, self.transportPickup, self.transportGroupSet, self.transportWaitForCargo) + + -- Task to disembark the troops at the drop off point. + local TaskDisEmbark=CONTROLLABLE.TaskDisembarking(TaskControllable, self.transportDropoff, self.transportGroupSet) + + table.insert(DCStasks, TaskEmbark) + table.insert(DCStasks, TaskDisEmbark) + + elseif self.type==AUFTRAG.Type.RESCUEHELO then + + ------------------------- + -- RESCUE HELO Mission -- + ------------------------- + + local DCStask={} + + DCStask.id="Formation" + + -- We create a "fake" DCS task and pass the parameters to the FLIGHTGROUP. + local param={} + param.unitname=self:GetTargetName() + param.offsetX=200 + param.offsetZ=240 + param.altitude=70 + param.dtFollow=1.0 + + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.ARTY then + + ------------------ + -- ARTY Mission -- + ------------------ + + local DCStask=CONTROLLABLE.TaskFireAtPoint(nil, self:GetTargetVec2(), self.artyRadius, self.artyShots, self.engageWeaponType) + + table.insert(DCStasks, DCStask) + + else + self:E(self.lid..string.format("ERROR: Unknown mission task!")) + return nil + end + + + -- Set ORBIT task. Also applies to other missions: AWACS, TANKER, CAP, CAS. + if self.type==AUFTRAG.Type.ORBIT or + self.type==AUFTRAG.Type.CAP or + self.type==AUFTRAG.Type.CAS or + self.type==AUFTRAG.Type.PATROL or + self.type==AUFTRAG.Type.AWACS or + self.type==AUFTRAG.Type.TANKER then + + ------------------- + -- ORBIT Mission -- + ------------------- + + local Coordinate=self:GetTargetCoordinate() + + local DCStask=CONTROLLABLE.TaskOrbit(nil, Coordinate, self.orbitAltitude, self.orbitSpeed, self.orbitRaceTrack) + + table.insert(DCStasks, DCStask) + + end + + self:I({missiontask=DCStasks}) + + -- Return the task. + if #DCStasks==1 then + return DCStasks[1] + else + return CONTROLLABLE.TaskCombo(nil, DCStasks) + end + +end + +--- Get DCS task table for an attack group or unit task. +-- @param #AUFTRAG self +-- @param #AUFTRAG.TargetData target Target data. +-- @param #table DCStasks DCS DCS tasks table to which the task is added. +-- @return DCS#Task The DCS task table. +function AUFTRAG:_GetDCSAttackTask(target, DCStasks) + + local DCStask=nil + + if target.Type==AUFTRAG.TargetType.GROUP then + + DCStask=CONTROLLABLE.TaskAttackGroup(nil, target.Target, self.engageWeaponType, self.engageWeaponExpend, self.engageQuantity, self.engageDirection, self.engageAltitude, self.engageAsGroup) + + table.insert(DCStasks, DCStask) + + elseif target.Type==AUFTRAG.TargetType.UNIT or target.Type==AUFTRAG.TargetType.STATIC then + + DCStask=CONTROLLABLE.TaskAttackUnit(nil, target.Target, self.engageAsGroup, self.WeaponExpend, self.engageQuantity, self.engageDirection, self.engageAltitude, self.engageWeaponType) + + table.insert(DCStasks, DCStask) + + elseif target.Type==AUFTRAG.TargetType.SETGROUP then + + -- Add all groups. + for _,group in pairs(target.Target.Set or {}) do + DCStask=CONTROLLABLE.TaskAttackGroup(nil, group, self.engageWeaponType, self.engageWeaponExpend, self.engageQuantity, self.engageDirection, self.engageAltitude, self.engageAsGroup) + table.insert(DCStasks, DCStask) + end + + elseif target.Type==AUFTRAG.TargetType.SETUNIT then + + -- Add tasks to attack all units. + for _,unit in pairs(target.Target.Set or {}) do + DCStask=CONTROLLABLE.TaskAttackUnit(nil, unit, self.engageAsGroup, self.WeaponExpend, self.engageQuantity, self.engageDirection, self.engageAltitude, self.engageWeaponType) + table.insert(DCStasks, DCStask) + end + + end + + return DCStasks +end + +--- Create target data from a given object. +-- @param #AUFTRAG self +-- @param Wrapper.Positionable#POSITIONABLE Object The target GROUP, UNIT, STATIC. +-- @return #AUFTRAG.TargetData Target. +function AUFTRAG:_TargetFromObject(Object) + + local target={} --#AUFTRAG.TargetData + + -- The object. + target.Target=Object + + if Object:IsInstanceOf("GROUP") then + + target.Type=AUFTRAG.TargetType.GROUP + + local object=Object --Wrapper.Group#GROUP + + target.Name=object:GetName() + + elseif Object:IsInstanceOf("UNIT") then + + target.Type=AUFTRAG.TargetType.UNIT + + local object=Object --Wrapper.Unit#UNIT + + target.Name=object:GetName() + + elseif Object:IsInstanceOf("STATIC") then + + target.Type=AUFTRAG.TargetType.STATIC + + target.Name=Object:GetName() + + elseif Object:IsInstanceOf("COORDINATE") then + + target.Type=AUFTRAG.TargetType.COORDINATE + + local object=Object --Core.Point#COORDINATE + + target.Name=object:ToStringLLDMS() + + elseif Object:IsInstanceOf("AIRBASE") then + + target.Type=AUFTRAG.TargetType.AIRBASE + + local object=Object --Wrapper.Airbase#AIRBASE + + target.Name=object:GetName() + + elseif Object:IsInstanceOf("SET_GROUP") then + + target.Type=AUFTRAG.TargetType.SETGROUP + + local object=Object --Core.Set#SET_GROUP + + target.Name=object:GetFirst():GetName() + + elseif Object:IsInstanceOf("SET_UNIT") then + + target.Type=AUFTRAG.TargetType.SETUNIT + + local object=Object --Core.Set#SET_UNIT + + target.Name=object:GetFirst():GetName() + + else + self:E(self.lid.."ERROR: Unknown object given as target. Needs to be a GROUP, UNIT, STATIC, COORDINATE") + return nil + end + + + -- Number of initial targets. + local Ninitial=self:CountMissionTargets(target) + + -- Initial total life point. + local Lifepoints=self:_GetTargetLife(target, true) + + -- Set engage Target. + self.engageTarget=target + self.engageTarget.Ninital=Ninitial + self.engageTarget.Lifepoints=Lifepoints + + -- TODO: get rid of this. + self.Ntargets=Ninitial + + -- Debug info. + self:I(self.lid..string.format("Mission Target %s Type=%s, Ntargets=%d, Lifepoints=%d", target.Name, target.Type, Ninitial, Lifepoints)) + + return target +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua new file mode 100644 index 000000000..01f48b2c0 --- /dev/null +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -0,0 +1,3758 @@ +--- **Ops** - Enhanced Airborne 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 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 actype Type name of the aircraft. +-- @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 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 #boolean ishelo If true, the is a helicopter group. +-- +-- @extends Ops.OpsGroup#OPSGROUP + +--- *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", + homebase = nil, + destbase = nil, + homezone = nil, + destzone = 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, + ishelo = nil, +} + + +--- 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 @{#OPSGROUP.ElementStatus}. +-- @field #number damage Damage of element in percent. +-- @field Wrapper.Airbase#AIRBASE.ParkingSpot parking The parking spot table the element is parking on. + + +--- FLIGHTGROUP class version. +-- @field #string version +FLIGHTGROUP.version="0.5.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- 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 Wrapper.Group#GROUP group The group object. Can also be given as #string with the 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(group, autostart) + + -- First check if we already have a flight group for this group. + local fg=_DATABASE:GetFlightGroup(group) + if fg then + fg:I(fg.lid..string.format("WARNING: Flight group already exists in data base!")) + return fg + end + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, OPSGROUP:New(group)) -- #FLIGHTGROUP + + -- Set some string id for output to DCS.log file. + self.lid=string.format("FLIGHTGROUP %s | ", self.groupname) + + -- Defaults + self:SetFuelLowThreshold() + self:SetFuelCriticalThreshold() + self:SetDefaultROE() + self:SetDefaultROT() + self:SetDetection() + + -- Holding flag. + self.flaghold=USERFLAG:New(string.format("%s_FlagHold", self.groupname)) + self.flaghold:Set(0) + + -- Add FSM transitions. + -- From State --> Event --> To State + 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("*", "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("*", "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("Airborne", "EngageTargets", "Engaging") -- Engage targets. + self:AddTransition("Engaging", "Disengage", "Airborne") -- Engagement over. + + + 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("*", "ElementOutOfAmmo", "*") -- An element is completely out of ammo. + + + 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. + + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- 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. + + -- TODO: Add pseudo functions. + + -- Debug trace. + if false then + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(1) + end + + -- Add to data base. + _DATABASE:AddFlightGroup(self) + + -- 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) + + -- Init waypoints. + self:InitWaypoints() + + -- Initialize group. + self:_InitGroup() + + -- Start the status monitoring. + self:__CheckZone(-1) + self:__Status(-2) + self:__QueueUpdate(-3) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- 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 + +--- 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 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 + + +--- 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 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 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 + + + +--- 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 + +--- 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 + +--- 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 fuel10 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==OPSGROUP.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~=OPSGROUP.ElementStatus.DEAD and element.status~=OPSGROUP.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==OPSGROUP.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==OPSGROUP.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:GetGroupStatus(self), tostring(mission.status), Cstart, Cstop, mission.prio, tostring(mission:GetGroupWaypointIndex(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 fuelmin1 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, OPSGROUP.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, OPSGROUP.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, OPSGROUP.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, OPSGROUP.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, OPSGROUP.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, OPSGROUP.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, OPSGROUP.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, OPSGROUP.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, OPSGROUP.ElementStatus.DEAD) +end + + +--- On after "Spawned" event. Sets the template, initializes the waypoints. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterSpawned(From, Event, To) + self:I(self.lid..string.format("Flight spawned!")) + + if self.ai then + + -- Set default ROE and ROT options. + self:SetOptionROE(self.roe) + self:SetOptionROT(self.rot) + + -- 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 + + -- Update route. + self:__UpdateRoute(-0.5) + + 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(OPSGROUP.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:_CheckGroupDone() + + 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 + +--- 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:_CheckGroupDone(delay) + + if self:IsAlive() and self.ai then + + if delay and delay>0 then + -- Delayed call. + self:ScheduleOnce(delay, FLIGHTGROUP._CheckGroupDone, 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 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:_CheckGroupDone(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 "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 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 + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Task functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Mission functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Special Task Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- 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() + + -- Define category. + self.isAircraft=true + self.isNaval=false + self.isGround=false + + -- 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) + + -- Add elemets. + for _,unit in pairs(self.group:GetUnits()) do + local element=self:AddElementByName(unit:GetName()) + end + + -- 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()) + + -- 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=OPSGROUP.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) + + if unit:IsAlive() then + self:ElementSpawned(element) + end + + return element + 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 d %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:_CheckGroupDone(1) + self:__UpdateRoute(-1) + end + + return self +end + +--- Add an AIR 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:AddWaypoint(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==OPSGROUP.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:GetGroupWaypointIndex(self) + + -- Increase number if this waypoint lies in the future. + if wpidx and wpidx>=wpnumber then + mission:SetGroupWaypointIndex(self, wpidx+1) + end + + end + + -- Update route. + if updateroute==nil or updateroute==true then + self:_CheckGroupDone(1) + --self:__UpdateRoute(-1) + end + + return wpnumber +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 + + + +--- 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 + +--- 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 + +--- 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 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 --Wrapper.Group#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 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("*", "FullStop", "Holding") -- Hold position. + self:AddTransition("*", "Cruise", "Cruising") -- Hold position. + + self:AddTransition("*", "TurnIntoWind", "*") -- Command the group to turn into the wind. + self:AddTransition("*", "TurningStarted", "*") -- Group started turning. + self:AddTransition("*", "TurningStopped", "*") -- Group stopped turning. + + self:AddTransition("*", "Dive", "Diving") -- Command a submarine to dive. + self:AddTransition("Diving", "Surface", "Cruising") -- Command a submarine to go to the surface. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Stop". Stops the NAVYGROUP and all its event handlers. + -- @param #NAVYGROUP self + + --- Triggers the FSM event "Stop" after a delay. Stops the NAVYGROUP and all its event handlers. + -- @function [parent=#NAVYGROUP] __Stop + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + + -- TODO: Add pseudo functions. + + + -- Init waypoints. + self:InitWaypoints() + + -- Initialize the group. + self:_InitGroup() + + -- Debug trace. + if false then + self.Debug=true + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(1) + end + + -- Handle events: + self:HandleEvent(EVENTS.Birth, self.OnEventBirth) + self:HandleEvent(EVENTS.Dead, self.OnEventDead) + self:HandleEvent(EVENTS.RemoveUnit, self.OnEventRemoveUnit) + + -- Start the status monitoring. + self:__CheckZone(-1) + self:__Status(-2) + self:__QueueUpdate(-3) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add a *scheduled* task. +-- @param #NAVYGROUP self +-- @param Core.Point#COORDINATE Coordinate Coordinate of the target. +-- @param #number Radius Radius in meters. Default 100 m. +-- @param #number Nshots Number of shots to fire. Default 3. +-- @param #number WeaponType Type of weapon. Default auto. +-- @param #string Clock Time when to start the attack. +-- @param #number Prio Priority of the task. +function NAVYGROUP:AddTaskFireAtPoint(Coordinate, Radius, Nshots, WeaponType, Clock, Prio) + + local DCStask=CONTROLLABLE.TaskFireAtPoint(nil, Coordinate:GetVec2(), Radius, Nshots, WeaponType) + + self:AddTask(DCStask, Clock, nil, Prio) + +end + +--- Add a *scheduled* task. +-- @param #NAVYGROUP self +-- @param Wrapper.Group#GROUP TargetGroup Target group. +-- @param #number WeaponExpend How much weapons does are used. +-- @param #number WeaponType Type of weapon. Default auto. +-- @param #string Clock Time when to start the attack. +-- @param #number Prio Priority of the task. +function NAVYGROUP:AddTaskAttackGroup(TargetGroup, WeaponExpend, WeaponType, Clock, Prio) + + local DCStask=CONTROLLABLE.TaskAttackGroup(nil, TargetGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit, GroupAttack) + + self:AddTask(DCStask, Clock, nil, Prio) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Update status. +-- @param #NAVYGROUP self +function NAVYGROUP:onafterStatus(From, Event, To) + + -- FSM state. + local fsmstate=self:GetState() + + --- + -- Detection + --- + + -- Check if group has detected any units. + if self.detectionOn then + self:_CheckDetectedUnits() + end + + + -- Current heading and position of the carrier. + local hdg=self:GetHeading() + local pos=self:GetCoordinate() + local speed=self.group:GetVelocityKNOTS() + + -- Check if group started or stopped turning. + self:_CheckTurning() + + -- Check water is ahead. + local collision=self:_CheckCollisionCoord(pos:Translate(self.collisiondist or 5000, hdg)) + + local intowind=false + if self.intowind then + + if timer.getAbsTime()>=self.intowind.Tstop then + + if self.intowind.Uturn then + self:UpdateRoute(self.currentwp) + else + self:UpdateRoute() + end + + self.intowind=nil + + else + intowind=true + end + + end + + -- Get number of tasks and missions. + local nTaskTot, nTaskSched, nTaskWP=self:CountRemainingTasks() + local nMissions=self:CountRemainingMissison() + + -- Info text. + local text=string.format("State %s: Speed=%.1f knots Heading=%03d intowind=%s turning=%s collision=%s Tasks=%d Missions=%d", fsmstate, speed, hdg, tostring(intowind), tostring(self.turning), tostring(collision), nTaskTot, nMissions) + self:I(self.lid..text) + + + --- + -- 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 --Ops.OpsGroup#OPSGROUP.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==OPSGROUP.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==OPSGROUP.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:GetGroupStatus(self), tostring(mission.status), Cstart, Cstop, mission.prio, tostring(mission:GetGroupWaypointIndex(self)), mission:CountMissionTargets()) + end + self:I(self.lid..text) + end + + + + -- Next check in ~30 seconds. + if not self:IsStopped() then + self:__Status(-10) + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Events +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "ElementSpawned" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #NAVYGROUP.Element Element The group element. +function NAVYGROUP:onafterElementSpawned(From, Event, To, Element) + self:I(self.lid..string.format("Element spawned %s", Element.name)) + + -- Set element status. + self:_UpdateStatus(Element, OPSGROUP.ElementStatus.SPAWNED) + +end + +--- On after "Spawned" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function NAVYGROUP:onafterSpawned(From, Event, To) + self:I(self.lid..string.format("Group spawned!")) + + if self.ai then + + -- Set default ROE and ROT options. + self:SetOptionROE(self.roe) + + end + + -- Get orientation. + self.Corientlast=self.group:GetUnit(1):GetOrientationX() + + -- Update route. + self:__Cruise(-1) + +end + +--- On after "UpdateRoute" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number n Waypoint number. Default is next waypoint. +-- @param #number Speed Speed in knots. Default cruise speed. +-- @param #number Depth Depth in meters. Default 0 meters. +function NAVYGROUP:onafterUpdateRoute(From, Event, To, n, Speed, Depth) + + -- 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 waypoints={} + + -- Speed. + local speed=Speed and UTILS.KnotsToKmph(Speed) or self.speedCruise + + -- Depth for submarines. + local depth=Depth or 0 + + local current=self:GetCoordinate():WaypointNaval(speed, depth) + table.insert(waypoints, current) + + -- Add remaining waypoints to route. + for i=n, #self.waypoints do + local wp=self.waypoints[i] + + -- Set speed. + wp.speed=UTILS.KmphToMps(speed) + wp.alt=-depth --Depth and -Depth or wp.alt + + -- Add waypoint. + table.insert(waypoints, wp) + end + + + if #waypoints>1 then + + self:I(self.lid..string.format("Updateing route: WP=%d, Speed=%.1f knots, depth=%d meters", #self.waypoints-n+1, UTILS.KmphToKnots(speed), depth)) + + -- Route group to all defined waypoints remaining. + self:Route(waypoints) + + else + + --- + -- No waypoints left + --- + + self:I(self.lid..string.format("No waypoints left")) + + -- TODO: Switch to waypoint 1 + + --self:UpdateRoute(1) + + end + +end + +--- On after "TurnIntoWind" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number Duration Duration in seconds. +-- @param #number Speed Speed in knots. +-- @param #boolean Uturn Return to the place we came from. +function NAVYGROUP:onafterTurnIntoWind(From, Event, To, Duration, Speed, Uturn) + + local headingTo=self:GetCoordinate():GetWind(50) + + local intowind={} --#NAVYGROUP.IntoWind + intowind.Speed=Speed + intowind.Tstart=timer.getAbsTime() + intowind.Tstop=intowind.Tstart+Duration + intowind.Uturn=Uturn or false + intowind.Heading=headingTo + + self.intowind=intowind + + self:I(self.lid..string.format("Steaming into wind: Heading=%03d Speed=%.1f knots, Tstart=%d Tstop=%d", intowind.Heading, intowind.Speed, intowind.Tstart, intowind.Tstop)) + + local distance=UTILS.NMToMeters(1000) + + local wp={} + + local coord=self:GetCoordinate() + local Coord=coord:Translate(distance, headingTo) + + wp[1]=coord:WaypointNaval(Speed) + wp[2]=Coord:WaypointNaval(Speed) + + self:Route(wp) + +end + +--- On after "FullStop" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function NAVYGROUP:onafterFullStop(From, Event, To) + + -- Get current position. + local pos=self:GetCoordinate() + + -- Create a new waypoint. + local wp=pos:WaypointNaval(0) + + -- Create new route consisting of only this position ==> Stop! + self:Route({wp}) + +end + +--- On after "Cruise" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function NAVYGROUP:onafterCruise(From, Event, To) + + self:UpdateRoute() + +end + +--- On after "Dive" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number Depth Dive depth in meters. +function NAVYGROUP:onafterDive(From, Event, To, Depth) + + env.info("FF Diving") + self:UpdateRoute(nil, nil, Depth) + +end + +--- On after "Surface" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number Depth Dive depth in meters. +function NAVYGROUP:onafterSurface(From, Event, To) + + self:UpdateRoute(nil, nil, 0) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Events DCS +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Event function handling the birth of a unit. +-- @param #NAVYGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function NAVYGROUP: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 + + if self.respawning then + + local function reset() + self.respawning=nil + end + + -- Reset switch in 1 sec. This should allow all birth events of n>1 groups to have passed. + -- TODO: Can I do this more rigorously? + self:ScheduleOnce(1, reset) + + else + + -- Get element. + local element=self:GetElementByName(unitname) + + -- Set element to spawned state. + self:T3(self.lid..string.format("EVENT: Element %s born ==> spawned", element.name)) + self:ElementSpawned(element) + + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Routing +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add an a waypoint to the route. +-- @param #NAVYGROUP 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 11 kts. +-- @param #boolean updateroute If true or nil, call UpdateRoute. If false, no call. +-- @return #number Waypoint index. +function NAVYGROUP:AddWaypoint(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 11 + + -- Speed at waypoint. + local speedkmh=UTILS.KnotsToKmph(speed) + + -- Create a Naval waypoint. + local wp=coordinate:WaypointNaval(speedkmh) + + -- Add to table. + table.insert(self.waypoints, wpnumber, wp) + + -- Debug info. + self:T(self.lid..string.format("Adding NAVAL 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 --Ops.OpsGroup#OPSGROUP.Task + if task.type==OPSGROUP.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:GetGroupWaypointIndex(self) + + -- Increase number if this waypoint lies in the future. + if wpidx and wpidx>=wpnumber then + mission:SetGroupWaypointIndex(self, wpidx+1) + end + + end + + -- Update route. + if updateroute==nil or updateroute==true then + self:_CheckGroupDone(1) + end + + return wpnumber +end + +--- Initialize group parameters. Also initializes waypoints if self.waypoints is nil. +-- @param #NAVYGROUP self +-- @return #NAVYGROUP self +function NAVYGROUP:_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() + + -- Define category. + self.isAircraft=false + self.isNaval=true + self.isGround=false + + + -- Helo group. + --self.isSubmarine=self.group:IsSubmarine() + + -- Ships are always AI. + self.ai=true + + -- Is (template) group late activated. + self.isLateActivated=self.template.lateActivation + + -- Naval groups cannot be uncontrolled. + self.isUncontrolled=false + + -- Max speed in km/h. + self.speedmax=self.group:GetSpeedMax() + + -- Cruise speed: 70% of max speed but within limit. + self.speedCruise=self.speedmax*0.7 + + -- Group ammo. + --self.ammo=self:GetAmmoTot() + + self.traveldist=0 + self.traveltime=timer.getAbsTime() + self.position=self:GetCoordinate() + + -- Radio parameters from template. + self.radioOn=true -- Radio is always on for ships. + self.radioFreq=tonumber(self.template.units[1].frequency)/1000000 + self.radioModu=tonumber(self.template.units[1].modulation)/1000000 + + -- 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 + + local units=self.group:GetUnits() + + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + + local element={} --#NAVYGROUP.Element + element.name=unit:GetName() + element.typename=unit:GetTypeName() + element.status=OPSGROUP.ElementStatus.INUTERO + table.insert(self.elements, element) + + self:GetAmmoUnit(unit, true) + + if unit:IsAlive() then + self:ElementSpawned(element) + end + + end + + -- Get first unit. This is used to extract other parameters. + local unit=self.group:GetUnit(1) + + if unit then + + self.descriptors=unit:GetDesc() + + self.actype=unit:GetTypeName() + + -- Debug info. + local text=string.format("Initialized Navy 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("Speed cruise = %.1f Knots\n", UTILS.KmphToKnots(self.speedCruise)) + 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())) + self:I(self.lid..text) + + -- Init done. + self.groupinitialized=true + + end + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check for possible collisions between two coordinates. +-- @param #NAVYGROUP self +-- @param Core.Point#COORDINATE coordto Coordinate to which the collision is check. +-- @param Core.Point#COORDINATE coordfrom Coordinate from which the collision is check. +-- @return #boolean If true, surface type ahead is not deep water. +-- @return #number Max free distance in meters. +function NAVYGROUP:_CheckCollisionCoord(coordto, coordfrom) + + -- Increment in meters. + local dx=100 + + -- From coordinate. Default 500 in front of the carrier. + local d=0 + if coordfrom then + d=0 + else + d=250 + coordfrom=self:GetCoordinate():Translate(d, self:GetHeading()) + end + + -- Distance between the two coordinates. + local dmax=coordfrom:Get2DDistance(coordto) + + -- Direction. + local direction=coordfrom:HeadingTo(coordto) + + -- Scan path between the two coordinates. + local clear=true + while d<=dmax do + + -- Check point. + local cp=coordfrom:Translate(d, direction) + + -- Check if surface type is water. + if not cp:IsSurfaceTypeWater() then + + -- Debug mark points. + if self.Debug or true then + local st=cp:GetSurfaceType() + cp:MarkToAll(string.format("Collision check surface type %d", st)) + end + + -- Collision WARNING! + clear=false + break + end + + -- Increase distance. + d=d+dx + end + + local text="" + if clear then + text=string.format("Path into direction %03d° is clear for the next %.1f NM.", direction, UTILS.MetersToNM(d)) + else + text=string.format("Detected obstacle at distance %.1f NM into direction %03d°.", UTILS.MetersToNM(d), direction) + end + self:T(self.lid..text) + + return not clear, d +end + +--- Check if group is turning. +-- @param #NAVYGROUP self +function NAVYGROUP:_CheckTurning() + + -- Current orientation of carrier. + local vNew=self.group:GetUnit(1):GetOrientationX() + + -- Last orientation from 30 seconds ago. + local vLast=self.Corientlast or vNew + + -- We only need the X-Z plane. + vNew.y=0 ; vLast.y=0 + + -- Angle between current heading and last time we checked ~30 seconds ago. + local deltaLast=math.deg(math.acos(UTILS.VecDot(vNew,vLast)/UTILS.VecNorm(vNew)/UTILS.VecNorm(vLast))) + + -- Last orientation becomes new orientation + self.Corientlast=vNew + + -- Carrier is turning when its heading changed by at least two degrees since last check. + local turning=math.abs(deltaLast)>=2 + + -- Check if turning stopped. + if self.turning and not turning then + + -- Carrier was turning but is not any more. + self:TurningStopped() + + elseif turning and not self.turning then + + -- Carrier was not turning but is now. + self:TurningStarted() + + end + + -- Update turning. + self.turning=turning + +end + +--- Check if group 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 #NAVYGROUP self +-- @param #number delay Delay in seconds. +function NAVYGROUP:_CheckGroupDone(delay) + + if self:IsAlive() and self.ai then + + if delay and delay>0 then + -- Delayed call. + self:ScheduleOnce(delay, NAVYGROUP._CheckGroupDone, self) + else + + if not self.passedfinalwp then + self:UpdateRoute() + end + + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua new file mode 100644 index 000000000..bb6979c23 --- /dev/null +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -0,0 +1,2815 @@ +--- **Ops** - Generic group enhancement functions. +-- +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Ops.OpsGroup +-- @image OPS_OpsGroup.png + + +--- OPSGROUP class. +-- @type OPSGROUP +-- @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 the group. +-- @field Wrapper.Group#GROUP group Group object. +-- @field #table template Template of the group. +-- @field #boolean isLateActivated Is the group late activated. +-- @field #boolean isUncontrolled Is the group uncontrolled. +-- @field #table elements Table of elements, i.e. units of the group. +-- @field #boolean ai If true, group is purely AI. +-- @field #boolean isAircraft If true, group is airplane or helicopter. +-- @field #boolean isNaval If true, group is ships or submarine. +-- @field #boolean isGround If true, group is some ground unit. +-- @field #table waypoints Table of waypoints. +-- @field #table waypoints0 Table of initial waypoints. +-- @field #number 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 #string attribute Generalized attribute. +-- @field #number speedmax Max speed in km/h. +-- @field #number speedCruise Cruising speed in km/h. +-- @field #boolean passedfinalwp Group has passed the final waypoint. +-- @field #boolean respawning Group is being respawned. +-- @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 detectionOn If true, detected units of the group are analyzed. +-- @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 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. +-- +-- @extends Core.Fsm#FSM + +--- *Something must be left to chance; nothing is sure in a sea fight above all.* --- Horatio Nelson +-- +-- === +-- +-- ![Banner Image](..\Presentations\OPSGROUP\OpsGroup_Main.jpg) +-- +-- # The OPSGROUP Concept +-- +-- The OPSGROUP class contains common functions used by other classes such as FLIGHGROUP and NAVYGROUP. +-- +-- This class is **not** meant to be used itself by the end user. +-- +-- +-- @field #OPSGROUP +OPSGROUP = { + ClassName = "OPSGROUP", + Debug = false, + verbose = 0, + lid = nil, + groupname = nil, + group = nil, + template = nil, + isLateActivated = nil, + waypoints = nil, + waypoints0 = nil, + currentwp = 1, + elements = {}, + taskqueue = {}, + taskcounter = nil, + taskcurrent = nil, + taskenroute = nil, + taskpaused = {}, + missionqueue = {}, + currentmission = nil, + detectedunits = {}, + attribute = nil, + checkzones = nil, + inzones = nil, + groupinitialized = nil, + respawning = nil, +} + +--- Status of group element. +-- @type OPSGROUP.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. +OPSGROUP.ElementStatus={ + INUTERO="inutero", + SPAWNED="spawned", + PARKING="parking", + ENGINEON="engineon", + TAXIING="taxiing", + TAKEOFF="takeoff", + AIRBORNE="airborne", + LANDING="landing", + LANDED="landed", + ARRIVED="arrived", + DEAD="dead", +} + +--- Ops group task status. +-- @type OPSGROUP.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. +OPSGROUP.TaskStatus={ + SCHEDULED="scheduled", + EXECUTING="executing", + PAUSED="paused", + DONE="done", +} + +--- Ops group task status. +-- @type OPSGROUP.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. +OPSGROUP.TaskType={ + SCHEDULED="scheduled", + WAYPOINT="waypoint", +} + +--- Task structure. +-- @type OPSGROUP.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 OPSGROUP.EnrouteTask +-- @field DCS#Task DCStask DCS task structure table. +-- @field #number WaypointIndex Waypoint number at which the enroute task is added. + +--- Ammo data. +-- @type OPSGROUP.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 Torpedos Amount of torpedos. +-- @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. +-- @field #number MissilesCR Amount of cruise missiles. +-- @field #number MissilesBM Amount of ballistic missiles. + +--- NavyGroup version. +-- @field #string version +OPSGROUP.version="0.0.1" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Implement common functions. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new OPSGROUP class object. +-- @param #OPSGROUP self +-- @param Wrapper.Group#GROUP Group The group object. Can also be given by its group name as #string. +-- @return #OPSGROUP self +function OPSGROUP:New(Group) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #OPSGROUP + + -- Get group and group name. + if type(Group)=="string" then + self.groupname=Group + self.group=GROUP:FindByName(self.groupname) + else + self.group=Group + self.groupname=Group:GetName() + end + + -- Set some string id for output to DCS.log file. + self.lid=string.format("OPSGROUP %s |", self.groupname) + + -- Init set of detected units. + self.detectedunits=SET_UNIT:New() + + -- Init inzone set. + self.inzones=SET_ZONE:New() + + -- Init task counter. + self.taskcurrent=0 + self.taskcounter=0 + + -- Start state. + self:SetStartState("InUtero") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("InUtero", "Spawned", "Spawned") -- The whole group was spawned. + self:AddTransition("*", "Dead", "Dead") -- The whole group is dead. + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + self:AddTransition("*", "Status", "*") -- Status update. + self:AddTransition("*", "QueueUpdate", "*") -- Update task and mission queues. + + self:AddTransition("*", "UpdateRoute", "*") -- Update route of group. Only if airborne. + self:AddTransition("*", "Respawn", "*") -- Respawn group. + self:AddTransition("*", "PassingWaypoint", "*") -- Passing waypoint. + + 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("*", "PassingWaypoint", "*") -- Group passed a waypoint. + self:AddTransition("*", "GotoWaypoint", "*") -- Group switches to a specific waypoint. + + 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("*", "CheckZone", "*") -- Check if group enters/leaves a certain zone. + self:AddTransition("*", "EnterZone", "*") -- Group entered a certain zone. + self:AddTransition("*", "LeaveZone", "*") -- Group leaves a certain zone. + + 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("*", "ElementSpawned", "*") -- An element was spawned. + self:AddTransition("*", "ElementDead", "*") -- An element is dead. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- 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. + + -- TODO: Add pseudo functions. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get coalition. +-- @param #OPSGROUP self +-- @return #number Coalition side of carrier. +function OPSGROUP:GetCoalition() + return self.group:GetCoalition() +end + +--- Returns the absolute (average) life points of the group. +-- @param #OPSGROUP self +-- @return #number Life points. If group contains more than one element, the average is given. +-- @return #number Initial life points. +function OPSGROUP:GetLifePoints() + if self.group then + return self.group:GetLife(), self.group:GetLife0() + end +end + +--- Set detection on or off. +-- @param #OPSGROUP self +-- @param #boolean Switch If true, detection is on. If false or nil, detection is off. Default is off. +-- @return #OPSGROUP self +function OPSGROUP: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 #OPSGROUP self +-- @param Core.Set#SET_ZONE CheckZonesSet Set of zones. +-- @return #OPSGROUP self +function OPSGROUP: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 #OPSGROUP self +-- @param Core.Zone#ZONE CheckZone Zone to check. +-- @return #OPSGROUP self +function OPSGROUP: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 #OPSGROUP self +-- @return Core.Set#SET_UNIT Set of detected units. +function OPSGROUP:GetDetectedUnits() + return self.detectedunits +end + +--- Get MOOSE GROUP object. +-- @param #OPSGROUP self +-- @return Wrapper.Group#GROUP Moose group object. +function OPSGROUP:GetGroup() + return self.group +end + +--- Get the group name. +-- @param #OPSGROUP self +-- @return #string Group name. +function OPSGROUP:GetName() + return self.groupname +end + +--- Get current coordinate of the group. +-- @param #OPSGROUP self +-- @return Core.Point#COORDINATE The coordinate (of the first unit) of the group. +function OPSGROUP: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 current heading of the group. +-- @param #OPSGROUP self +-- @return #number Current heading of the group in degrees. +function OPSGROUP:GetHeading() + if self:IsAlive()~=nil then + return self.group:GetHeading() + else + self:E(self.lid.."WARNING: Group is not alive. Cannot get heading!") + end + return nil +end + +--- Get waypoint. +-- @param #OPSGROUP self +-- @param #number indx Waypoint index. +-- @return #table Waypoint table. +function OPSGROUP:GetWaypoint(indx) + return self.waypoints[indx] +end + +--- Get final waypoint. +-- @param #OPSGROUP self +-- @return #table Waypoint table. +function OPSGROUP:GetWaypointFinal() + return self.waypoints[#self.waypoints] +end + +--- Get next waypoint. +-- @param #OPSGROUP self +-- @return #table Waypoint table. +function OPSGROUP:GetWaypointNext() + local n=math.min(self.currentwp+1, #self.waypoints) + return self.waypoints[n] +end + +--- Get current waypoint. +-- @param #OPSGROUP self +-- @return #table Waypoint table. +function OPSGROUP:GetWaypointCurrent() + return self.waypoints[self.currentwp] +end + +--- Activate a *late activated* group. +-- @param #OPSGROUP self +-- @param #number delay (Optional) Delay in seconds before the group is activated. Default is immediately. +-- @return #OPSGROUP self +function OPSGROUP: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, OPSGROUP.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 #OPSGROUP 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 OPSGROUP:SelfDestruction(Delay, ExplosionPower) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP.SelfDestruction, self, 0, ExplosionPower) + else + + -- Loop over all elements. + for i,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + local unit=element.unit + + if unit and unit:IsAlive() then + unit:Explode(ExplosionPower) + end + end + end + +end + +--- Check if group 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 + +--- Check if this group is currently "late activated" and needs to be "activated" to appear in the mission. +-- @param #OPSGROUP self +-- @return #boolean Is this the group late activated? +function OPSGROUP:IsLateActivated() + return self.isLateActivated +end + +--- Check if group is in state in utero. +-- @param #OPSGROUP self +-- @return #boolean If true, group is not spawned yet. +function OPSGROUP:IsInUtero() + return self:Is("InUtero") +end + +--- Check if group is in state spawned. +-- @param #OPSGROUP self +-- @return #boolean If true, group is spawned. +function OPSGROUP:IsSpawned() + return self:Is("Spawned") +end + +--- Check if group is dead. +-- @param #OPSGROUP self +-- @return #boolean If true, all units/elements of the group are dead. +function OPSGROUP:IsDead() + return self:Is("Dead") +end + +--- Check if FSM is stopped. +-- @param #OPSGROUP self +-- @return #boolean If true, FSM state is stopped. +function OPSGROUP:IsStopped() + return self:Is("Stopped") +end + +--- Check if this group is currently "uncontrolled" and needs to be "started" to begin its route. +-- @param #OPSGROUP self +-- @return #boolean If this group uncontrolled. +function OPSGROUP:IsUncontrolled() + return self.isUncontrolled +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Waypoint Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Remove a waypoint. +-- @param #OPSGROUP self +-- @param #number wpindex Waypoint number. +-- @return #OPSGROUP self +function OPSGROUP: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 --#OPSGROUP.Task + if task.type==OPSGROUP.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:GetGroupWaypointIndex(self) + + -- Reduce number if this waypoint lies in the future. + if wpidx and wpidx>wpindex then + mission:SetGroupWaypointIndex(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:_CheckGroupDone() + + 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 + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Task Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- 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 + + 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 #OPSGROUP self +-- @param #table DCSTask DCS task structure. +-- @return #OPSGROUP self +function OPSGROUP: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 #OPSGROUP self +-- @param #table DCSTask DCS task structure. +-- @return #OPSGROUP self +function OPSGROUP: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 #OPSGROUP 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 #OPSGROUP.Task The task structure. +function OPSGROUP:AddTaskWaypoint(task, waypointindex, description, prio, duration) + + -- Increase counter. + self.taskcounter=self.taskcounter+1 + + -- Task data structure. + local newtask={} --#OPSGROUP.Task + newtask.description=description + newtask.status=OPSGROUP.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=OPSGROUP.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:_CheckGroupDone(1) + self:__UpdateRoute(-1) + + return newtask +end + +--- Add an *enroute* task. +-- @param #OPSGROUP self +-- @param #table task DCS task table structure. +function OPSGROUP: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 + +--- Get the unfinished waypoint tasks +-- @param #OPSGROUP self +-- @param #number n Waypoint index. Counting starts at one. +-- @return #table Table of tasks. Table could also be empty {}. +function OPSGROUP: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 --#OPSGROUP.Task + if task.type==OPSGROUP.TaskType.WAYPOINT and task.status==OPSGROUP.TaskStatus.SCHEDULED and task.waypoint==n then + table.insert(tasks, task) + end + end + + return tasks +end + +--- Sort task queue. +-- @param #OPSGROUP self +function OPSGROUP:_SortTaskQueue() + + -- Sort results table wrt prio and then start time. + local function _sort(a, b) + local taskA=a --#OPSGROUP.Task + local taskB=b --#OPSGROUP.Task + return (taskA.prio=task.time then + return task + end + end + + return nil +end + +--- Get the currently executed task if there is any. +-- @param #OPSGROUP self +-- @return #OPSGROUP.Task Current task or nil. +function OPSGROUP:GetTaskCurrent() + return self:GetTaskByID(self.taskcurrent, OPSGROUP.TaskStatus.EXECUTING) +end + +--- Get task by its id. +-- @param #OPSGROUP self +-- @param #number id Task id. +-- @param #string status (Optional) Only return tasks with this status, e.g. OPSGROUP.TaskStatus.SCHEDULED. +-- @return #OPSGROUP.Task The task or nil. +function OPSGROUP:GetTaskByID(id, status) + + for _,_task in pairs(self.taskqueue) do + local task=_task --#OPSGROUP.Task + + if task.id==id then + if status==nil or status==task.status then + return task + end + end + + end + + return nil +end + +--- On after TaskExecute event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP.Task Task The task. +function OPSGROUP:onafterTaskExecute(From, Event, To, Task) + + -- Debug message. + local text=string.format("Task %s ID=%d execute.", tostring(Task.description), Task.id) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:I(self.lid..text) + + -- Cancel current task if there is any. + if self.taskcurrent>0 then + self:TaskCancel() + end + + -- Set current task. + self.taskcurrent=Task.id + + -- Set time stamp. + Task.timestamp=timer.getAbsTime() + + -- Task status executing. + Task.status=OPSGROUP.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==OPSGROUP.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("OPSGROUP._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 "TaskCancel" event. Cancels the current task or simply sets the status to DONE if the task is not the current one. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP.Task Task The task to cancel. Default is the current task (if any). +function OPSGROUP: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==OPSGROUP.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 #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP.Task Task +function OPSGROUP:onbeforeTaskDone(From, Event, To, Task) + + local allowed=true + + if Task.status==OPSGROUP.TaskStatus.PAUSED then + allowed=false + end + + return allowed +end + +--- On after "TaskDone" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP.Task Task +function OPSGROUP: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=OPSGROUP.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:GetGroupStatus(self) + + if status~=AUFTRAG.GroupStatus.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 ==> _CheckGroupDone in 1 sec") + self:_CheckGroupDone(1) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Mission Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add mission to queue. +-- @param #OPSGROUP self +-- @param Ops.Auftrag#AUFTRAG Mission Mission for this group. +-- @return #OPSGROUP self +function OPSGROUP:AddMission(Mission) + + -- Add group to mission. + Mission:AddOpsGroup(self) + + -- Set group status to SCHEDULED.. + Mission:SetGroupStatus(self, AUFTRAG.GroupStatus.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 #OPSGROUP self +-- @param Ops.Auftrag#AUFTRAG Mission Mission to be removed. +-- @return #OPSGROUP self +function OPSGROUP: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:GetGroupWaypointTask(self) + + if Task then + self:RemoveTask(Task) + end + + -- Remove mission from queue. + table.remove(self.missionqueue, i) + + return self + end + + end + + return self +end + +--- Count remaining missons. +-- @param #OPSGROUP self +-- @return #number Number of missions to be done. +function OPSGROUP:CountRemainingMissison() + + local N=0 + + -- Loop over mission queue. + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission and mission:IsNotOver() then + + -- Get group status. + local status=mission:GetGroupStatus(self) + + if status~=AUFTRAG.GroupStatus.DONE and status~=AUFTRAG.GroupStatus.CANCELLED then + N=N+1 + end + + end + end + + return N +end + +--- Get next mission. +-- @param #OPSGROUP self +-- @return Ops.Auftrag#AUFTRAG Next mission or *nil*. +function OPSGROUP:_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.prio0 then + -- Delayed call. + self:ScheduleOnce(delay, OPSGROUP.RouteToMission, self, mission) + else + + -- Next waypoint. + local nextwaypoint=self.currentwp+1 + + -- Get coordinate where the mission is executed. + local waypointcoord=mission:GetMissionWaypointCoord(self.group) + + -- Add enroute tasks. + for _,task in pairs(mission.enrouteTasks) do + self:AddTaskEnroute(task) + end + + -- Add waypoint. + self:AddWaypoint(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:SetGroupWaypointTask(self, waypointtask) + + -- Set waypoint index. + mission:SetGroupWaypointIndex(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 + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Queue Update: Missions & Tasks +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "QueueUpdate" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterQueueUpdate(From, Event, To) + + --- + -- Mission + --- + + -- First check if group is alive? Late activated groups are activated and uncontrolled units are started automatically. + if self:IsAlive()~=nil then + + local mission=self:_GetNextMission() + + if mission then + + local currentmission=self:GetMissionCurrent() + + if currentmission then + + -- Current mission but new mission is urgent with higher prio. + if mission.urgent and mission.prio0 then + for i,_task in pairs(tasks) do + local task=_task --#OPSGROUP.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 --Ops.OpsGroup#OPSGROUP.Task + + -- Task execute. + table.insert(taskswp, self.group:TaskFunction("OPSGROUP._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("OPSGROUP._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:_CheckGroupDone(1) + end + + end +end + +--- On after "GotoWaypoint" event. Group will got to the given waypoint and execute its route from there. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number n The goto waypoint number. +function OPSGROUP: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 --#OPSGROUP.Task + task.status=OPSGROUP.TaskStatus.SCHEDULED + end + + end + + -- Update the route. + self:UpdateRoute() + +end + +--- On after "DetectedUnit" event. Add newly detected unit to detected units set. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Unit#UNIT Unit The detected unit. +function OPSGROUP: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 #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Unit#UNIT Unit The detected unit. +function OPSGROUP:onafterDetectedUnitNew(From, Event, To, Unit) + self:T(self.lid..string.format("Detected New unit %s", Unit:GetName())) +end + +--- On after "EnterZone" event. Sets self.inzones[zonename]=true. +-- @param #OPSGROUP 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 OPSGROUP: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 #OPSGROUP 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 OPSGROUP: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 + +--- On after "CheckZone" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterCheckZone(From, Event, To) + + if self:IsAlive()==true then + self:_CheckInZones() + end + + if not self:IsStopped() then + self:__CheckZone(-1) + end +end + +--- Check if group is in zones. +-- @param #OPSGROUP self +function OPSGROUP:_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 group 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 group 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 #OPSGROUP self +function OPSGROUP:_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 + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Waypoints & Routing +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- 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:I(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 + + end + + return self +end + +--- Route group along waypoints. +-- @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 +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:I(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:I(opsgroup.lid..text) + + -- Set current waypoint. + opsgroup.currentwp=i + + -- Trigger PassingWaypoint event. + opsgroup:PassingWaypoint(i, final) + +end + +--- Function called when a task is executed. +--@param Wrapper.Group#GROUP group Group which should execute the task. +--@param #OPSGROUP opsgroup Ops group. +--@param #OPSGROUP.Task task Task. +function OPSGROUP._TaskExecute(group, opsgroup, task) + + -- Debug message. + local text=string.format("_TaskExecute %s", task.description) + opsgroup:T3(opsgroup.lid..text) + + -- Set current task to nil so that the next in line can be executed. + if opsgroup then + opsgroup:TaskExecute(task) + end +end + +--- Function called when a task is done. +--@param Wrapper.Group#GROUP group Group for which the task is done. +--@param #OPSGROUP opsgroup Ops group. +--@param #OPSGROUP.Task task Task. +function OPSGROUP._TaskDone(group, opsgroup, task) + + -- Debug message. + local text=string.format("_TaskDone %s", task.description) + opsgroup:T3(opsgroup.lid..text) + + -- Set current task to nil so that the next in line can be executed. + if opsgroup then + opsgroup:TaskDone(task) + end +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 #OPSGROUP self +-- @param #number roe ROE of group. Default is `ENUMS.ROE.ReturnFire`. +-- @return #OPSGROUP self +function OPSGROUP:SetDefaultROE(roe) + self.roeDefault=roe or ENUMS.ROE.ReturnFire + return self +end + +--- Set current ROE for the group. +-- @param #OPSGROUP self +-- @param #string roe ROE of group. Default is the value defined by :SetDefaultROE(). +-- @return #OPSGROUP self +function OPSGROUP:SetOptionROE(roe) + + self.roe=roe or self.roeDefault + + if self:IsAlive() then + + self.group:OptionROE(self.roe) + + self:I(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 + +--- Get current ROE of the group. +-- @param #OPSGROUP self +-- @return #number Current ROE. +function OPSGROUP:GetROE() + return self.roe +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 #OPSGROUP self +-- @param #number roe ROE of group. Default is ENUMS.ROT.PassiveDefense. +-- @return #OPSGROUP self +function OPSGROUP:SetDefaultROT(roe) + self.rotDefault=roe or ENUMS.ROT.PassiveDefense + return self +end + +--- Set ROT for the group. +-- @param #OPSGROUP self +-- @param #string rot ROT of group. Default is the value defined by :SetDefaultROT(). +-- @return #OPSGROUP self +function OPSGROUP:SetOptionROT(rot) + + self.rot=rot or self.rotDefault + + if self:IsAlive() then + + self.group:OptionROT(self.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 + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Element and Group Status Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check if all elements of the group have the same status (or are dead). +-- @param #OPSGROUP self +-- @param #string unitname Name of unit. +function OPSGROUP:_AllSameStatus(status) + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + if element.status==OPSGROUP.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 group have the same status (or are dead). +-- @param #OPSGROUP self +-- @param #string status Status to check. +-- @return #boolean If true, all elements have a similar status. +function OPSGROUP:_AllSimilarStatus(status) + + -- Check if all are dead. + if status==OPSGROUP.ElementStatus.DEAD then + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if element.status~=OPSGROUP.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 --#OPSGROUP.Element + + self:T2(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~=OPSGROUP.ElementStatus.DEAD then + + ---------- + -- ALIVE + ---------- + + if status==OPSGROUP.ElementStatus.SPAWNED then + + -- Element SPAWNED: Check that others are not still IN UTERO + if element.status~=status and + element.status==OPSGROUP.ElementStatus.INUTERO then + return false + end + + elseif status==OPSGROUP.ElementStatus.PARKING then + + -- Element PARKING: Check that the other are not still SPAWNED + if element.status~=status or + (element.status==OPSGROUP.ElementStatus.INUTERO or + element.status==OPSGROUP.ElementStatus.SPAWNED) then + return false + end + + elseif status==OPSGROUP.ElementStatus.ENGINEON then + + -- Element TAXIING: Check that the other are not still SPAWNED or PARKING + if element.status~=status and + (element.status==OPSGROUP.ElementStatus.INUTERO or + element.status==OPSGROUP.ElementStatus.SPAWNED or + element.status==OPSGROUP.ElementStatus.PARKING) then + return false + end + + elseif status==OPSGROUP.ElementStatus.TAXIING then + + -- Element TAXIING: Check that the other are not still SPAWNED or PARKING + if element.status~=status and + (element.status==OPSGROUP.ElementStatus.INUTERO or + element.status==OPSGROUP.ElementStatus.SPAWNED or + element.status==OPSGROUP.ElementStatus.PARKING or + element.status==OPSGROUP.ElementStatus.ENGINEON) then + return false + end + + elseif status==OPSGROUP.ElementStatus.TAKEOFF then + + -- Element TAKEOFF: Check that the other are not still SPAWNED, PARKING or TAXIING + if element.status~=status and + (element.status==OPSGROUP.ElementStatus.INUTERO or + element.status==OPSGROUP.ElementStatus.SPAWNED or + element.status==OPSGROUP.ElementStatus.PARKING or + element.status==OPSGROUP.ElementStatus.ENGINEON or + element.status==OPSGROUP.ElementStatus.TAXIING) then + return false + end + + elseif status==OPSGROUP.ElementStatus.AIRBORNE then + + -- Element AIRBORNE: Check that the other are not still SPAWNED, PARKING, TAXIING or TAKEOFF + if element.status~=status and + (element.status==OPSGROUP.ElementStatus.INUTERO or + element.status==OPSGROUP.ElementStatus.SPAWNED or + element.status==OPSGROUP.ElementStatus.PARKING or + element.status==OPSGROUP.ElementStatus.ENGINEON or + element.status==OPSGROUP.ElementStatus.TAXIING or + element.status==OPSGROUP.ElementStatus.TAKEOFF) then + return false + end + + elseif status==OPSGROUP.ElementStatus.LANDED then + + -- Element LANDED: check that the others are not still AIRBORNE or LANDING + if element.status~=status and + (element.status==OPSGROUP.ElementStatus.AIRBORNE or + element.status==OPSGROUP.ElementStatus.LANDING) then + return false + end + + elseif status==OPSGROUP.ElementStatus.ARRIVED then + + -- Element ARRIVED: check that the others are not still AIRBORNE, LANDING, or LANDED (taxiing). + if element.status~=status and + (element.status==OPSGROUP.ElementStatus.AIRBORNE or + element.status==OPSGROUP.ElementStatus.LANDING or + element.status==OPSGROUP.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:T2(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 group have the same status or are dead. +-- @param #OPSGROUP self +-- @param #OPSGROUP.Element element Element. +-- @param #string newstatus New status of element +-- @param Wrapper.Airbase#AIRBASE airbase Airbase if applicable. +function OPSGROUP:_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 -- #OPSGROUP.Element + self:T3(self.lid..string.format("Element %s: %s", Element.name, Element.status)) + end + + if newstatus==OPSGROUP.ElementStatus.SPAWNED then + --- + -- SPAWNED + --- + + if self:_AllSimilarStatus(newstatus) then + self:__Spawned(-0.5) + end + + elseif newstatus==OPSGROUP.ElementStatus.PARKING then + --- + -- PARKING + --- + + if self:_AllSimilarStatus(newstatus) then + self:__FlightParking(-0.5) + end + + elseif newstatus==OPSGROUP.ElementStatus.ENGINEON then + --- + -- ENGINEON + --- + + -- No FLIGHT status. Waiting for taxiing. + + elseif newstatus==OPSGROUP.ElementStatus.TAXIING then + --- + -- TAXIING + --- + + if self:_AllSimilarStatus(newstatus) then + self:__FlightTaxiing(-0.5) + end + + elseif newstatus==OPSGROUP.ElementStatus.TAKEOFF then + --- + -- TAKEOFF + --- + + if self:_AllSimilarStatus(newstatus) then + -- Trigger takeoff event. Also triggers airborne event. + self:__FlightTakeoff(-0.5, airbase) + end + + elseif newstatus==OPSGROUP.ElementStatus.AIRBORNE then + --- + -- AIRBORNE + --- + + if self:_AllSimilarStatus(newstatus) then + self:__FlightAirborne(-0.5) + end + + elseif newstatus==OPSGROUP.ElementStatus.LANDED then + --- + -- LANDED + --- + + if self:_AllSimilarStatus(newstatus) then + self:FlightLanded(airbase) + end + + elseif newstatus==OPSGROUP.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==OPSGROUP.ElementStatus.DEAD then + --- + -- DEAD + --- + + if self:_AllSimilarStatus(newstatus) then + self:Dead() + end + + end +end + +--- Set status for all elements (except dead ones). +-- @param #OPSGROUP self +-- @param #string status Element status. +function OPSGROUP:_SetElementStatusAll(status) + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if element.status~=OPSGROUP.ElementStatus.DEAD then + element.status=status + end + end + +end + +--- Get the element of a group. +-- @param #OPSGROUP self +-- @param #string unitname Name of unit. +-- @return #OPSGROUP.Element The element. +function OPSGROUP:GetElementByName(unitname) + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + if element.name==unitname then + return element + end + + end + + return nil +end + +--- Get number of elements alive. +-- @param #OPSGROUP self +-- @param #string status (Optional) Only count number, which are in a special status. +-- @return #number Number of elements. +function OPSGROUP:GetNelements(status) + + local n=0 + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if element.status~=OPSGROUP.ElementStatus.DEAD then + if element.unit and element.unit:IsAlive() then + if status==nil or element.status==status then + n=n+1 + end + end + end + end + + + return n +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 #OPSGROUP self +-- @param #OPSGROUP.Element element The element. +-- @return #OPSGROUP.Ammo Ammo data. +function OPSGROUP:GetAmmoElement(element) + return self:GetAmmoUnit(element.unit) +end + +--- Get total amount of ammunition of the whole group. +-- @param #OPSGROUP self +-- @return #OPSGROUP.Ammo Ammo data. +function OPSGROUP:GetAmmoTot() + + local units=self.group:GetUnits() + + local Ammo={} --#OPSGROUP.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 #OPSGROUP self +-- @param Wrapper.Unit#UNIT unit The unit object. +-- @param #boolean display Display ammo table as message to all. Default false. +-- @return #OPSGROUP.Ammo Ammo data. +function OPSGROUP: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 nmissilesSA=0 + local nmissilesBM=0 + local nmissilesCR=0 + local ntorps=0 + local nbombs=0 + + -- Output. + local text=string.format("OPSGROUP 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, torpedo=4 + 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.SAM then + nmissiles=nmissiles+Nammo + nmissilesSA=nmissilesSA+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.CRUISE then + nmissiles=nmissiles+Nammo + nmissilesCR=nmissilesCR+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) + + elseif Category==Weapon.Category.TORPEDO then + + -- Add up all rockets. + ntorps=ntorps+Nammo + + -- Debug info. + text=text..string.format("- %d torpedos of type %s\n", Nammo, _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+ntorps + + local ammo={} --#OPSGROUP.Ammo + ammo.Total=nammo + ammo.Guns=nshells + ammo.Rockets=nrockets + ammo.Bombs=nbombs + ammo.Torpedos=ntorps + ammo.Missiles=nmissiles + ammo.MissilesAA=nmissilesAA + ammo.MissilesAG=nmissilesAG + ammo.MissilesAS=nmissilesAS + ammo.MissilesCR=nmissilesCR + ammo.MissilesBM=nmissilesBM + ammo.MissilesSA=nmissilesSA + + return ammo +end + +--- Returns a name of a missile category. +-- @param #OPSGROUP 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 OPSGROUP:_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 + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Squadron.lua b/Moose Development/Moose/Ops/Squadron.lua new file mode 100644 index 000000000..8f65cf1d5 --- /dev/null +++ b/Moose Development/Moose/Ops/Squadron.lua @@ -0,0 +1,792 @@ +--- **Ops** - Airwing Squadron. +-- +-- **Main Features:** +-- +-- * Set parameters like livery, skill valid for all squadron members. +-- * Define modex and callsigns. +-- * Define mission types, this squadron can perform (see Ops.Auftrag#AUFTRAG). +-- * Pause/unpause squadron operations. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Ops.Squadron +-- @image OPS_Squadron.png + + +--- SQUADRON class. +-- @type SQUADRON +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. Messages to all about status. +-- @field #string lid Class id string for output to DCS log file. +-- @field #string name Name of the squadron. +-- @field #string templatename Name of the template group. +-- @field #string aircrafttype Type of the airframe the squadron is using. +-- @field Wrapper.Group#GROUP templategroup Template group. +-- @field #table assets Squadron assets. +-- @field #table missiontypes Capabilities (mission types and performances) of the squadron. +-- @field #string livery Livery of the squadron. +-- @field #number skill Skill of squadron members. +-- @field #number modex Modex. +-- @field #number modexcounter Counter to incease modex number for assets. +-- @field #string callsignName Callsign name. +-- @field #number callsigncounter Counter to increase callsign names for new assets. +-- @field Ops.AirWing#AIRWING airwing The AIRWING object the squadron belongs to. +-- @field #number Ngroups Number of asset flight groups this squadron has. +-- @field #number engageRange Engagement range in meters. +-- @field #string attribute Generalized attribute of the squadron template group. +-- @field #number tankerSystem For tanker squads, the refuel system used (boom=0 or probpe=1). Default nil. +-- @field #number refuelSystem For refuelable squads, the refuel system used (boom=0 or probpe=1). Default nil. +-- @field #number TACANmin TACAN min channel. +-- @field #number TACANmax TACAN max channel. +-- @field #table TACANused Table of used TACAN channels. +-- @field #number radioFreq Radio frequency in MHz the squad uses. +-- @field #number radioModu Radio modulation the squad uses. +-- @extends Core.Fsm#FSM + +--- *It is unbelievable what a squadron of twelve aircraft did to tip the balance.* -- Adolf Galland +-- +-- === +-- +-- ![Banner Image](..\Presentations\Squadron\SQUADRON_Main.jpg) +-- +-- # The SQUADRON Concept +-- +-- A SQUADRON is essential part of an AIRWING and consists of **one** type of aircraft. +-- +-- +-- +-- @field #SQUADRON +SQUADRON = { + ClassName = "SQUADRON", + Debug = nil, + lid = nil, + name = nil, + templatename = nil, + aircrafttype = nil, + assets = {}, + missiontypes = {}, + livery = nil, + skill = nil, + modex = nil, + modexcounter = 0, + callsignName = nil, + callsigncounter= 11, + airwing = nil, + Ngroups = nil, + engageRange = nil, + tankerSystem = nil, + refuelSystem = nil, + TACANmin = nil, + TACANmax = nil, + TACANused = {}, +} + +--- SQUADRON class version. +-- @field #string version +SQUADRON.version="0.0.7" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- DONE: Engage radius. +-- DONE: Modex. +-- DONE: Call signs. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new SQUADRON object and start the FSM. +-- @param #SQUADRON self +-- @param #string TemplateGroupName Name of the template group. +-- @param #number Ngroups Number of asset groups of this squadron. Default 3. +-- @param #string SquadronName Name of the squadron, e.g. "VFA-37". +-- @return #SQUADRON self +function SQUADRON:New(TemplateGroupName, Ngroups, SquadronName) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #SQUADRON + + -- Name of the template group. + self.templatename=TemplateGroupName + + -- Squadron name. + self.name=tostring(SquadronName or TemplateGroupName) + + -- Set some string id for output to DCS.log file. + self.lid=string.format("SQUADRON %s | ", self.name) + + -- Template group. + self.templategroup=GROUP:FindByName(self.templatename) + + -- Check if template group exists. + if not self.templategroup then + self:E(self.lid..string.format("ERROR: Template group %s does not exist!", tostring(self.templatename))) + return nil + end + + -- Defaults. + self.Ngroups=Ngroups or 3 + self:SetEngagementRange() + + -- Everyone can ORBIT. + self:AddMissonCapability(AUFTRAG.Type.ORBIT) + + self.attribute=self.templategroup:GetAttribute() + + self.aircrafttype=self.templategroup:GetTypeName() + + self.refuelSystem=select(2, self.templategroup:GetUnit(1):IsRefuelable()) + self.tankerSystem=select(2, self.templategroup:GetUnit(1):IsTanker()) + + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "OnDuty") -- Start FSM. + self:AddTransition("*", "Status", "*") -- Status update. + self:AddTransition("OnDuty", "Pause", "Paused") -- Pause squadron. + self:AddTransition("Paused", "Unpause", "OnDuty") -- Unpause squadron. + self:AddTransition("*", "Stop", "Stopped") -- Stop squadron. + + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the SQUADRON. Initializes parameters and starts event handlers. + -- @function [parent=#SQUADRON] Start + -- @param #SQUADRON self + + --- Triggers the FSM event "Start" after a delay. Starts the SQUADRON. Initializes parameters and starts event handlers. + -- @function [parent=#SQUADRON] __Start + -- @param #SQUADRON self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the SQUADRON and all its event handlers. + -- @param #SQUADRON self + + --- Triggers the FSM event "Stop" after a delay. Stops the SQUADRON and all its event handlers. + -- @function [parent=#SQUADRON] __Stop + -- @param #SQUADRON self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Status". + -- @function [parent=#SQUADRON] Status + -- @param #SQUADRON self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#SQUADRON] __Status + -- @param #SQUADRON 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 + self.Debug=true + + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set livery painted on all squadron aircraft. +-- Note that the livery name in general is different from the name shown in the mission editor. +-- +-- Valid names are the names of the **livery directories**. Check out the folder in your DCS installation for: +-- +-- * Full modules: `DCS World OpenBeta\CoreMods\aircraft\\Liveries\\` +-- * AI units: `DCS World OpenBeta\Bazar\Liveries\\` +-- +-- The folder name `` is the string you want. +-- +-- Or personal liveries you have installed somewhere in your saved games folder. +-- +-- @param #SQUADRON self +-- @param #string LiveryName Name of the livery. +-- @return #SQUADRON self +function SQUADRON:SetLivery(LiveryName) + self.livery=LiveryName + return self +end + +--- Set skill level of all squadron team members. +-- @param #SQUADRON self +-- @param #string Skill Skill of all flights. +-- @usage mysquadron:SetSkill(AI.Skill.EXCELLENT) +-- @return #SQUADRON self +function SQUADRON:SetSkill(Skill) + self.skill=Skill + return self +end + +--- Set radio frequency and modulation the squad uses. +-- @param #SQUADRON self +-- @param #number Frequency Radio frequency in MHz. Default 251 MHz. +-- @param #number Modulation Radio modulation. Default 0=AM. +-- @usage mysquadron:SetSkill(AI.Skill.EXCELLENT) +-- @return #SQUADRON self +function SQUADRON:SetRadio(Frequency, Modulation) + self.radioFreq=Frequency or 251 + self.radioModu=Modulation or radio.modulation.AM + return self +end + +--- Set mission types this squadron is able to perform. +-- @param #SQUADRON self +-- @param #table MissionTypes Table of mission types. Can also be passed as a #string if only one type. +-- @param #number Performance Performance describing how good this mission can be performed. Higher is better. Default 50. Max 100. +-- @return #SQUADRON self +function SQUADRON:AddMissonCapability(MissionTypes, Performance) + + -- Ensure Missiontypes is a table. + if MissionTypes and type(MissionTypes)~="table" then + MissionTypes={MissionTypes} + end + + -- Set table. + self.missiontypes=self.missiontypes or {} + + for _,missiontype in pairs(MissionTypes) do + + -- Check not to add the same twice. + if self:CheckMissionCapability(missiontype, self.missiontypes) then + self:E(self.lid.."WARNING: Mission capability already present! No need to add it twice.") + -- TODO: update performance. + else + + local capability={} --Ops.Auftrag#AUFTRAG.Capability + capability.MissionType=missiontype + capability.Performance=Performance or 50 + table.insert(self.missiontypes, capability) + + end + end + + -- Debug info. + self:I(self.missiontypes) + + return self +end + +--- Get mission types this squadron is able to perform. +-- @param #SQUADRON self +-- @return #table Table of mission types. Could be empty {}. +function SQUADRON:GetMissionTypes() + + local missiontypes={} + + for _,Capability in pairs(self.missiontypes) do + local capability=Capability --Ops.Auftrag#AUFTRAG.Capability + table.insert(missiontypes, capability.MissionType) + end + + return missiontypes +end + +--- Get mission capabilities of this squadron. +-- @param #SQUADRON self +-- @return #table Table of mission capabilities. +function SQUADRON:GetMissionCapabilities() + return self.missiontypes +end + +--- Get mission performance for a given type of misson. +-- @param #SQUADRON self +-- @param #string MissionType Type of mission. +-- @return #number Performance or -1. +function SQUADRON:GetMissionPeformance(MissionType) + + for _,Capability in pairs(self.missiontypes) do + local capability=Capability --Ops.Auftrag#AUFTRAG.Capability + if capability.MissionType==MissionType then + return capability.Performance + end + end + + return -1 +end + +--- Set max engagement range. +-- @param #SQUADRON self +-- @param #number EngageRange Engagement range in NM. Default 80 NM. +-- @return #SQUADRON self +function SQUADRON:SetEngagementRange(EngageRange) + self.engageRange=UTILS.NMToMeters(EngageRange or 80) + return self +end + +--- Set call sign. +-- @param #SQUADRON self +-- @param #number Callsign Callsign from CALLSIGN.Aircraft, e.g. "Chevy" for CALLSIGN.Aircraft.CHEVY. +-- @param #number Index Callsign index, Chevy-**1**. +-- @return #SQUADRON self +function SQUADRON:SetCallsign(Callsign, Index) + self.callsignName=Callsign + self.callsignIndex=Index + return self +end + +--- Set modex. +-- @param #SQUADRON self +-- @param #number Modex A number like 100. +-- @param #string Prefix A prefix string, which is put before the `Modex` number. +-- @param #string Suffix A suffix string, which is put after the `Modex` number. +-- @return #SQUADRON self +function SQUADRON:SetModex(Modex, Prefix, Suffix) + self.modex=Modex + self.modexPrefix=Prefix + self.modexSuffix=Suffix + return self +end + +--- Set airwing. +-- @param #SQUADRON self +-- @param Ops.AirWing#AIRWING Airwing The airwing. +-- @return #SQUADRON self +function SQUADRON:SetAirwing(Airwing) + self.airwing=Airwing + return self +end + + +--- Add airwing asset to squadron. +-- @param #SQUADRON self +-- @param Ops.AirWing#AIRWING.SquadronAsset Asset The airwing asset. +-- @return #SQUADRON self +function SQUADRON:AddAsset(Asset) + self:T(self.lid..string.format("Adding asset %s of type %s", Asset.spawngroupname, Asset.unittype)) + Asset.squadname=self.name + table.insert(self.assets, Asset) + return self +end + +--- Remove airwing asset from squadron. +-- @param #SQUADRON self +-- @param Ops.AirWing#AIRWING.SquadronAsset Asset The airwing asset. +-- @return #SQUADRON self +function SQUADRON:DelAsset(Asset) + for i,_asset in pairs(self.assets) do + local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset + if Asset.uid==asset.uid then + self:T2(self.lid..string.format("Removing asset %s", asset.spawngroupname)) + table.remove(self.assets, i) + break + end + end + return self +end + +--- Get radio frequency and modulation. +-- @param #SQUADRON self +-- @return #number Radio frequency in MHz. +-- @return #number Radio Modulation (0=AM, 1=FM). +function SQUADRON:GetRadio() + return self.radioFreq, self.radioModu +end + +--- Create a callsign for the asset. +-- @param #SQUADRON self +-- @param Ops.AirWing#AIRWING.SquadronAsset Asset The airwing asset. +-- @return #SQUADRON self +function SQUADRON:GetCallsign(Asset) + + if self.callsignName then + + Asset.callsign={} + + for i=1,Asset.nunits do + + local callsign={} + callsign[1]=self.callsignName + callsign[2]=math.floor(self.callsigncounter / 10) + callsign[3]=self.callsigncounter % 10 + if callsign[3]==0 then + callsign[3]=1 + self.callsigncounter=self.callsigncounter+2 + else + self.callsigncounter=self.callsigncounter+1 + end + + Asset.callsign[i]=callsign + + self:T3({callsign=callsign}) + + --TODO: there is also a table entry .name, which is a string. + end + + + end + +end + +--- Create a modex for the asset. +-- @param #SQUADRON self +-- @param Ops.AirWing#AIRWING.SquadronAsset Asset The airwing asset. +-- @return #SQUADRON self +function SQUADRON:GetModex(Asset) + + if self.modex then + + Asset.modex={} + + for i=1,Asset.nunits do + + Asset.modex[i]=string.format("%03d", self.modex+self.modexcounter) + + self.modexcounter=self.modexcounter+1 + + self:T3({modex=Asset.modex[i]}) + + end + + end + +end + +--- Get an unused TACAN channel. +-- @param #SQUADRON self +-- @param Ops.AirWing#AIRWING.SquadronAsset Asset The airwing asset. +-- @return #number TACAN channel or *nil* if no channel is free. +function SQUADRON:GetTACAN() + + if self.TACANmin and self.TACANmax then + + for channel=self.TACANmin, self.TACANmax do + + if not self.TACANused[channel] then + self.TACANused[channel]=true + return channel + end + + end + + end + + return nil +end + +--- "Return" a used TACAN channel. +-- @param #SQUADRON self +-- @param #number channel The channel that is available again. +function SQUADRON:ReturnTACAN(channel) + self.TACANused[channel]=false +end + +--- Check if squadron is "OnDuty". +-- @param #SQUADRON self +-- @return #boolean If true, squdron is in state "OnDuty". +function SQUADRON:IsOnDuty() + return self:Is("OnDuty") +end + +--- Check if squadron is "Stopped". +-- @param #SQUADRON self +-- @return #boolean If true, squdron is in state "Stopped". +function SQUADRON:IsStopped() + return self:Is("Stopped") +end + +--- Check if squadron is "Paused". +-- @param #SQUADRON self +-- @return #boolean If true, squdron is in state "Paused". +function SQUADRON:IsPaused() + return self:Is("Paused") +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Start & Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the FLIGHTGROUP FSM and event handlers. +-- @param #SQUADRON self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SQUADRON:onafterStart(From, Event, To) + + -- Short info. + local text=string.format("Starting SQUADRON", self.name) + self:I(self.lid..text) + + -- Start the status monitoring. + self:__Status(-1) +end + +--- On after "Status" event. +-- @param #SQUADRON self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SQUADRON:onafterStatus(From, Event, To) + + -- FSM state. + local fsmstate=self:GetState() + + -- Check if group has detected any units. + --self:_CheckAssetStatus() + + -- Short info. + local text=string.format("Status %s: Assets %d", fsmstate, #self.assets) + self:I(self.lid..text) + + if not self:IsStopped() then + self:__Status(-30) + end +end + + +--- Check asset status. +-- @param #SQUADRON self +function SQUADRON:_CheckAssetStatus() + + for _,_asset in pairs(self.assets) do + local asset=_asset + + end + +end + +--- On after "Stop" event. +-- @param #SQUADRON self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SQUADRON:onafterStop(From, Event, To) + + self:I(self.lid.."STOPPING Squadron!") + + -- Remove all assets. + for i=#self.assets,1,-1 do + local asset=self.assets[i] + self:DelAsset(asset) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check if there is a squadron that can execute a given mission. +-- We check the mission type, the refuelling system, engagement range +-- @param #SQUADRON self +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @return #boolean If true, Squadron can do that type of mission. +function SQUADRON:CanMission(Mission) + + local cando=true + + -- On duty?= + if not self:IsOnDuty() then + self:I(self.lid..string.format("Squad in not OnDuty but in state %s. Cannot do mission %s with target %s", self:GetState(), Mission.name, Mission:GetTargetName())) + return false + end + + -- Check mission type. WARNING: This assumes that all assets of the squad can do the same mission types! + if not self:CheckMissionType(Mission.type, self:GetMissionTypes()) then + self:I(self.lid..string.format("INFO: Squad cannot do mission type %s (%s, %s)", Mission.type, Mission.name, Mission:GetTargetName())) + return false + end + + -- Check that tanker mission + if Mission.type==AUFTRAG.Type.TANKER then + + if Mission.refuelSystem and Mission.refuelSystem==self.tankerSystem then + -- Correct refueling system. + else + self:I(self.lid..string.format("INFO: Wrong refueling system requested=%s != %s=available", tostring(Mission.refuelSystem), tostring(self.tankerSystem))) + return false + end + + end + + -- Distance to target. + local TargetDistance=Mission:GetTargetDistance(self.airwing:GetCoordinate()) + + -- Max engage range. + local engagerange=Mission.engageRange and math.max(self.engageRange, Mission.engageRange) or self.engageRange + + -- Set range is valid. Mission engage distance can overrule the squad engage range. + if TargetDistance>engagerange then + self:I(self.lid..string.format("INFO: Squad is not in range. Target dist=%d > %d NM max engage Range", UTILS.MetersToNM(TargetDistance), UTILS.MetersToNM(engagerange))) + return false + end + + return true +end + +--- Get assets for a mission. +-- @param #SQUADRON self +-- @return #number Assets not spawned. +function SQUADRON:CountAssetsInStock() + + local N=0 + for _,_asset in pairs(self.assets) do + local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset + if asset.spawned then + + else + N=N+1 + end + end + + return N +end + +--- Get assets for a mission. +-- @param #SQUADRON self +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @return #table Assets that can do the required mission. +function SQUADRON:RecruitAssets(Mission) + + -- Number of payloads available. + local Npayloads=self.airwing:CountPayloadsInStock(Mission.type, self.aircrafttype) + + local assets={} + + -- Loop over assets. + for _,_asset in pairs(self.assets) do + local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset + + + -- Check if asset is currently on a mission (STARTED or QUEUED). + if self.airwing:IsAssetOnMission(asset) then + + --- + -- Asset is already on a mission. + --- + + -- Check if this asset is currently on a PATROL mission (STARTED or EXECUTING). + if self.airwing:IsAssetOnMission(asset, AUFTRAG.Type.PATROL) and Mission.type==AUFTRAG.Type.INTERCEPT then + + -- Check if the payload of this asset is compatible with the mission. + -- Note: we do not check the payload as an asset that is on a PATROL mission should be able to do an INTERCEPT as well! + self:I(self.lid.."Adding asset on PATROL mission for an INTERCEPT mission") + table.insert(assets, asset) + + end + + else + + --- + -- Asset as no current mission + --- + + if asset.spawned then + + --- + -- Asset is already SPAWNED (could be uncontrolled on the airfield or inbound after another mission) + --- + + local flightgroup=asset.flightgroup + + -- Firstly, check if it has the right payload. + if self:CheckMissionCapability(Mission.type, asset.payload.capabilities) and flightgroup and flightgroup:IsAlive() then + + -- Assume we are ready and check if any condition tells us we are not. + local combatready=true + + if Mission.type==AUFTRAG.Type.INTERCEPT then + combatready=flightgroup:CanAirToAir() + else + combatready=flightgroup:CanAirToGround() + end + + -- No more attacks if fuel is already low. Safety first! + if flightgroup:IsFuelLow() then + combatready=false + end + + -- Check if in a state where we really do not want to fight any more. + if flightgroup:IsLanding() or flightgroup:IsLanded() or flightgroup:IsArrived() or flightgroup:IsDead() then + combatready=false + end + + -- This asset is "combatready". + if combatready then + self:I(self.lid.."Adding SPAWNED asset to ANOTHER mission as it is COMBATREADY") + table.insert(assets, asset) + end + + end + + else + + --- + -- Asset is still in STOCK + --- + + -- Check that asset is not already requested for another mission. + if Npayloads>0 and not asset.requested then + + -- Add this asset to the selection. + table.insert(assets, asset) + + -- Reduce number of payloads so we only return the number of assets that could do the job. + Npayloads=Npayloads-1 + + end + + end + end + end -- loop over assets + + return assets +end + + +--- Checks if a mission type is contained in a table of possible types. +-- @param #SQUADRON self +-- @param #string MissionType The requested mission type. +-- @param #table PossibleTypes A table with possible mission types. +-- @return #boolean If true, the requested mission type is part of the possible mission types. +function SQUADRON:CheckMissionType(MissionType, PossibleTypes) + + if type(PossibleTypes)=="string" then + PossibleTypes={PossibleTypes} + end + + for _,canmission in pairs(PossibleTypes) do + if canmission==MissionType then + return true + end + end + + return false +end + +--- Check if a mission type is contained in a list of possible capabilities. +-- @param #SQUADRON self +-- @param #string MissionType The requested mission type. +-- @param #table Capabilities A table with possible capabilities. +-- @return #boolean If true, the requested mission type is part of the possible mission types. +function SQUADRON:CheckMissionCapability(MissionType, Capabilities) + + for _,cap in pairs(Capabilities) do + local capability=cap --Ops.Auftrag#AUFTRAG.Capability + if capability.MissionType==MissionType then + return true + end + end + + return false +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + diff --git a/Moose Development/Moose/Wrapper/Marker.lua b/Moose Development/Moose/Wrapper/Marker.lua new file mode 100644 index 000000000..3cc74baec --- /dev/null +++ b/Moose Development/Moose/Wrapper/Marker.lua @@ -0,0 +1,776 @@ +--- **Wrapper** - Markers On the F10 map. +-- +-- **Main Features:** +-- +-- * Convenient handling of markers via multiple user API functions. +-- * Update text and position of marker easily via scripting. +-- * Delay creation and removal of markers via (optional) parameters. +-- * Retrieve data such as text and coordinate. +-- * Marker specific FSM events when a marker is added, removed or changed. +-- * Additional FSM events when marker text or position is changed. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Wrapper.Marker +-- @image Wrapper_Marker.png + + +--- Marker class. +-- @type MARKER +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. Messages to all about status. +-- @field #string lid Class id string for output to DCS log file. +-- @field #number mid Marker ID. +-- @field Core.Point#COORDINATE coordinate Coordinate of the mark. +-- @field #string text Text displayed in the mark panel. +-- @field #string message Message dispayed when the mark is added. +-- @field #boolean readonly Marker is read-only. +-- @field #number coalition Coalition to which the marker is displayed. +-- @extends Core.Fsm#FSM + +--- Just because... +-- +-- === +-- +-- ![Banner Image](..\Presentations\MARKER\Marker_Main.jpg) +-- +-- # The MARKER Class Idea +-- +-- The MARKER class simplifies creating, updating and removing of markers on the F10 map. +-- +-- # Create a Marker +-- +-- -- Create a MARKER object at Batumi with a trivial text. +-- local Coordinate=AIRBASE:FindByName("Batumi"):GetCoordinate() +-- mymarker=MARKER:New(Coordinate, "I am Batumi Airfield") +-- +-- Now this does **not** show the marker yet. We still need to specifiy to whom it is shown. There are several options, i.e. +-- show the marker to everyone, to a speficic coaliton only, or only to a specific group. +-- +-- ## For Everyone +-- +-- If the marker should be visible to everyone, you can use the :ToAll() function. +-- +-- mymarker=MARKER:New(Coordinate, "I am Batumi Airfield"):ToAll() +-- +-- ## For a Coaliton +-- +-- If the maker should be visible to a specific coalition, you can use the :ToCoalition() function. +-- +-- mymarker=MARKER:New(Coordinate, "I am Batumi Airfield"):ToCoaliton(coaliton.side.BLUE) +-- +-- ### To Blue Coaliton +-- +-- ### To Red Coalition +-- +-- This would show the marker only to the Blue coaliton. +-- +-- ## For a Group +-- +-- +-- # Removing a Marker +-- +-- +-- # Updating a Marker +-- +-- The marker text and coordinate can be updated easily as shown below. +-- +-- However, note that **updateing involves to remove and recreate the marker if either text or its coordinate is changed**. +-- *This is a DCS scripting engine limitation.* +-- +-- ## Update Text +-- +-- If you created a marker "mymarker" as shown above, you can update the dispayed test by +-- +-- mymarker:UpdateText("I am the new text at Batumi") +-- +-- The update can also be delayed by, e.g. 90 seconds, using +-- +-- mymarker:UpdateText("I am the new text at Batumi", 90) +-- +-- ## Update Coordinate +-- +-- If you created a marker "mymarker" as shown above, you can update its coordinate on the F10 map by +-- +-- mymarker:UpdateCoordinate(NewCoordinate) +-- +-- The update can also be delayed by, e.g. 60 seconds, using +-- +-- mymarker:UpdateCoordinate(NewCoordinate, 60) +-- +-- # Retrieve Data +-- +-- The important data as the displayed text and the coordinate of the marker can be retrieved easily. +-- +-- ## Text +-- +-- local text=mymarker:GetText() +-- env.info("Marker Text = " .. text) +-- +-- ## Coordinate +-- +-- local Coordinate=mymarker:GetCoordinate() +-- env.info("Marker Coordinate LL DSM = " .. Coordinate:ToStringLLDMS()) +-- +-- +-- # FSM Events +-- +-- Moose creates addditonal events, so called FSM event, when markers are added, changed, removed, and text or the coordianteis updated. +-- +-- These events can be captured and used for processing via OnAfter functions as shown below. +-- +-- ## Added +-- +-- ## Changed +-- +-- ## Removed +-- +-- ## TextUpdate +-- +-- ## CoordUpdate +-- +-- +-- # Examples +-- +-- +-- @field #MARKER +MARKER = { + ClassName = "MARKER", + Debug = false, + lid = nil, + mid = nil, + coordinate = nil, + text = nil, + message = nil, + readonly = nil, + coalition = nil, +} + +--- Marker ID. Running number. +_MARKERID=0 + +--- Marker class version. +-- @field #string version +MARKER.version="0.1.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: User "Get" functions. E.g., :GetCoordinate() +-- DONE: Add delay to user functions. +-- DONE: Handle events. +-- DONE: Create FSM events. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new MARKER class object. +-- @param #MARKER self +-- @param Core.Point#COORDINATE Coordinate Coordinate where to place the marker. +-- @param #string Text Text displayed on the mark panel. +-- @return #MARKER self +function MARKER:New(Coordinate, Text) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #MARKER + + self.coordinate=Coordinate + + self.text=Text + + -- Defaults + self.readonly=false + self.message="" + + -- New marker ID. This is not the one of the actual marker. + _MARKERID=_MARKERID+1 + + self.myid=_MARKERID + + -- Log ID. + self.lid=string.format("Marker #%d | ", self.myid) + + -- Start State. + self:SetStartState("Invisible") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Invisible", "Added", "Visible") -- Marker was added. + self:AddTransition("Visible", "Removed", "Invisible") -- Marker was removed. + self:AddTransition("*", "Changed", "*") -- Marker was changed. + + self:AddTransition("*", "TextUpdate", "*") -- Text updated. + self:AddTransition("*", "CoordUpdate", "*") -- Coordinates updated. + + --- Triggers the FSM event "Added". + -- @function [parent=#MARKER] Added + -- @param #MARKER self + -- @param Core.Event#EVENTDATA EventData Event data table. + + --- Triggers the delayed FSM event "Added". + -- @function [parent=#MARKER] __Added + -- @param #MARKER self + -- @param Core.Event#EVENTDATA EventData Event data table. + + --- On after "Added" event user function. + -- @function [parent=#MARKER] OnAfterAdded + -- @param #MARKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Event#EVENTDATA EventData Event data table. + + + --- Triggers the FSM event "Removed". + -- @function [parent=#MARKER] Removed + -- @param #MARKER self + -- @param Core.Event#EVENTDATA EventData Event data table. + + --- Triggers the delayed FSM event "Removed". + -- @function [parent=#MARKER] __Removed + -- @param #MARKER self + -- @param Core.Event#EVENTDATA EventData Event data table. + + --- On after "Removed" event user function. + -- @function [parent=#MARKER] OnAfterRemoved + -- @param #MARKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Event#EVENTDATA EventData Event data table. + + + --- Triggers the FSM event "Changed". + -- @function [parent=#MARKER] Changed + -- @param #MARKER self + -- @param Core.Event#EVENTDATA EventData Event data table. + + --- Triggers the delayed FSM event "Changed". + -- @function [parent=#MARKER] __Changed + -- @param #MARKER self + -- @param Core.Event#EVENTDATA EventData Event data table. + + --- On after "Changed" event user function. + -- @function [parent=#MARKER] OnAfterChanged + -- @param #MARKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Event#EVENTDATA EventData Event data table. + + + --- Triggers the FSM event "TextUpdate". + -- @function [parent=#MARKER] TextUpdate + -- @param #MARKER self + -- @param #string Text The new text. + + --- Triggers the delayed FSM event "TextUpdate". + -- @function [parent=#MARKER] __TextUpdate + -- @param #MARKER self + -- @param #string Text The new text. + + --- On after "TextUpdate" event user function. + -- @function [parent=#MARKER] OnAfterTextUpdate + -- @param #MARKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string Text The new text. + + + --- Triggers the FSM event "CoordUpdate". + -- @function [parent=#MARKER] CoordUpdate + -- @param #MARKER self + -- @param Core.Point#COORDINATE Coordinate The new Coordinate. + + --- Triggers the delayed FSM event "CoordUpdate". + -- @function [parent=#MARKER] __CoordUpdate + -- @param #MARKER self + -- @param Core.Point#COORDINATE Coordinate The updated Coordinate. + + --- On after "CoordUpdate" event user function. + -- @function [parent=#MARKER] OnAfterCoordUpdate + -- @param #MARKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Point#COORDINATE Coordinate The updated Coordinate. + + + -- Handle events. + self:HandleEvent(EVENTS.MarkAdded) + self:HandleEvent(EVENTS.MarkRemoved) + self:HandleEvent(EVENTS.MarkChange) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User API Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Marker is readonly. Text cannot be changed and marker cannot be removed. +-- @param #MARKER self +-- @return #MARKER self +function MARKER:ReadOnly() + + self.readonly=true + + return self +end + +--- Set message that is displayed on screen if the marker is added. +-- @param #MARKER self +-- @param #string Text Message displayed when the marker is added. +-- @return #MARKER self +function MARKER:Message(Text) + + self.message=Text or "" + + return self +end + +--- Place marker visible for everyone. +-- @param #MARKER self +-- @param #number Delay (Optional) Delay in seconds, before the marker is created. +-- @return #MARKER self +function MARKER:ToAll(Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, MARKER.ToAll, self) + else + + self.toall=true + self.tocoaliton=nil + self.coalition=nil + self.togroup=nil + self.groupname=nil + self.groupid=nil + + -- First remove an existing mark. + if self.shown then + self:Remove() + end + + self.mid=UTILS.GetMarkID() + + -- Call DCS function. + trigger.action.markToAll(self.mid, self.text, self.coordinate:GetVec3(), self.readonly, self.message) + + end + + return self +end + +--- Place marker visible for a specific coalition only. +-- @param #MARKER self +-- @param #number Coalition Coalition 1=Red, 2=Blue, 0=Neutral. See `coaliton.side.RED`. +-- @param #number Delay (Optional) Delay in seconds, before the marker is created. +-- @return #MARKER self +function MARKER:ToCoalition(Coalition, Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, MARKER.ToCoalition, self, Coalition) + else + + self.coalition=Coalition + + self.tocoaliton=true + self.toall=false + self.togroup=false + self.groupname=nil + self.groupid=nil + + -- First remove an existing mark. + if self.shown then + self:Remove() + end + + self.mid=UTILS.GetMarkID() + + -- Call DCS function. + trigger.action.markToCoalition(self.mid, self.text, self.coordinate:GetVec3(), self.coalition, self.readonly, self.message) + + end + + return self +end + +--- Place marker visible for the blue coalition only. +-- @param #MARKER self +-- @param #number Delay (Optional) Delay in seconds, before the marker is created. +-- @return #MARKER self +function MARKER:ToBlue(Delay) + self:ToCoalition(coalition.side.BLUE, Delay) + return self +end + +--- Place marker visible for the blue coalition only. +-- @param #MARKER self +-- @param #number Delay (Optional) Delay in seconds, before the marker is created. +-- @return #MARKER self +function MARKER:ToRed(Delay) + self:ToCoalition(coalition.side.RED, Delay) + return self +end + +--- Place marker visible for the neutral coalition only. +-- @param #MARKER self +-- @param #number Delay (Optional) Delay in seconds, before the marker is created. +-- @return #MARKER self +function MARKER:ToNeutral(Delay) + self:ToCoalition(coalition.side.NEUTRAL, Delay) + return self +end + + +--- Place marker visible for a specific group only. +-- @param #MARKER self +-- @param Wrapper.Group#GROUP Group The group to which the marker is displayed. +-- @param #number Delay (Optional) Delay in seconds, before the marker is created. +-- @return #MARKER self +function MARKER:ToGroup(Group, Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, MARKER.ToGroup, self, Group) + else + + -- Check if group exists. + if Group and Group:IsAlive()~=nil then + + self.groupid=Group:GetID() + + if self.groupid then + + self.groupname=Group:GetName() + + self.togroup=true + self.tocoaliton=nil + self.coalition=nil + self.toall=nil + + -- First remove an existing mark. + if self.shown then + self:Remove() + end + + self.mid=UTILS.GetMarkID() + + -- Call DCS function. + trigger.action.markToGroup(self.mid, self.text, self.coordinate:GetVec3(), self.groupid, self.readonly, self.message) + + end + + else + --TODO: Warning! + end + + end + + return self +end + +--- Update the text displayed on the mark panel. +-- @param #MARKER self +-- @param #string Text Updated text. +-- @param #number Delay (Optional) Delay in seconds, before the marker is created. +-- @return #MARKER self +function MARKER:UpdateText(Text, Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, MARKER.UpdateText, self, Text) + else + + self.text=tostring(Text) + + self:Refresh() + + self:TextUpdate(tostring(Text)) + + end + + return self +end + +--- Update the coordinate where the marker is displayed. +-- @param #MARKER self +-- @param Core.Point#COORDINATE Coordinate The new coordinate. +-- @param #number Delay (Optional) Delay in seconds, before the marker is created. +-- @return #MARKER self +function MARKER:UpdateCoordinate(Coordinate, Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, MARKER.UpdateCoordinate, self, Coordinate) + else + + self.coordinate=Coordinate + + self:Refresh() + + self:CoordUpdate(Coordinate) + + end + + return self +end + +--- Refresh the marker. +-- @param #MARKER self +-- @param #number Delay (Optional) Delay in seconds, before the marker is created. +-- @return #MARKER self +function MARKER:Refresh(Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, MARKER.Refresh, self) + else + + if self.toall then + + self:ToAll() + + elseif self.tocoaliton then + + self:ToCoalition(self.coalition) + + elseif self.togroup then + + local group=GROUP:FindByName(self.groupname) + + self:ToGroup(group) + + else + self:E(self.lid.."ERROR: unknown To in :Refresh()!") + end + + end + + return self +end + +--- Remove a marker. +-- @param #MARKER self +-- @param #number Delay (Optional) Delay in seconds, before the marker is removed. +-- @return #MARKER self +function MARKER:Remove(Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, MARKER.Remove, self) + else + + if self.shown then + + -- Call DCS function. + trigger.action.removeMark(self.mid) + + end + + end + + return self +end + +--- Get position of the marker. +-- @param #MARKER self +-- @return Core.Point#COORDINATE The coordinate of the marker. +function MARKER:GetCoordinate() + return self.coordinate +end + +--- Get text that is displayed in the marker panel. +-- @param #MARKER self +-- @return #string Marker text. +function MARKER:GetText() + return self.text +end + +--- Set text that is displayed in the marker panel. Note this does not show the marker. +-- @param #MARKER self +-- @param #string Text Marker text. Default is an empty sting "". +-- @return #MARKER self +function MARKER:SetText(Text) + self.text=Text and tostring(Text) or "" + return self +end + + +--- Check if marker is currently visible on the F10 map. +-- @param #MARKER self +-- @return #boolean True if the marker is currently visible. +function MARKER:IsVisible() + return self:Is("Visible") +end + +--- Check if marker is currently invisible on the F10 map. +-- @param #MARKER self +-- @return +function MARKER:IsInvisible() + return self:Is("Invisible") +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Event Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Event function when a MARKER is added. +-- @param #MARKER self +-- @param Core.Event#EVENTDATA EventData +function MARKER:OnEventMarkAdded(EventData) + + if EventData and EventData.MarkID then + + local MarkID=EventData.MarkID + + self:T3(self.lid..string.format("Captured event MarkAdded for Mark ID=%s", tostring(MarkID))) + + if MarkID==self.mid then + + self.shown=true + + self:Added(EventData) + + end + + end + +end + +--- Event function when a MARKER is removed. +-- @param #MARKER self +-- @param Core.Event#EVENTDATA EventData +function MARKER:OnEventMarkRemoved(EventData) + + if EventData and EventData.MarkID then + + local MarkID=EventData.MarkID + + self:T3(self.lid..string.format("Captured event MarkAdded for Mark ID=%s", tostring(MarkID))) + + if MarkID==self.mid then + + self.shown=false + + self:Removed(EventData) + + end + + end + +end + +--- Event function when a MARKER changed. +-- @param #MARKER self +-- @param Core.Event#EVENTDATA EventData +function MARKER:OnEventMarkChange(EventData) + + if EventData and EventData.MarkID then + + local MarkID=EventData.MarkID + + self:T3(self.lid..string.format("Captured event MarkChange for Mark ID=%s", tostring(MarkID))) + + if MarkID==self.mid then + + self:Changed(EventData) + + self:TextChanged(tostring(EventData.MarkText)) + + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Event Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "Added" event. +-- @param #MARKER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Event#EVENTDATA EventData Event data table. +function MARKER:onafterAdded(From, Event, To, EventData) + + -- Debug info. + local text=string.format("Captured event MarkAdded for myself:\n") + text=text..string.format("Marker ID = %s\n", tostring(EventData.MarkID)) + text=text..string.format("Coalition = %s\n", tostring(EventData.MarkCoalition)) + text=text..string.format("Group ID = %s\n", tostring(EventData.MarkGroupID)) + text=text..string.format("Initiator = %s\n", EventData.IniUnit and EventData.IniUnit:GetName() or "Nobody") + text=text..string.format("Coordinate = %s\n", EventData.MarkCoordinate and EventData.MarkCoordinate:ToStringLLDMS() or "Nowhere") + text=text..string.format("Text: \n%s", tostring(EventData.MarkText)) + self:T2(self.lid..text) + +end + +--- On after "Removed" event. +-- @param #MARKER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Event#EVENTDATA EventData Event data table. +function MARKER:onafterRemoved(From, Event, To, EventData) + + -- Debug info. + local text=string.format("Captured event MarkRemoved for myself:\n") + text=text..string.format("Marker ID = %s\n", tostring(EventData.MarkID)) + text=text..string.format("Coalition = %s\n", tostring(EventData.MarkCoalition)) + text=text..string.format("Group ID = %s\n", tostring(EventData.MarkGroupID)) + text=text..string.format("Initiator = %s\n", EventData.IniUnit and EventData.IniUnit:GetName() or "Nobody") + text=text..string.format("Coordinate = %s\n", EventData.MarkCoordinate and EventData.MarkCoordinate:ToStringLLDMS() or "Nowhere") + text=text..string.format("Text: \n%s", tostring(EventData.MarkText)) + self:T2(self.lid..text) + +end + +--- On after "Changed" event. +-- @param #MARKER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Event#EVENTDATA EventData Event data table. +function MARKER:onafterChanged(From, Event, To, EventData) + + -- Debug info. + local text=string.format("Captured event MarkChange for myself:\n") + text=text..string.format("Marker ID = %s\n", tostring(EventData.MarkID)) + text=text..string.format("Coalition = %s\n", tostring(EventData.MarkCoalition)) + text=text..string.format("Group ID = %s\n", tostring(EventData.MarkGroupID)) + text=text..string.format("Initiator = %s\n", EventData.IniUnit and EventData.IniUnit:GetName() or "Nobody") + text=text..string.format("Coordinate = %s\n", EventData.MarkCoordinate and EventData.MarkCoordinate:ToStringLLDMS() or "Nowhere") + text=text..string.format("Text: \n%s", tostring(EventData.MarkText)) + self:T2(self.lid..text) + +end + +--- On after "TextUpdate" event. +-- @param #MARKER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string Text The updated text, displayed in the mark panel. +function MARKER:onafterTextUpdate(From, Event, To, Text) + + self:I(self.lid..string.format("New Marker Text:\n%s", Text)) + +end + +--- On after "CoordUpdate" event. +-- @param #MARKER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Coordinate The updated coordinates. +function MARKER:onafterCoordUpdate(From, Event, To, Coordinate) + + self:I(self.lid..string.format("New Marker Coordinate in LL DMS: %s", Coordinate:ToStringLLDMS())) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index 4852f9346..f74007e69 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -33,6 +33,7 @@ Wrapper/Client.lua Wrapper/Static.lua Wrapper/Airbase.lua Wrapper/Scenery.lua +Wrapper/Marker.lua Cargo/Cargo.lua Cargo/CargoUnit.lua @@ -64,6 +65,11 @@ Ops/Airboss.lua Ops/RecoveryTanker.lua Ops/RescueHelo.lua Ops/ATIS.lua +Ops/AirWing.lua +Ops/Auftrag.lua +Ops/FlightGroup.lua +Ops/NavyGroup.lua +Ops/Squadron.lua AI/AI_Balancer.lua AI/AI_Air.lua