--- **OPS** - Manage recovery of aircraft at airdromes. -- -- -- -- **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 #number verbose Verbosity level. -- @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 flights All flights table. -- @field #table clients Table with all clients spawning at this airbase. -- @field Ops.ATIS#ATIS atis ATIS object. -- @field #number activerwyno Number of active runway. -- @field #number atcfreq ATC radio frequency. -- @field Core.RadioQueue#RADIOQUEUE atcradio ATC radio queue. -- @field #number Nlanding Max number of aircraft groups in the landing pattern. -- @field #number dTlanding Time interval in seconds between landing clearance. -- @field #number Nparkingspots Total number of parking spots. -- @field Core.Spawn#SPAWN parkingGuard Parking guard spawner. -- @extends Core.Fsm#FSM --- **Ground Control**: Airliner X, Good news, you are clear to taxi to the active. -- **Pilot**: Roger, What's the bad news? -- **Ground Control**: No bad news at the moment, but you probably want to get gone before I find any. -- -- === -- -- ![Banner Image](..\Presentations\FLIGHTCONTROL\FlightControl_Main.jpg) -- -- # The FLIGHTCONTROL Concept -- -- -- -- @field #FLIGHTCONTROL FLIGHTCONTROL = { ClassName = "FLIGHTCONTROL", verbose = 3, lid = nil, theatre = nil, airbasename = nil, airbase = nil, airbasetype = nil, zoneAirbase = nil, parking = {}, runways = {}, flights = {}, clients = {}, atis = nil, activerwyno = 1, atcfreq = nil, atcradio = nil, atcradiounitname = nil, Nlanding = nil, dTlanding = nil, Nparkingspots = nil, } --- 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. --- Player menu data. -- @type FLIGHTCONTROL.PlayerMenu -- @field Core.Menu#MENU_GROUP root Root menu. -- @field Core.Menu#MENU_GROUP_COMMAND RequestTaxi Request taxi. --- Parking spot data. -- @type FLIGHTCONTROL.ParkingSpot -- @field Wrapper.Group#GROUP ParkingGuard Parking guard for this spot. -- @extends Wrapper.Airbase#AIRBASE.ParkingSpot --- Parking spot data. -- @type FLIGHTCONTROL.FlightStatus -- @field #string INBOUND Flight is inbound. -- @field #string HOLDING Flight is holding. -- @field #string LANDING Flight is landing. -- @field #string TAXIINB Flight is taxiing to parking area. -- @field #string ARRIVED Flight arrived at parking spot. -- @field #string TAXIOUT Flight is taxiing to runway for takeoff. -- @field #string READYTO Flight is ready for takeoff. -- @field #string TAKEOFF Flight is taking off. FLIGHTCONTROL.FlightStatus={ INBOUND="Inbound", HOLDING="Holding", LANDING="Landing", TAXIINB="Taxi Inbound", ARRIVED="Arrived", PARKING="Parking", TAXIOUT="Taxi to runway", READYTO="Ready For Takeoff", TAKEOFF="Takeoff", } --- 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. --- Sound file data. -- @type FLIGHTCONTROL.Soundfile -- @field #string filename Name of the file -- @field #number duration Duration in seconds. --- Sound files. -- @type FLIGHTCONTROL.Sound -- @field #FLIGHTCONTROL.Soundfile ActiveRunway FLIGHTCONTROL.Sound = { ActiveRunway={filename="ActiveRunway.ogg", duration=0.99}, } --- FlightControl class version. -- @field #string version FLIGHTCONTROL.version="0.5.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Runway destroyed. -- TODO: Define holding zone -- DONE: Add parking guard. -- TODO: Accept and forbit parking spots. -- NOGO: Add FARPS? -- TODO: Add helos. -- TODO: Talk me down option. -- TODO: ATIS option. -- TODO: ATC voice overs. -- TODO: Check runways and clean up. -- DONE: 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 -- Try to get the airbase. self.airbase=AIRBASE:FindByName(airbasename) -- Name of the airbase. self.airbasename=airbasename -- Set some string id for output to DCS.log file. self.lid=string.format("FLIGHTCONTROL %s | ", airbasename) -- Check if the airbase exists. if not self.airbase then self:E(string.format("ERROR: Could not find airbase %s!", tostring(airbasename))) return nil end -- Check if airbase is an airdrome. if self.airbase:GetAirbaseCategory()~=Airbase.Category.AIRDROME then self:E(string.format("ERROR: Airbase %s is not an AIRDROME! Script does not handle FARPS or ships.", tostring(airbasename))) return nil end -- Airbase category airdrome, FARP, SHIP. self.airbasetype=self.airbase:GetAirbaseCategory() -- Current map. self.theatre=env.mission.theatre -- 5 NM zone around the airbase. self.zoneAirbase=ZONE_RADIUS:New("FC", self:GetCoordinate():GetVec2(), UTILS.NMToMeters(5)) -- Defaults: self:SetLandingMax() self:SetLandingInterval() -- Init runways. self:_InitRunwayData() -- 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. -- Add to data base. _DATABASE:AddFlightControl(self) return self end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- User API Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Set the number of aircraft groups, that are allowed to land simultaniously. -- @param #FLIGHTCONTROL self -- @param #number n Max number of aircraft landing simultaniously. Default 2. -- @return #FLIGHTCONTROL self function FLIGHTCONTROL:SetLandingMax(n) self.Nlanding=n or 2 return self end --- Set time interval between landing clearance of groups. -- @param #FLIGHTCONTROL self -- @param #number dt Time interval in seconds. Default 180 sec (3 min). -- @return #FLIGHTCONTROL self function FLIGHTCONTROL:SetLandingInterval(dt) self.dTlanding=dt or 180 return self end --- 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 --- Set the parking guard group. -- @param #FLIGHTCONTROL self -- @param #string TemplateGroupName Name of the template group. -- @return #FLIGHTCONTROL self function FLIGHTCONTROL:SetParkingGuard(TemplateGroupName) local alias=string.format("Parking Guard %s", self.airbasename) -- Need spawn with alias for multiple FCs. self.parkingGuard=SPAWN:NewWithAlias(TemplateGroupName, alias) --self.parkingGuard=SPAWNSTATIC:NewFromStatic("Parking Guard"):InitNamePrefix(alias) 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)) -- Init parking spots. self:_InitParkingSpots() -- 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) self.atcradio=RADIOQUEUE:New(self.atcfreq or 305, nil, string.format("FC %s", self.airbasename)) self.atcradio:Start(1, 0.1) -- Init status updates. self:__Status(-1) end --- Update status. -- @param #FLIGHTCONTROL self function FLIGHTCONTROL:onafterStatus() -- Check status of all registered flights. self:_CheckFlights() -- Check parking spots. --self:_CheckParking() -- Check waiting and landing queue. self:_CheckQueues() -- Get runway. local runway=self:GetActiveRunway() local Nflights= self:CountFlights() local NQparking=self:CountFlights(FLIGHTCONTROL.FlightStatus.PARKING) local NQtaxiout=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAXIOUT) local NQreadyto=self:CountFlights(FLIGHTCONTROL.FlightStatus.READYTO) local NQtakeoff=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAKEOFF) local NQinbound=self:CountFlights(FLIGHTCONTROL.FlightStatus.INBOUND) local NQholding=self:CountFlights(FLIGHTCONTROL.FlightStatus.HOLDING) local NQlanding=self:CountFlights(FLIGHTCONTROL.FlightStatus.LANDING) local NQtaxiinb=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAXIINB) local NQarrived=self:CountFlights(FLIGHTCONTROL.FlightStatus.ARRIVED) -- ========================================================================================================= local Nqueues = (NQparking+NQtaxiout+NQreadyto+NQtakeoff) + (NQinbound+NQholding+NQlanding+NQtaxiinb+NQarrived) -- Count free parking spots. --TODO: get and substract number of reserved parking spots. local nfree=self.Nparkingspots-NQarrived-NQparking local Nfree=self:CountParking(AIRBASE.SpotStatus.FREE) local Noccu=self:CountParking(AIRBASE.SpotStatus.OCCUPIED) local Nresv=self:CountParking(AIRBASE.SpotStatus.RESERVED) if Nfree+Noccu+Nresv~=self.Nparkingspots then self:E(self.lid..string.format("WARNING: Number of parking spots does not match! Nfree=%d, Noccu=%d, Nreserved=%d != %d total", Nfree, Noccu, Nresv, self.Nparkingspots)) end -- Info text. if self.verbose>0 then local text=string.format("State %s - Runway %s - Parking F=%d/O=%d/R=%d of %d - Flights=%s", self:GetState(), runway.idx, Nfree, Noccu, Nresv, self.Nparkingspots, Nflights) self:I(self.lid..text) end if Nflights==Nqueues then --Check! else self:E(string.format("WARNING: Number of total flights %d!=%d number of flights in all queues!", Nflights, Nqueues)) end if self.verbose>1 then local text="Queue:" text=text..string.format("\n- Flights = %d", Nflights) text=text..string.format("\n---------------------------------------------") text=text..string.format("\n- Parking = %d", NQparking) text=text..string.format("\n- Taxi Out = %d", NQtaxiout) text=text..string.format("\n- Ready TO = %d", NQreadyto) text=text..string.format("\n- Take off = %d", NQtakeoff) text=text..string.format("\n---------------------------------------------") text=text..string.format("\n- Inbound = %d", NQinbound) text=text..string.format("\n- Holding = %d", NQholding) text=text..string.format("\n- Landing = %d", NQlanding) text=text..string.format("\n- Taxi Inb = %d", NQtaxiinb) text=text..string.format("\n- Arrived = %d", NQarrived) text=text..string.format("\n---------------------------------------------") self:I(self.lid..text) end -- Next status update in ~30 seconds. self:__Status(-20) end --- Start FLIGHTCONTROL FSM. Handle events. -- @param #FLIGHTCONTROL self function FLIGHTCONTROL:onafterStop() -- 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) self.atcradio:Stop() end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Event Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Event handler for event birth. -- @param #FLIGHTCONTROL self -- @param Core.Event#EVENTDATA EventData function FLIGHTCONTROL:OnEventBirth(EventData) self:F3({EvendData=EventData}) if EventData and EventData.IniGroupName and EventData.IniUnit then -- Debug self:T2(self.lid..string.format("BIRTH: unit = %s", tostring(EventData.IniUnitName))) self:T2(self.lid..string.format("BIRTH: group = %s", tostring(EventData.IniGroupName))) -- Unit that was born. local unit=EventData.IniUnit -- Check if birth took place at this airfield. local bornhere=EventData.Place and EventData.Place:GetName()==self.airbasename or false -- We delay this, to have all elements of the group in the game. if unit:IsAir() and bornhere then -- We got a player? local playerunit, playername=self:_GetPlayerUnitAndName(EventData.IniUnitName) -- Create flight group. if playername then --self:ScheduleOnce(0.5, self._CreateFlightGroup, self, EventData.IniGroup) end self:ScheduleOnce(0.5, self._CreateFlightGroup, self, EventData.IniGroup) -- Spawn parking guard. self:SpawnParkingGuard(unit) end end 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:I(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:I(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 queue. if true then self:_PrintQueue(self.flights, "All flights") end -- Number of holding groups. local nholding=self:CountFlights(FLIGHTCONTROL.FlightStatus.HOLDING) -- Number of groups landing. local nlanding=self:CountFlights(FLIGHTCONTROL.FlightStatus.LANDING) -- Number of parking groups. local nparking=self:CountFlights(FLIGHTCONTROL.FlightStatus.PARKING) -- Number of groups taking off. local ntakeoff=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAKEOFF) -- Get next flight in line: either holding or parking. local flight, isholding, parking=self:_GetNextFlight() -- Check if somebody wants something. if flight then if isholding then -------------------- -- Holding flight -- -------------------- -- No other flight is taking off and number of landing flights is below threshold. if ntakeoff==0 and nlanding=self.dTlanding then -- Message. local text=string.format("Flight %s, you are cleared to land.", flight.groupname) MESSAGE:New(text, 5, "FLIGHTCONTROL"):ToAll() -- Give AI the landing signal. -- TODO: Humans have to confirm via F10 menu. if flight.isAI then self:_LandAI(flight, parking) end -- Set time last flight got landing clearance. self.Tlanding=timer.getAbsTime() end else self:I(self.lid..string.format("FYI: Landing clearance for flight %s denied as other flights are taking off (N=%d) or max. landing reached (N=%d/%d).", flight.groupname, ntakeoff, nlanding, self.Nlanding)) end else -------------------- -- Takeoff flight -- -------------------- -- No other flight is taking off or landing. if ntakeoff==0 and nlanding==0 then -- Check if flight is AI. Humans have to request taxi via F10 menu. if flight.isAI then --- -- AI --- -- Message. local text=string.format("Flight %s, you are cleared to taxi to runway.", flight.groupname) self:I(self.lid..text) MESSAGE:New(text, 5, "FLIGHTCONTROL"):ToAll() -- Start uncontrolled aircraft. if flight:IsUncontrolled() then flight:StartUncontrolled() end -- Add flight to takeoff queue. self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.TAKEOFF) -- Remove parking guards. for _,_element in pairs(flight.elements) do local element=_element --Ops.FlightGroup#FLIGHTGROUP.Element if element and element.parking then local spot=self:GetParkingSpotByID(element.parking.TerminalID) self:RemoveParkingGuard(spot) end end else --- -- PLAYER --- local text=string.format("HUMAN Flight %s, you are cleared for takeoff.", flight.groupname) self:I(self.lid..text) MESSAGE:New(text, 5, "FLIGHTCONTROL"):ToAll() end else self:I(self.lid..string.format("FYI: Take of for flight %s denied as other flights are taking off (N=%d) or landing (N=%d).", flight.groupname, ntakeoff, nlanding)) end end else self:I(self.lid..string.format("FYI: No flight in queue for takeoff or landing.")) end end --- Get next flight in line, either waiting for landing or waiting for takeoff. -- @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. -- @return #boolean If true, flight is holding and waiting for landing, if false, flight is parking and waiting for takeoff. -- @return #table Parking data for holding flights or nil. function FLIGHTCONTROL:_GetNextFlight() local flightholding=self:_GetNextFightHolding() local flightparking=self:_GetNextFightParking() -- If no flight is waiting for landing just return the takeoff flight or nil. if not flightholding then return flightparking, false, nil end -- Get number of alive elements of the holding flight. local nH=flightholding:GetNelements() -- Free parking spots. local parking=flightholding:GetParking(self.airbase) -- If no flight is waiting for takeoff return the holding flight or nil. if not flightparking then if parking then return flightholding, true, parking else self:E(self.lid..string.format("WARNING: No flight parking but no parking spots! nP=%d nH=%d", #parking, nH)) return nil, nil, nil end end -- We got flights waiting for landing and for takeoff. if flightholding and flightparking then -- Return holding flight if fuel is low. if flightholding.fuellow then if parking then -- Enough parking ==> land return flightholding, true, parking else -- Not enough parking ==> take off return flightparking, false, nil end end -- Return the flight which is waiting longer. NOTE that Tholding and Tparking are abs. mission time. So a smaller value means waiting longer. if flightholding.Tholding0 then -- TODO: Could be sorted by distance to active runway! Take the runway spawn point for distance measure. -- First come, first serve. return QreadyTO[1] end -- Get flights parking. local Qparking=self:GetFlights(FLIGHTCONTROL.FlightStatus.PARKING) -- Check special cases where only up to one flight is waiting for takeoff. if #Qparking==0 then return nil end -- Sort flights parking time. local function _sortByTparking(a, b) local flightA=a --Ops.FlightGroup#FLIGHTGROUP local flightB=b --Ops.FlightGroup#FLIGHTGROUP return flightA.Tparking=0 then holding=UTILS.SecondsToClock(holding, true) else holding="X" end local parking=flight:GetParkingTime() if parking>=0 then parking=UTILS.SecondsToClock(parking, true) else parking="X" end local nunits=flight.nunits or 1 -- Main info. text=text..string.format("\n[%d] %s (%s*%d): status=%s, ai=%s, fuel=%d, holding=%s, parking=%s", i, flight.groupname, actype, nunits, flight:GetState(), ai, fuel, holding, parking) -- 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() local park=element.parking and tostring(element.parking.TerminalID) or "N/A" text=text..string.format("\n (%d) %s (%s): status=%s, ai=%s, airborne=%s life=%d/%d spot=%s", j, tostring(element.modex), element.name, tostring(element.status), tostring(element.ai), tostring(element.unit:InAir()), life, life0, park) end end end -- Display text. self:I(self.lid..text) return 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. -- @param #string queuename Name of the 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, queuename) queuename=queuename or "unknown" -- 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 %s queue.", flight.groupname, queuename)) table.remove(queue, i) if not flight.isAI then flight:_UpdateMenu() end return true, i end end self:I(self.lid..string.format("Could NOT remove flight group %s from %s queue.", flight.groupname, queuename)) return false, nil end --- Set flight status. -- @param #FLIGHTCONTROL self -- @param Ops.FlightGroup#FLIGHTGROUP flight Flight group. -- @param #string status New status. -- @param #FLIGHTCONTROL self function FLIGHTCONTROL:SetFlightStatus(flight, status) -- Debug info. self:I(self.lid..string.format("New Flight Status for %s [%s]: %s-->%s", flight:GetName(), flight:GetState(), tostring(flight.controlstatus), status)) -- Set control status flight.controlstatus=status return self end --- Get flight status. -- @param #FLIGHTCONTROL self -- @param Ops.FlightGroup#FLIGHTGROUP flight Flight group. -- @return #string Flight status function FLIGHTCONTROL:GetFlightStatus(flight) if flight then return flight.controlstatus or "unkonwn" end return "unknown" end --- Check if FC has control over this flight. -- @param #FLIGHTCONTROL self -- @param Ops.FlightGroup#FLIGHTGROUP flight Flight group. -- @return #boolean If true, this FC is controlling this flight group. function FLIGHTCONTROL:IsControlling(flight) return flight.flightcontrol and flight.flightcontrol.airbasename==self.airbasename or false 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 --- Get flights. -- @param #FLIGHTCONTROL self -- @param #string Status Return only flights in this status. -- @return #table Table of flights. function FLIGHTCONTROL:GetFlights(Status) if Status then local flights={} for _,_flight in pairs(self.flights) do local flight=_flight --Ops.FlightGroup#FLIGHTGROUP local status=self:GetFlightStatus(flight, Status) if status==Status then table.insert(flights, flight) end end return flights else return self.flights end end --- Count flights in a given status. -- @param #FLIGHTCONTROL self -- @param #string Status Return only flights in this status. -- @return #number function FLIGHTCONTROL:CountFlights(Status) if Status then local flights=self:GetFlights(Status) return #flights else return #self.flights end end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Runway Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Initialize data of runways. -- @param #FLIGHTCONTROL self function FLIGHTCONTROL:_InitRunwayData() self.runways=self.airbase:GetRunwayData() end --- Get the active runway based on current wind direction. -- @param #FLIGHTCONTROL self -- @return Wrapper.Airbase#AIRBASE.Runway Active runway. function FLIGHTCONTROL:GetActiveRunway() return self.airbase:GetActiveRunway() end --- Get the active runway based on current wind direction. -- @param #FLIGHTCONTROL self -- @return #string Runway text, e.g. "31L" or "09". function FLIGHTCONTROL:GetActiveRunwayText() local rwy="" local rwyL if self.atis then rwy, rwyL=self.atis:GetActiveRunway() if rwyL==true then rwy=rwy.."L" elseif rwyL==false then rwy=rwy.."R" end else rwy=self.airbase:GetActiveRunway().idx end return rwy end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Parking Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Init parking spots. -- @param #FLIGHTCONTROL self function FLIGHTCONTROL:_InitParkingSpots() -- Parking spots of airbase. local parkingdata=self.airbase:GetParkingSpotsTable() -- Init parking spots table. self.parking={} self.Nparkingspots=0 for _,_spot in pairs(parkingdata) do local spot=_spot --Wrapper.Airbase#AIRBASE.ParkingSpot -- Mark position. local text=string.format("Parking ID=%d, Terminal=%d: Free=%s, Client=%s, Dist=%.1f", spot.TerminalID, spot.TerminalType, tostring(spot.Free), tostring(spot.ClientSpot), spot.DistToRwy) self:I(self.lid..text) -- Add to table. self.parking[spot.TerminalID]=spot spot.Marker=MARKER:New(spot.Coordinate, "Spot"):ReadOnly() spot.Marker.tocoaliton=true spot.Marker.coalition=self:GetCoalition() -- Check if spot is initially free or occupied. if spot.Free then -- Parking spot is free. self:SetParkingFree(spot) else -- Scan for the unit sitting here. local unit=spot.Coordinate:FindClosestUnit(20) if unit then local unitname=unit and unit:GetName() or "unknown" local isalive=unit:IsAlive() env.info(string.format("FF parking spot %d is occupied by unit %s alive=%s", spot.TerminalID, unitname, tostring(isalive))) if isalive then self:SetParkingOccupied(spot, unitname) self:SpawnParkingGuard(unit) else -- TODO env.info(string.format("FF parking spot %d is occupied by NOT ALIVE unit %s", spot.TerminalID, unitname)) -- Parking spot is free. self:SetParkingFree(spot) end else self:I(self.lid..string.format("ERROR: Parking spot is NOT FREE but no unit could be found there!")) end end -- Increase counter self.Nparkingspots=self.Nparkingspots+1 end end --- Get parking spot by its Terminal ID. -- @param #FLIGHTCONTROL self -- @param #number TerminalID -- @return #FLIGHTCONTROL.ParkingSpot Parking spot data table. function FLIGHTCONTROL:GetParkingSpotByID(TerminalID) return self.parking[TerminalID] end --- Set parking spot to FREE and update F10 marker. -- @param #FLIGHTCONTROL self -- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot The parking spot data table. function FLIGHTCONTROL:SetParkingFree(spot) local spot=self:GetParkingSpotByID(spot.TerminalID) -- Debug info. self:I(self.lid..string.format("Parking spot %d: %s-->%s", spot.TerminalID, tostring(spot.Status), AIRBASE.SpotStatus.FREE)) spot.Status=AIRBASE.SpotStatus.FREE spot.OccupiedBy=nil spot.ReservedBy=nil self:UpdateParkingMarker(spot) end --- Set parking spot to RESERVED and update F10 marker. -- @param #FLIGHTCONTROL self -- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot The parking spot data table. -- @param #string unitname Name of the unit occupying the spot. Default "unknown". function FLIGHTCONTROL:SetParkingReserved(spot, unitname) local spot=self:GetParkingSpotByID(spot.TerminalID) -- Debug info. self:I(self.lid..string.format("Parking spot %d: %s-->%s", spot.TerminalID, tostring(spot.Status), AIRBASE.SpotStatus.RESERVED)) spot.Status=AIRBASE.SpotStatus.RESERVED spot.ReservedBy=unitname or "unknown" self:UpdateParkingMarker(spot) end --- Set parking spot to OCCUPIED and update F10 marker. -- @param #FLIGHTCONTROL self -- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot The parking spot data table. -- @param #string unitname Name of the unit occupying the spot. Default "unknown". function FLIGHTCONTROL:SetParkingOccupied(spot, unitname) local spot=self:GetParkingSpotByID(spot.TerminalID) -- Debug info. self:I(self.lid..string.format("Parking spot %d: %s-->%s", spot.TerminalID, tostring(spot.Status), AIRBASE.SpotStatus.OCCUPIED)) spot.Status=AIRBASE.SpotStatus.OCCUPIED spot.OccupiedBy=unitname or "unknown" self:UpdateParkingMarker(spot) end --- Get free parking spots. -- @param #FLIGHTCONTROL self -- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot The parking spot data table. function FLIGHTCONTROL:UpdateParkingMarker(spot) local spot=self:GetParkingSpotByID(spot.TerminalID) --env.info(string.format("FF updateing spot %d status=%s", spot.TerminalID, spot.Status)) -- Only mark OCCUPIED and RESERVED spots. if spot.Status==AIRBASE.SpotStatus.FREE then if spot.Marker then spot.Marker:Remove() end else local text=string.format("Spot %d (type %d): %s", spot.TerminalID, spot.TerminalType, spot.Status:upper()) if spot.OccupiedBy then text=text..string.format("\nOccupied by %s", spot.OccupiedBy) end if spot.ReservedBy then text=text..string.format("\nReserved for %s", spot.ReservedBy) end if spot.ClientSpot then text=text..string.format("\nClient %s", tostring(spot.ClientSpot)) end if spot.Marker then if text~=spot.Marker.text then spot.Marker:UpdateText(text) end else spot.Marker=MARKER:New(spot.Coordinate, text):ToAll() end end end --- Check if parking spot is free. -- @param #FLIGHTCONTROL self -- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot Parking spot data. -- @return #boolean If true, parking spot is free. function FLIGHTCONTROL:IsParkingFree(spot) return spot.Status==AIRBASE.SpotStatus.FREE end --- Check if a parking spot is reserved by a flight group. -- @param #FLIGHTCONTROL self -- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot Parking spot to check. -- @return #string Name of element or nil. function FLIGHTCONTROL:IsParkingOccupied(spot) if spot.Status==AIRBASE.SpotStatus.OCCUPIED then return tostring(spot.OccupiedBy) else return false end end --- Check if a parking spot is reserved by a flight group. -- @param #FLIGHTCONTROL self -- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot Parking spot to check. -- @return #string Name of element or *nil*. function FLIGHTCONTROL:IsParkingReserved(spot) if spot.Status==AIRBASE.SpotStatus.RESERVED then return tostring(spot.ReservedBy) else return false end -- Init all elements as NOT parking anywhere. 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 local parking=element.parking if parking and parking.TerminalID==spot.TerminalID then return element.name end end end return nil 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 --Wrapper.Airbase#AIRBASE.ParkingSpot if self:IsParkingFree(parking) then if terminal==nil or terminal==parking.terminal then n=n+1 table.insert(freespots, parking) end end end return n,freespots end --- Get closest parking spot. -- @param #FLIGHTCONTROL self -- @param Core.Point#COORDINATE coordinate Reference coordinate. -- @param #number terminaltype (Optional) Check only this terminal type. -- @param #boolean free (Optional) If true, check only free spots. -- @return #FLIGHTCONTROL.ParkingSpot Closest parking spot. function FLIGHTCONTROL:GetClosestParkingSpot(coordinate, terminaltype, free) local distmin=math.huge local spotmin=nil for TerminalID, Spot in pairs(self.parking) do local spot=Spot --Wrapper.Airbase#AIRBASE.ParkingSpot if (not free) or (free==true and not (self:IsParkingReserved(spot) or self:IsParkingOccupied(spot))) then if terminaltype==nil or terminaltype==spot.TerminalType then -- Get distance from coordinate to spot. local dist=coordinate:Get2DDistance(spot.Coordinate) -- Check if distance is smaller. if dist0 then MESSAGE:New("Negative ghostrider, other flights are currently landing. Talk to you soon.", 5):ToAll() self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.READYTO) elseif Ntakeoff>0 then MESSAGE:New("Negative ghostrider, other flights are ahead of you. Talk to you soon.", 5):ToAll() self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.READYTO) end else MESSAGE:New(string.format("Negative, you must request TAXI before you can request TAKEOFF!"), 5):ToAll() end end end --- Player wants to abort takeoff. -- @param #FLIGHTCONTROL self -- @param #string groupname Name of the flight group. function FLIGHTCONTROL:_PlayerAbortTakeoff(groupname) MESSAGE:New("Abort takeoff", 5):ToAll() local flight=_DATABASE:GetFlightGroup(groupname) if flight then if self:GetFlightStatus(flight)==FLIGHTCONTROL.FlightStatus.TAKEOFF then MESSAGE:New("Afirm, You are removed from takeoff queue", 5):ToAll() --TODO: what now? taxi inbound? or just another later attempt to takeoff. self:SetFlightStatus(flight,FLIGHTCONTROL.FlightStatus.READYTO) else MESSAGE:New("Negative, You are NOT in the takeoff queue", 5):ToAll() end end 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 self:E(self.lid..string.format("WARNING: Flight group %s does already exist!", group:GetName())) 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:GetOpsGroup(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 if flight.homebase and flight.homebase:GetName()==self.airbasename then flight:SetFlightControl(self) end flight:SetVerbosity(2) return flight end --- Remove flight from all queues. -- @param #FLIGHTCONTROL self -- @param Ops.FlightGroup#FLIGHTGROUP flight The flight to be removed. function FLIGHTCONTROL:_RemoveFlight(flight) self:_RemoveFlightFromQueue(self.flights, flight, "flights") 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 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Check Sanity Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Check status of all registered flights and do some sanity checks. -- @param #FLIGHTCONTROL self function FLIGHTCONTROL:_CheckFlights() -- First remove all dead flights. for i=#self.flights,1,-1 do local flight=self.flights[i] --Ops.FlightGroup#FLIGHTGROUP if flight:IsDead() then self:I(self.lid..string.format("Removing DEAD flight %s", tostring(flight.groupname))) self:_RemoveFlight(flight) end end --TODO: check parking? end --- Check status of all registered flights and do some sanity checks. -- @param #FLIGHTCONTROL self function FLIGHTCONTROL:_CheckParking() for TerminalID,_spot in pairs(self.parking) do local spot=_spot --Wrapper.Airbase#AIRBASE.ParkingSpot if spot.Reserved then if spot.MarkerID then spot.Coordinate:RemoveMark(spot.MarkerID) end spot.MarkerID=spot.Coordinate:MarkToCoalition(string.format("Parking reserved for %s", tostring(spot.Reserved)), self:GetCoalition()) end -- First remove all dead flights. for i=1,#self.flights do local flight=self.flights[i] --Ops.FlightGroup#FLIGHTGROUP for _,_element in pairs(flight.elements) do local element=_element --Ops.FlightGroup#FLIGHTGROUP.Element if element.parking and element.parking.TerminalID==TerminalID then if spot.MarkerID then spot.Coordinate:RemoveMark(spot.MarkerID) end spot.MarkerID=spot.Coordinate:MarkToCoalition(string.format("Parking spot occupied by %s", tostring(element.name)), self:GetCoalition()) end end end end end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Routing Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Tell AI to land at the airbase. Flight is added to the landing queue. -- @param #FLIGHTCONTROL self -- @param Ops.FlightGroup#FLIGHTGROUP flight Flight group. -- @param #table parking Free parking spots table. function FLIGHTCONTROL:_LandAI(flight, parking) -- Debug info. self:I(self.lid..string.format("Landing AI flight %s.", flight.groupname)) -- Set flight status to LANDING. self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.LANDING) -- Flight is not holding any more. flight.Tholding=nil local respawn=false if respawn then -- Get group template. local Template=flight.group:GetTemplate() -- TODO: get landing waypoints from flightgroup. -- Set route points. Template.route.points=wp for i,unit in pairs(Template.units) do local spot=parking[i] --Wrapper.Airbase#AIRBASE.ParkingSpot local element=flight:GetElementByName(unit.name) if element then -- Set the parking spot at the destination airbase. unit.parking_landing=spot.TerminalID local text=string.format("FF Reserving parking spot %d for unit %s", spot.TerminalID, tostring(unit.name)) self:I(self.lid..text) -- Set parking to RESERVED. self:SetParkingReserved(spot, element.name) else env.info("FF error could not get element to assign parking!") end end -- Debug message. MESSAGE:New(string.format("Respawning group %s", flight.groupname)):ToAll() --Respawn the group. flight:Respawn(Template) else -- Give signal to land. flight:ClearToLand() 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.heading+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.heading):SetAltitude(angels) return holdingpoint end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Radio Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Transmission via RADIOQUEUE. -- @param #FLIGHTCONTROL self -- @param #FLIGHTCONTROL.Soundfile sound FLIGHTCONTROL sound object. -- @param #number interval Interval in seconds after the last transmission finished. -- @param #string subtitle Subtitle of the transmission. -- @param #string path Path to sound file. Default self.soundpath. function FLIGHTCONTROL:Transmission(sound, interval, subtitle, path) self.radioqueue:NewTransmission(sound.filename, sound.duration, path or self.soundpath, nil, interval, subtitle, self.subduration) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Misc Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Add parking guard in front of a parking aircraft. -- @param #FLIGHTCONTROL self -- @param Wrapper.Unit#UNIT unit The aircraft. function FLIGHTCONTROL:SpawnParkingGuard(unit) if unit and self.parkingGuard then -- Position of the unit. local coordinate=unit:GetCoordinate() -- Parking spot. local spot=self:GetClosestParkingSpot(coordinate) -- Current heading of the unit. local heading=unit:GetHeading() -- Length of the unit + 3 meters. local size, x, y, z=unit:GetObjectSize() self:I(self.lid..string.format("Parking guard for %s: heading=%d, distance x=%.1f m", unit:GetName(), heading, x)) -- Coordinate for the guard. local Coordinate=coordinate:Translate(0.75*x+3, heading) -- Let him face the aircraft. local lookat=heading-180 -- Set heading and AI off to save resources. self.parkingGuard:InitHeading(lookat):InitAIOff() -- Group that is spawned. spot.ParkingGuard=self.parkingGuard:SpawnFromCoordinate(Coordinate) --spot.ParkingGuard=self.parkingGuard:SpawnFromCoordinate(Coordinate, lookat) end end --- Remove parking guard. -- @param #FLIGHTCONTROL self -- @param #FLIGHTCONTROL.ParkingSpot spot -- @param #number delay Delay in seconds. function FLIGHTCONTROL:RemoveParkingGuard(spot, delay) if delay and delay>0 then self:ScheduleOnce(delay, FLIGHTCONTROL.RemoveParkingGuard, self, spot) else if spot.ParkingGuard then spot.ParkingGuard:Destroy() spot.ParkingGuard=nil end end end --- 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 country of the airbase. -- @param #FLIGHTCONTROL self -- @return #number Country ID. function FLIGHTCONTROL:GetCountry() return self.airbase:GetCountry() end --- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. -- @param #FLIGHTCONTROL 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 FLIGHTCONTROL:_GetPlayerUnitAndName(unitName) if unitName then -- Get DCS unit from its name. local DCSunit=Unit.getByName(unitName) if DCSunit then -- Get player name if any. local playername=DCSunit:getPlayerName() -- Unit object. local unit=UNIT:Find(DCSunit) -- Check if enverything is there. if DCSunit and unit and playername then self:T(self.lid..string.format("Found DCS unit %s with player %s", tostring(unitName), tostring(playername))) return unit, playername end end end -- Return nil if we could not find a player. return nil,nil end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------