--- **OPS** - (R2.5) - Manage recovery of aircraft at airdromes and FARPS. -- -- -- -- **Main Features:** -- -- * Manage aircraft recovery. -- -- === -- -- ### Author: **funkyfranky** -- @module OPS.FlightControl -- @image OPS_FlightControl.png --- FLIGHTCONTROL class. -- @type FLIGHTCONTROL -- @field #string ClassName Name of the class. -- @field #boolean Debug Debug mode. Messages to all about status. -- @field #string theatre The DCS map used in the mission. -- @field #string lid Class id string for output to DCS log file. -- @field #string airbasename Name of airbase. -- @field #number airbasetype Type of airbase. -- @field Wrapper.Airbase#AIRBASE airbase Airbase object. -- @field Core.Zone#ZONE zoneAirbase Zone around the airbase. -- @field #table parking Parking spots table. -- @field #table runways Runway table. -- @field #table flight All flights table. -- @field #table Qwaiting Queue of aircraft waiting for landing permission. -- @field #table Qlanding Queue of aircraft currently on final approach. -- @field #table Qtakeoff Queue of aircraft about to takeoff. -- @field #table Qparking Queue of aircraft parking. -- @field #number activerwyno Number of active runway. -- @extends Core.Fsm#FSM --- Be surprised! -- -- === -- -- ![Banner Image](..\Presentations\FLIGHTCONTROL\FlightControl_Main.jpg) -- -- # The FLIGHTCONTROL Concept -- -- -- -- @field #FLIGHTCONTROL FLIGHTCONTROL = { ClassName = "FLIGHTCONTROL", Debug = false, lid = nil, theatre = nil, airbasename = nil, airbase = nil, airbasetype = nil, zoneAirbase = nil, parking = {}, runways = {}, flights = {}, Qwaiting = {}, Qlanding = {}, Qtakeoff = {}, Qparking = {}, activerwyno = 1, } --- Holding point -- @type FLIGHTCONTROL.HoldingPoint -- @field Core.Point#COORDINATE pos0 First poosition of racetrack holding point. -- @field Core.Point#COORDINATE pos1 Second position of racetrack holding point. -- @field #number angelsmin Smallest holding altitude in angels. -- @field #number angelsmax Largest holding alitude in angels. --- Parking spot data. -- @type FLIGHTCONTROL.ParkingSpot -- @field #number index Parking index. -- @field #number id Parking id. -- @field Core.Point#COORDINATE position Coordinate of the spot. -- @field #number terminal Terminal type. -- @field #boolean free If true, spot is free. -- @field #number drunway Distance to runway. -- @field #boolean reserved If true, reserved. -- @field #number markerid ID of the marker. --- Runway data. -- @type FLIGHTCONTROL.Runway -- @field #number direction Direction of the runway. -- @field #number length Length of runway in meters. -- @field #number width Width of runway in meters. -- @field Core.Point#COORDINATE position Position of runway start. --- FlightControl class version. -- @field #string version FLIGHTCONTROL.version="0.0.5" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Add FARPS. -- TODO: Add helos. -- TODO: Task me down option. -- TODO: ATIS option. -- TODO: ATC voice overs. -- TODO: Check runways and clean up. -- TODO: Interface with FLIGHTGROUP ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Create a new FLIGHTCONTROL class object for an associated airbase. -- @param #FLIGHTCONTROL self -- @param #string airbasename Name of the airbase. -- @return #FLIGHTCONTROL self function FLIGHTCONTROL:New(airbasename) -- Inherit everything from FSM class. local self=BASE:Inherit(self, FSM:New()) -- #FLIGHTCONTROL self.airbase=AIRBASE:FindByName(airbasename) -- Name of the airbase. self.airbasename=airbasename -- Airbase category airdrome, FARP, SHIP. self.airbasetype=self.airbase:GetDesc().category -- Set some string id for output to DCS.log file. self.lid=string.format("FLIGHTCONTROL %s | ", airbasename) -- Current map. self.theatre=env.mission.theatre -- 30 NM zone around the airbase. self.zoneAirbase=ZONE_RADIUS:New("FC", self:GetCoordinate():GetVec2(), UTILS.NMToMeters(30)) -- Init runways. self:_InitRunwayData() -- Init parking spots. self:_InitParkingSpots() -- Start State. self:SetStartState("Stopped") -- Add FSM transitions. -- From State --> Event --> To State self:AddTransition("Stopped", "Start", "Running") -- Start FSM. self:AddTransition("*", "Status", "*") -- Update status. -- Debug trace. if true then self.Debug=true BASE:TraceOnOff(true) BASE:TraceClass(self.ClassName) BASE:TraceLevel(1) --self.dTstatus=0.1 end -- Add to data base. _DATABASE:AddFlightControl(self) return self end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- User API Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Set runway. This clears all auto generated runways. -- @param #FLIGHTCONTROL self -- @param #FLIGHTCONTROL.Runway Runway. -- @return #FLIGHTCONTROL self function FLIGHTCONTROL:SetRunway(runway) -- Reset table. self.runways={} -- Set runway. table.insert(self.runways, runway) return self end --- Add runway. -- @param #FLIGHTCONTROL self -- @param #FLIGHTCONTROL.Runway Runway. -- @return #FLIGHTCONTROL self function FLIGHTCONTROL:AddRunway(runway) -- Set runway. table.insert(self.runways, runway) return self end --- Set active runway number. Counting refers to the position in the table entry. -- @param #FLIGHTCONTROL self -- @param #number no Number in the runways table. -- @return #FLIGHTCONTROL self function FLIGHTCONTROL:SetActiveRunwayNumber(no) self.activerwyno=no return self end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Status ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Start FLIGHTCONTROL FSM. Handle events. -- @param #FLIGHTCONTROL self function FLIGHTCONTROL:onafterStart() -- Events are handled my MOOSE. self:I(self.lid..string.format("Starting FLIGHTCONTROL v%s for airbase %s of type %d on map %s", FLIGHTCONTROL.version, self.airbasename, self.airbasetype, self.theatre)) -- Handle events. self:HandleEvent(EVENTS.Birth) self:HandleEvent(EVENTS.EngineStartup) self:HandleEvent(EVENTS.Takeoff) self:HandleEvent(EVENTS.Land) self:HandleEvent(EVENTS.EngineShutdown) self:HandleEvent(EVENTS.Crash) -- Init status updates. self:__Status(-1) end --- Update status. -- @param #FLIGHTCONTROL self function FLIGHTCONTROL:onafterStatus() -- Check zone for flights inbound. --self:_CheckInbound() --self:_UpdateParkingSpots() -- Check parking spots. self:_CheckParking() -- Check waiting and landing queue. self:_CheckQueues() -- Get runway. local runway=self:_GetActiveRunway() -- Get free parking spots. local nfree=self:_GetFreeParkingSpots() -- Info text. local text=string.format("State %s - Active Runway %03d - Free Parking %d", self:GetState(), runway.direction, nfree) self:I(self.lid..text) -- Next status update in ~30 seconds. self:__Status(-30) end --- Initialize data of runways. -- @param #FLIGHTCONTROL self function FLIGHTCONTROL:_InitRunwayData() -- Get spawn points on runway. local runwaycoords=self.airbase:GetParkingSpotsCoordinates(AIRBASE.TerminalType.Runway) self:E(self.lid..string.format("Runway coords # = %d", #runwaycoords)) for i=1,#runwaycoords,2 do -- Assuming each runway has two points. local j=(i+1)/2 -- Coordinates of the two runway points. local c1=runwaycoords[i] --Core.Point#COORDINATES local c2=runwaycoords[i+1] --Core.Point#COORDINATES -- Debug mark c1:MarkToAll("Runway Point 1") c2:MarkToAll("Runway Point 2") -- Heading of runway. local hdg=c1:HeadingTo(c2) -- Debug info. self:T(self.lid..string.format("Runway %d heading=%03d", j, hdg)) -- Runway table. local runway={} --#FLIGHTCONTROL.Runway runway.direction=hdg runway.length=c1:Get2DDistance(c2) runway.position=c1 -- Add runway. table.insert(self.runways, runway) -- Inverse runway. local runway={} --#FLIGHTCONTROL.Runway local hdg=hdg-180 if hdg<0 then hdg=hdg+360 end runway.direction=hdg runway.length=c1:Get2DDistance(c2) runway.position=c2 -- Add inverse runway. table.insert(self.runways, runway) end end --- Get the active runway based on current wind direction. -- @param #FLIGHTCONTROL self -- @return #FLIGHTCONTROL.Runway Active runway. function FLIGHTCONTROL:_GetActiveRunway() -- TODO: get runway. local i=math.max(self.activerwyno, #self.runways) return self.runways[i] end --- Init parking spots. -- @param #FLIGHTCONTROL self function FLIGHTCONTROL:_InitParkingSpots() -- Parking spots of airbase. local parkingdata=self.airbase:GetParkingSpotsTable() self.parking={} for _,_spot in pairs(parkingdata) do local spot=_spot --Wrapper.Airbase#AIRBASE.ParkingData local parking={} --#FLIGHTCONTROL.ParkingSpot parking.position=spot.Coordinate parking.drunway=spot.DistToRwy parking.terminal=spot.TerminalType parking.id=spot.TerminalID parking.free=spot.Free parking.reserved=spot.TOAC -- Mark position. local text=string.format("ID=%d, Terminal=%d, Free=%s, Reserved=%s, Dist=%.1f", parking.id, parking.terminal, tostring(parking.free), tostring(parking.reserved), parking.drunway) parking.markerid=parking.position:MarkToAll(text) -- Add to table. table.insert(self.parking, parking) end end --- Get free parking spots. -- @param #FLIGHTCONTROL self -- @param #number terminal Terminal type or nil. -- @return #number Number of free spots. Total if terminal=nil or of the requested terminal type. -- @return #table Table of free parking spots of data type #FLIGHCONTROL.ParkingSpot. function FLIGHTCONTROL:_GetFreeParkingSpots(terminal) local freespots={} local n=0 for _,_parking in pairs(self.parking) do local parking=_parking --#FLIGHTCONTROL.ParkingSpot if parking.free then if terminal==nil or terminal==parking.terminal then n=n+1 table.insert(freespots, parking) end end end return n,freespots end --- Init parking spots. -- @param #FLIGHTCONTROL self function FLIGHTCONTROL:_UpdateParkingSpots() -- Parking spots of airbase. local parkingdata=self.airbase:GetParkingSpotsTable() local message="Parking Spots:" for _,_parkingspot in pairs(self.parking) do local parking=_parkingspot --#FLIGHTCONTROL.ParkingSpot for _,_spot in pairs(parkingdata) do local spot=_spot --Wrapper.Airbase#AIRBASE.ParkingSpot if parking.id==spot.TerminalID then parking.position=spot.Coordinate parking.drunway=spot.DistToRwy parking.terminal=spot.TerminalType parking.id=spot.TerminalID parking.free=spot.Free parking.reserved=spot.TOAC -- Mark position. if parking.markerid then parking.position:RemoveMark(parking.markerid) end local text=string.format("ID=%d, Terminal=%d, Free=%s, Reserved=%s, Dist=%.1f", parking.id, parking.terminal, tostring(parking.free), tostring(parking.reserved), parking.drunway) message=message.."\n"..text parking.markerid=parking.position:MarkToAll(text) break end end end self:E(self.lid..message) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Event Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Event handler for event birth. -- @param #FLIGHTCONTROL self -- @param Core.Event#EVENTDATA EventData function FLIGHTCONTROL:OnEventBirth(EventData) self:F3({EvendData=EventData}) self:T2(self.lid..string.format("BIRTH: unit = %s", tostring(EventData.IniUnitName))) self:T2(self.lid..string.format("BIRTH: group = %s", tostring(EventData.IniGroupName))) end --- Event handler for event land. -- @param #FLIGHTCONTROL self -- @param Core.Event#EVENTDATA EventData function FLIGHTCONTROL:OnEventLand(EventData) self:F3({EvendData=EventData}) self:T2(self.lid..string.format("LAND: unit = %s", tostring(EventData.IniUnitName))) self:T2(self.lid..string.format("LAND: group = %s", tostring(EventData.IniGroupName))) end --- Event handler for event takeoff. -- @param #FLIGHTCONTROL self -- @param Core.Event#EVENTDATA EventData function FLIGHTCONTROL:OnEventTakeoff(EventData) self:F3({EvendData=EventData}) self:T2(self.lid..string.format("TAKEOFF: unit = %s", tostring(EventData.IniUnitName))) self:T2(self.lid..string.format("TAKEOFF: group = %s", tostring(EventData.IniGroupName))) -- This would be the closest airbase. local airbase=EventData.Place -- Unit that took off. local unit=EventData.IniUnit -- Nil check for airbase. Crashed as player gave me no airbase. if not (airbase or unit) then self:E(self.lid.."WARNING: Airbase or IniUnit is nil in takeoff event!") return end end --- Event handler for event engine startup. -- @param #FLIGHTCONTROL self -- @param Core.Event#EVENTDATA EventData function FLIGHTCONTROL:OnEventEngineStartup(EventData) self:F3({EvendData=EventData}) self:T2(self.lid..string.format("ENGINESTARTUP: unit = %s", tostring(EventData.IniUnitName))) self:T2(self.lid..string.format("ENGINESTARTUP: group = %s", tostring(EventData.IniGroupName))) -- Unit that took off. local unit=EventData.IniUnit -- Nil check for unit. if not unit then return end end --- Event handler for event engine shutdown. -- @param #FLIGHTCONTROL self -- @param Core.Event#EVENTDATA EventData function FLIGHTCONTROL:OnEventEngineShutdown(EventData) self:F3({EvendData=EventData}) self:T2(self.lid..string.format("ENGINESHUTDOWN: unit = %s", tostring(EventData.IniUnitName))) self:T2(self.lid..string.format("ENGINESHUTDOWN: group = %s", tostring(EventData.IniGroupName))) -- Unit that took off. local unit=EventData.IniUnit -- Nil check for unit. if not unit then return end end --- Event handler for event crash. -- @param #FLIGHTCONTROL self -- @param Core.Event#EVENTDATA EventData function FLIGHTCONTROL:OnEventCrash(EventData) self:F3({EvendData=EventData}) self:T2(self.lid..string.format("CRASH: unit = %s", tostring(EventData.IniUnitName))) self:T2(self.lid..string.format("CRASH: group = %s", tostring(EventData.IniGroupName))) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Queue Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Scan airbase zone. -- @param #FLIGHTCONTROL self function FLIGHTCONTROL:_CheckQueues() -- Print queues self:_PrintQueue(self.flights, "All flights") self:_PrintQueue(self.Qparking, "Parking") self:_PrintQueue(self.Qtakeoff, "Takeoff") self:_PrintQueue(self.Qwaiting, "Holding") self:_PrintQueue(self.Qlanding, "Landing") -- Get next wairing flight. local flight=self:_GetNextWaitingFight() -- Number of groups landing. local nlanding=#self.Qlanding -- Number of groups taking off. local ntakeoff=#self.Qtakeoff if flight and nlanding==0 and ntakeoff==0 then self:_LandAI(flight) self:_RemoveFlightFromQueue(self.Qwaiting, flight) end end --- Check parking spots. -- @param #FLIGHTCONTROL self function FLIGHTCONTROL:_CheckParking() -- Init all elements as NOT parking anywhere. for _,_flight in pairs(self.flights) do -- Loop over all elements. for _,_element in pairs(_flight.elements) do local element=_element --#FLIGHTCONTROL.FlightElement element.parking=false end end -- Loop over all parking spots. for i,_spot in pairs(self.parking) do local spot=_spot --#FLIGHTCONTROL.ParkingSpot -- Assume spot is free. spot.free=true -- Loop over all flights. for _,_flight in pairs(self.flights) do local flight=_flight --Ops.FlightGroup#FLIGHTGROUP -- Loop over all elements. for _,_element in pairs(_flight.elements) do local element=_element --Ops.FlightGroup#FLIGHTGROUP.Element if element.unit and element.unit:IsAlive() then -- Distance to parking spot. local dist=element.unit:GetCoordinate():Get3DDistance(spot.position) -- Element is parking on this spot if dist<5 and not element.unit:InAir() then element.parking=true spot.free=false end else self:E(self.lid..string.format("ERROR: Element %s is not alive any more!", element.name)) end end end end end --- Get next flight waiting for landing clearance. -- @param #FLIGHTCONTROL self -- @return Ops.FlightGroup#FLIGHTGROUP Marshal flight next in line and ready to enter the pattern. Or nil if no flight is ready. function FLIGHTCONTROL:_GetNextWaitingFight() -- Loop over all marshal flights. for _,_flight in pairs(self.Qwaiting) do local flight=_flight --Ops.FlightGroup#FLIGHTGROUP -- TODO: Sort by fuel, holding time. return flight end return nil end --- Print queue. -- @param #FLIGHTCONTROL self -- @param #table queue Queue to print. -- @param #string name Queue name. function FLIGHTCONTROL:_PrintQueue(queue, name) local text=string.format("%s Queue N=%d:", name, #queue) if #queue==0 then -- Queue is empty. text=text.." empty." else -- Loop over all flights in queue. for i,_flight in ipairs(queue) do local flight=_flight --Ops.FlightGroup#FLIGHTGROUP -- Gather info. local clock="0:0" --UTILS.SecondsToClock(timer.getAbsTime()-flight.time) local fuel=flight.group:GetFuelMin()*100 local ai=tostring(flight.ai) local actype=tostring(flight.actype) local holding=tostring(flight.holding) local nunits=flight.nunits or 1 -- Main info. text=text..string.format("\n[%d] %s (%s*%d): ai=%s, timestamp=%s, fuel=%d, inzone=%s, holding=%s", i, flight.groupname, actype, nunits, ai, clock, fuel, tostring(flight.inzone), holding) -- Elements info. for j,_element in pairs(flight.elements) do local element=_element --Ops.FlightGroup#FLIGHTGROUP.Element local life=element.unit:GetLife() local life0=element.unit:GetLife0() text=text..string.format("\n (%d) %s (%s): ai=%s, status=%s,, airborne=%s life=%.1f/%.1f", j, tostring(element.modex), element.name, tostring(element.ai) ,tostring(element.status), tostring(element.unit:InAir()), life, life0) end end end -- Display text. self:T(self.lid..text) end --- Remove a flight group from a queue. -- @param #FLIGHTCONTROL self -- @param #table queue The queue from which the group will be removed. -- @param Ops.FlightGroup#FLIGHTGROUP flight Flight group that will be removed from queue. -- @return #boolean True, flight was in Queue and removed. False otherwise. -- @return #number Table index of removed queue element or nil. function FLIGHTCONTROL:_RemoveFlightFromQueue(queue, flight) -- Loop over all flights in group. for i,_flight in pairs(queue) do local qflight=_flight --Ops.FlightGroup#FLIGHTGROUP -- Check for name. if qflight.groupname==flight.groupname then self:I(self.lid..string.format("Removing flight group %s from queue.", flight.groupname)) table.remove(queue, i) return true, i end end self:I(self.lid..string.format("Could NOT remove flight group %s from queue.", flight.groupname)) return false, nil end --- Set tookoff to true for the flight element. -- @param #FLIGHTCONTROL self -- @param Wrapper.Unit#UNIT unit The aircraft unit that was recovered. -- @return Ops.FlightGroup#FLIGHTGROUP Flight group of element. function FLIGHTCONTROL:_ElementTookOff(unit) -- Get element of flight. local element, idx, flight=self:_GetFlightElement(unit:GetName()) --#FLIGHTCONTROL.FlightElement -- Nil check. Could be if a helo landed or something else we dont know! if element then element.tookoff=true end return flight end --- Add flight to landing queue and set recovered to false for all elements of the flight and its section members. -- @param #FLIGHTCONTROL self -- @param Ops.FlightGroup#FLIGHTGROUP flight Flight group of element. function FLIGHTCONTROL:_AddFlightToLandingQueue(flight) -- Add flight to table. table.insert(self.Qlanding, flight) -- Set flag to -1. flight.flag=-1 -- New time stamp for time in pattern. flight.time=timer.getAbsTime() -- Init recovered switch. flight.recovered=false for _,elem in pairs(flight.elements) do elem.recoverd=false end end --- Add flight to takeoff queue. -- @param #FLIGHTCONTROL self -- @param Ops.FlightGroup#FLIGHTGROUP flight Flight group of element. function FLIGHTCONTROL:_AddFlightToTakeoffQueue(flight) -- Check if already in queue. if self:_InQueue(self.Qtakeoff,flight.group) then return end -- Add flight to table. table.insert(self.Qtakeoff, flight) -- Set flag to -1. flight.flag=-1 -- New time stamp for time in pattern. flight.time=timer.getAbsTime() -- Init recovered switch. flight.tookoff=false for _,elem in pairs(flight.elements) do elem.tookoff=false end end --- Check if a group is in a queue. -- @param #FLIGHTCONTROL self -- @param #table queue The queue to check. -- @param Wrapper.Group#GROUP group The group to be checked. -- @return #boolean If true, group is in the queue. False otherwise. function FLIGHTCONTROL:_InQueue(queue, group) local name=group:GetName() for _,_flight in pairs(queue) do local flight=_flight --Ops.FlightGroup#FLIGHTGROUP if name==flight.groupname then return true end end return false end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Flight and Element Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Create a new flight group. -- @param #FLIGHTCONTROL self -- @param Wrapper.Group#GROUP group Aircraft group. -- @return Ops.FlightGroup#FLIGHTGROUP Flight group. function FLIGHTCONTROL:_CreateFlightGroup(group) -- Check if not already in flights if self:_InQueue(self.flights, group) then return end -- Debug info. self:I(self.lid..string.format("Creating new flight for group %s of aircraft type %s.", group:GetName(), group:GetTypeName())) -- Get flightgroup from data base. local flight=_DATABASE:GetFlightGroup(group:GetName()) -- If it does not exist yet, create one. if not flight then flight=FLIGHTGROUP:New(group:GetName()) end if flight.destination and flight.destination:GetName()==self.airbasename then flight:SetFlightControl(self) end --[[ -- New flight. local flight={} --Ops.FlightGroup#FLIGHTGROUP -- Flight group name local groupname=group:GetName() local human, playername=self:_IsHuman(group) -- Queue table item. flight.group=group flight.groupname=group:GetName() flight.nunits=#group:GetUnits() flight.time=timer.getAbsTime() flight.dist0=group:GetCoordinate():Get2DDistance(self:GetCoordinate()) flight.flag=-100 flight.ai=not human flight.actype=group:GetTypeName() flight.onboardnumbers=self:_GetOnboardNumbers(group) flight.inzone=flight.group:IsCompletelyInZone(self.zoneAirbase) flight.holding=nil -- Flight elements. local text=string.format("Flight elements of group %s:", flight.groupname) flight.elements={} local units=group:GetUnits() for i,_unit in pairs(units) do local unit=_unit --Wrapper.Unit#UNIT local element={} --#FLIGHTCONTROL.FlightElement element.unit=unit element.unitname=unit:GetName() element.onboard=flight.onboardnumbers[element.unitname] element.sizemax=unit:GetObjectSize() element.ai=not self:_IsHumanUnit(unit) -- Debug text. text=text..string.format("\n[%d] %s onboard #%s, AI=%s", i, element.unitname, tostring(element.onboard), tostring(element.ai)) -- Add to table. table.insert(flight.elements, element) end self:T(self.lid..text) -- Onboard. if flight.ai then local onboard=flight.onboardnumbers[flight.seclead] flight.onboard=onboard else flight.onboard=self:_GetOnboardNumberPlayer(group) end ]] -- Add to known flights. table.insert(self.flights, flight) return flight end --- Get flight from group. -- @param #FLIGHTCONTROL self -- @param Wrapper.Group#GROUP group Group that will be removed from queue. -- @param #table queue The queue from which the group will be removed. -- @return Ops.FlightGroup#FLIGHTGROUP Flight group or nil. -- @return #number Queue index or nil. function FLIGHTCONTROL:_GetFlightFromGroup(group) if group then -- Group name local name=group:GetName() -- Loop over all flight groups in queue for i,_flight in pairs(self.flights) do local flight=_flight --Ops.FlightGroup#FLIGHTGROUP if flight.groupname==name then return flight, i end end self:T2(self.lid..string.format("WARNING: Flight group %s could not be found in queue.", name)) end self:T2(self.lid..string.format("WARNING: Flight group could not be found in queue. Group is nil!")) return nil, nil end --- Get element of flight from its unit name. -- @param #FLIGHTCONTROL self -- @param #string unitname Name of the unit. -- @return #FLIGHTCONTROL.FlightElement Element of the flight or nil. -- @return #number Element index or nil. -- @return Ops.FlightGroup#FLIGHTGROUP The Flight group or nil. function FLIGHTCONTROL:_GetFlightElement(unitname) -- Get the unit. local unit=UNIT:FindByName(unitname) -- Check if unit exists. if unit then -- Get flight element from all flights. local flight=self:_GetFlightFromGroup(unit:GetGroup()) -- Check if fight exists. if flight then -- Loop over all elements in flight group. for i,_element in pairs(flight.elements) do local element=_element --#FLIGHTCONTROL.FlightElement if element.unit:GetName()==unitname then return element, i, flight end end self:T2(self.lid..string.format("WARNING: Flight element %s could not be found in flight group.", unitname, flight.groupname)) end end return nil, nil, nil end --- Get parking spot of flight element. -- @param #FLIGHTCONTROL self -- @param Ops.FlightGroup#FLIGHTGROUP.Element element Element of flight group. -- @return #FLIGHTCONTROL.ParkingSpot Parking spot of flight element or nil. function FLIGHTCONTROL:_GetElementParkingSpot(element) if element then -- Unit object. local unit=element.unit if unit then local upos=unit:GetCoordinate() for _,_parkingspot in pairs(self.parking) do local parkingspot=_parkingspot --#FLIGHTCONTROL.ParkingSpot -- Spot position. local spos=parkingspot.position -- 3D distance from unit to spot. local dist=spos:Get3DDistance(upos) -- Distance threshold 5 meters. if dist<5 then return parkingspot end end end end return nil end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Routing Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Scan airbase zone and find new flights. -- @param #FLIGHTCONTROL self function FLIGHTCONTROL:_CheckInbound() -- Airbase position. local coord=self:GetCoordinate() -- Scan radius = 20 NM. local RCCZ=UTILS.NMToMeters(20) -- Debug info. self:T(self.lid..string.format("Scanning airbase zone. Radius=%.1f NM.", UTILS.MetersToNM(RCCZ))) -- Scan units in carrier zone. local _,_,_,unitscan=coord:ScanObjects(RCCZ, true, false, false) -- Make a table with all groups currently in the CCA zone. local insideZone={} for _,_unit in pairs(unitscan) do local unit=_unit --Wrapper.Unit#UNIT -- Necessary conditions to be met: local aircraft=unit:IsAir() local inzone=unit:IsInZone(self.zoneAirbase) local friendly=self:GetCoalition()==unit:GetCoalition() -- Check if this an aircraft and that it is inside the airbase zone and friendly. if aircraft and inzone and friendly then local group=unit:GetGroup() local groupname=group:GetName() -- Add group to table. if insideZone[groupname]==nil then insideZone[groupname]=group end end end -- Find new flights that are inside the airbase zone. for groupname,_group in pairs(insideZone) do local group=_group --Wrapper.Group#GROUP self:_CreateFlightGroup(group) end for _,_flight in pairs(self.flights) do local flight=_flight --Ops.FlightGroup#FLIGHTGROUP if flight and flight.destination and flight.destination:GetName()==self.airbasename then -- Send AI to orbit outside 10 NM zone and wait until the next Marshal stack is available. if not (self:_InQueue(self.Qwaiting, flight.group) or self:_InQueue(self.Qlanding, flight.group) or flight:IsHolding()) then self:_WaitAI(flight, 1, true) end end end end --- Command AI flight to orbit. -- @param #FLIGHTCONTROL self -- @param Ops.FlightGroup#FLIGHTGROUP flight Flight group. -- @param #number stack Holding stack. -- @param #boolean respawn If true respawn the group. Otherwise reset the mission task with new waypoints. function FLIGHTCONTROL:_WaitAI(flight, stack, respawn) -- Set flag to something other than -100 and <0 flight.flag=stack -- Holding point. local holding=self:_GetHoldingpoint(flight) local altitude=holding.pos0.y local angels=UTILS.MetersToFeet(altitude)/1000 flight:Hold(self.airbase, holding.pos0) if true then return end -- Flight group name. local group=flight.group local groupname=flight.groupname ---------------- -- Set Speeds -- ---------------- -- Aircraft speed 274 knots TAS ~= 250 KIAS when orbiting the pattern. (Orbit expects m/s.) local speedOrbitMps=UTILS.KnotsToMps(274) -- Orbit speed in km/h for waypoints. local speedOrbitKmh=UTILS.KnotsToKmph(274) -- Aircraft speed 400 knots when transiting to holding zone. (Waypoint expects km/h.) local speedTransit=UTILS.KnotsToKmph(370) --------------- -- Waypoints -- --------------- -- Waypoints array to be filled depending on case etc. local wp={} -- Current position. Always good for as the first waypoint. wp[1]=group:GetCoordinate():WaypointAirTurningPoint(nil, speedTransit, {}, "Current Position") -- Task function when arriving at the holding zone. This will set flight.holding=true. local TaskHolding=flight.group:TaskFunction("FLIGHTCONTROL._ReachedHoldingZone", self, flight) -- Set orbit task. local TaskOrbit=group:TaskOrbit(holding.pos0, altitude, speedOrbitMps, holding.pos1) -- Orbit at waypoint. wp[#wp+1]=holding.pos0:WaypointAirTurningPoint(nil, speedOrbitKmh, {TaskHolding, TaskOrbit}, string.format("Holding at Angels %d", angels)) -- Debug markers. if self.Debug then holding.pos0:MarkToAll(string.format("Waiting Orbit of flight %s at Angels %s", groupname, angels)) end if respawn then -- This should clear the landing waypoints. -- Note: This resets the weapons and the fuel state. But not the units fortunately. -- Get group template. local Template=group:GetTemplate() -- Set route points. Template.route.points=wp -- Respawn the group. group=group:Respawn(Template, true) end -- Reinit waypoints. group:WayPointInitialize(wp) -- Route group. group:Route(wp, 1) end --- Tell AI to land at the airbase. Flight is added to the landing queue. -- @param #FLIGHTCONTROL self -- @param Ops.FlightGroup#FLIGHTGROUP flight Flight group. function FLIGHTCONTROL:_LandAI(flight) -- Debug info. self:I(self.lid..string.format("Landing AI flight %s.", flight.groupname)) -- Add flight to landing queue. table.insert(self.Qlanding, flight) flight.flaghold:Set(1) if true then return end -- Airbase position. local airbase=self.airbase:GetCoordinate() -- Waypoints array. local wp={} -- Current speed. local CurrentSpeed=flight.group:GetVelocityKMH() -- Aircraft speed when flying the pattern. local Speed=UTILS.KnotsToKmph(150) -- Current positon. wp[#wp+1]=flight.group:GetCoordinate():WaypointAirTurningPoint(nil, CurrentSpeed, {}, "Current position") -- Get active runway. local runway=self:_GetActiveRunway() -- TODO: make dependend on AC type helos etc. -- Approach point: 10 NN in direction of runway. local papproach=runway.position:Translate(UTILS.NMToMeters(10), runway.direction):SetAltitude(1000) papproach:MarkToAll("Approach Point") -- Approach waypoint. wp[#wp+1]=papproach:WaypointAirTurningPoint(nil ,Speed, {}, "Final Approach") -- Landing waypoint. wp[#wp+1]=airbase:WaypointAirLanding(Speed, self.airbase, nil, "Landing") -- Reinit waypoints. flight.group:WayPointInitialize(wp) -- Route group. flight.group:Route(wp, 1) -- Add flight to landing queue. table.insert(self.Qlanding, flight) end --- Function called when a group has reached the holding zone. --@param Wrapper.Group#GROUP group Group that reached the holding zone. --@param #FLIGHTCONTROL flightcontrol Flightcontrol object. --@param Ops.FlightGroup#FLIGHTGROUP flight Flight group that has reached the holding zone. function FLIGHTCONTROL._ReachedHoldingZone(group, flightcontrol, flight) -- Debug message. local text=string.format("Flight %s reached holding zone.", group:GetName()) MESSAGE:New(text,10):ToAllIf(flightcontrol.Debug) flightcontrol:T(flightcontrol.lid..text) -- Debug mark. if flightcontrol.Debug then group:GetCoordinate():MarkToAll(text) end -- Set holding flag true and set timestamp for marshal time check. if flight then flight.holding=true flight.time=timer.getAbsTime() end end --- Get holding point. -- @param #FLIGHTCONTROL self -- @param Ops.FlightGroup#FLIGHTGROUP flight Flight group. -- @return #FLIGHTCONTROL.HoldingPoint Holding point. function FLIGHTCONTROL:_GetHoldingpoint(flight) local holdingpoint={} --#FLIGHTCONTROL.HoldingPoint local runway=self:_GetActiveRunway() local hdg=runway.direction+90 local dx=UTILS.NMToMeters(5) local dz=UTILS.NMToMeters(1) local angels=UTILS.FeetToMeters(math.random(6,10)*1000) holdingpoint.pos0=runway.position:Translate(dx, hdg):SetAltitude(angels) holdingpoint.pos1=holdingpoint.pos0:Translate(dz, runway.direction):SetAltitude(angels) return holdingpoint end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Misc Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --TODO: Get landing waypoint to check whether flight is meant to land at this airbase. --- Get coordinate of the airbase. -- @param #FLIGHTCONTROL self -- @return Core.Point#COORDINATE Coordinate of the airbase. function FLIGHTCONTROL:GetCoordinate() return self.airbase:GetCoordinate() end --- Get coalition of the airbase. -- @param #FLIGHTCONTROL self -- @return #number Coalition ID. function FLIGHTCONTROL:GetCoalition() return self.airbase:GetCoalition() end --- Get onboard number of player or client. -- @param #FLIGHTCONTROL self -- @param Wrapper.Group#GROUP group Aircraft group. -- @return #string Onboard number as string. function FLIGHTCONTROL:_GetOnboardNumberPlayer(group) return self:_GetOnboardNumbers(group, true) end --- Get onboard numbers of all units in a group. -- @param #FLIGHTCONTROL self -- @param Wrapper.Group#GROUP group Aircraft group. -- @param #boolean playeronly If true, return the onboard number for player or client skill units. -- @return #table Table of onboard numbers. function FLIGHTCONTROL:_GetOnboardNumbers(group, playeronly) -- Get group name. local groupname=group:GetName() -- Debug text. local text=string.format("Onboard numbers of group %s:", groupname) -- Units of template group. local units=group:GetTemplate().units -- Get numbers. local numbers={} for _,unit in pairs(units) do -- Onboard number and unit name. local n=tostring(unit.onboard_num) local name=unit.name local skill=unit.skill -- Debug text. text=text..string.format("\n- unit %s: onboard #=%s skill=%s", name, n, skill) if playeronly and skill=="Client" or skill=="Player" then -- There can be only one player in the group, so we skip everything else. return n end -- Table entry. numbers[name]=n end -- Debug info. self:T2(self.lid..text) return numbers end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------