From ddcc851951e45c6d055f04ef55f26bda62650a26 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 12 May 2022 08:03:08 +0200 Subject: [PATCH] FC - Improved FC --- Moose Development/Moose/Modules.lua | 1 + Moose Development/Moose/Ops/FlightControl.lua | 2676 +++++++++++++++++ Moose Development/Moose/Ops/FlightGroup.lua | 53 +- Moose Development/Moose/Ops/OpsGroup.lua | 23 + Moose Setup/Moose.files | 1 + 5 files changed, 2741 insertions(+), 13 deletions(-) create mode 100644 Moose Development/Moose/Ops/FlightControl.lua diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index 38e80ead8..7d15b28b1 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -101,6 +101,7 @@ __Moose.Include( 'Scripts/Moose/Ops/Chief.lua' ) __Moose.Include( 'Scripts/Moose/Ops/Flotilla.lua' ) __Moose.Include( 'Scripts/Moose/Ops/Fleet.lua' ) __Moose.Include( 'Scripts/Moose/Ops/Awacs.lua' ) +__Moose.Include( 'Scripts/Moose/Ops/FlightControl.lua' ) __Moose.Include( 'Scripts/Moose/AI/AI_Balancer.lua' ) __Moose.Include( 'Scripts/Moose/AI/AI_Air.lua' ) diff --git a/Moose Development/Moose/Ops/FlightControl.lua b/Moose Development/Moose/Ops/FlightControl.lua new file mode 100644 index 000000000..a38f6fe0d --- /dev/null +++ b/Moose Development/Moose/Ops/FlightControl.lua @@ -0,0 +1,2676 @@ +--- **OPS** - Air Traffic Control for AI and human players. +-- +-- +-- +-- **Main Features:** +-- +-- * Manage aircraft departure and arrival +-- * Handles AI and human players +-- * Immersive voice overs via SRS text-to-speech +-- +-- === +-- +-- ### 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 #string alias Radio alias, e.g. "Batumi Tower". +-- @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. +-- @field #table holdingpoints Holding points. +-- @field Sound.SRS#MSRS msrsTower Moose SRS wrapper. +-- @field Sound.SRS#MSRS msrsPilot Moose SRS wrapper. +-- @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. +-- +-- === +-- +-- # The FLIGHTCONTROL Concept +-- +-- This class implements an ATC for human and AI controlled aircraft. It gives permission for take-off and landing based on a sophisticated queueing system. +-- Therefore, it solves (or reduces) a lot of common problems with the DCS implementation (which is barly existing at this point). +-- +-- ## Prerequisites +-- +-- * SRS is used for radio communications +-- * +-- +-- ## Limitations +-- +-- Some (DCS) limitations you should be aware of: +-- +-- * As soon as AI aircraft taxi or land, we completely loose control. All is governed by the internal DCS AI logic. +-- * We have no control over the active runway or which runway is used by the AI if there are multiple. +-- * Only one player/client per group as we can create menus only for a group and not for a specific unit. +-- +-- +-- @field #FLIGHTCONTROL +FLIGHTCONTROL = { + ClassName = "FLIGHTCONTROL", + Debug = false, + 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, + holdingpoints = {}, +} + +--- Holding point. Contains holding stacks. +-- @type FLIGHTCONTROL.HoldingPoint +-- @field Core.Point#COORDINATE pos0 First position 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. +-- @field #table stacks Holding stacks. + +--- Holding stack. +-- @type FLIGHTCONTROL.HoldingStack +-- @field Ops.FlightGroup#FLIGHTGROUP flightgroup Flight group of this stack. +-- @field #number angels Holding altitude in Angels. +-- @field Core.Point#COORDINATE pos0 First position of racetrack holding point. +-- @field Core.Point#COORDINATE pos1 Second position of racetrack holding point. + +--- 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 + +--- Flight status. +-- @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 READYTX Flight is ready to taxi. +-- @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", + READYTX="Ready To Taxi", + 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: +-- TODO: Switch to enable/disable AI messages. +-- TODO: Improve TTS messages. +-- DONE: Add SRS TTS. +-- TODO: Define holding zone. +-- TODO: Add helos. +-- TODO: Talk me down option. +-- TODO: ATIS option. +-- TODO: ATC voice overs. +-- TODO: Check runways and clean up. +-- TODO: Accept and forbit parking spots. +-- DONE: Add parking guard. +-- NOGO: Add FARPS? +-- 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. +-- @param #number Frequency Radio frequency in MHz. Default 143.00 MHz. Can also be given as a `#table` of multiple frequencies. +-- @param #number Modulation Radio modulation: 0=AM (default), 1=FM. See `radio.modulation.AM` and `radio.modulation.FM` enumerators. Can also be given as a `#table` of multiple modulations. +-- @param #string PathToSRS Path to the directory, where SRS is located. +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:New(AirbaseName, Frequency, Modulation, PathToSRS) + + -- 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)) + + -- Set alias. + self.alias=self.airbasename.." Tower" + + -- Defaults: + self:SetLandingMax() + self:SetLandingInterval() + self:SetFrequency(Frequency, Modulation) + + -- SRS for Tower. + self.msrsTower=MSRS:New(PathToSRS, Frequency, Modulation) + self.msrsTower:SetLabel(self.alias) + + -- SRS for Pilot. + self.msrsPilot=MSRS:New(PathToSRS, Frequency, Modulation) + self.msrsPilot:SetGender("male") + self.msrsPilot:SetCulture("en-US") + self.msrsPilot:SetLabel("Pilot") + + -- 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 tower frequency. +-- @param #FLIGHTCONTROL self +-- @param #number Frequency Frequency in MHz. Default 305 MHz. +-- @param #number Modulation Modulation `radio.modulation.AM`=0, `radio.modulation.FM`=1. Default `radio.modulation.AM`. +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:SetFrequency(Frequency, Modulation) + + self.frequency=Frequency or 305 + self.modulation=Modulation or radio.modulation.AM + +end + + +--- 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 + +--- Add holding zone. +-- @param #FLIGHTCONTROL self +-- @param Core.Zone#ZONE ArrivalZone Zone where planes arrive. +-- @param #number Heading Heading in degrees. +-- @param #number Length Length in nautical miles. Default 15 NM. +-- @param #number FlightlevelMin Min flight level. +-- @param #number FlightlevelMax Max flight level. +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:AddHoldingPoint(ArrivalZone, Heading, Length, FlightlevelMin, FlightlevelMax) + + local hp={} --#FLIGHTCONTROL.HoldingPoint + + hp.arrivalzone=ArrivalZone + + hp.pos0=ArrivalZone:GetCoordinate() + + Length=UTILS.NMToMeters(Length or 15) + + hp.pos1=hp.pos0:Translate(Length, Heading) + + hp.pos0:ArrowToAll(hp.pos1, nil, {1,0,0}, 1, {1,1,0}, 0.5, 2, true) + + hp.angelsmin=FlightlevelMin or 5 + hp.angelsmax=FlightlevelMax or 10 + + hp.stacks={} + for i=hp.angelsmin, hp.angelsmax do + local stack={} --#FLIGHTCONTROL.HoldingStack + stack.angels=i + stack.flightgroup=nil + stack.pos0=UTILS.DeepCopy(hp.pos0) + stack.pos0:SetAltitude(UTILS.FeetToMeters(i*1000)) + stack.pos1=UTILS.DeepCopy(hp.pos1) + stack.pos1:SetAltitude(UTILS.FeetToMeters(i*1000)) + stack.heading=Heading + table.insert(hp.stacks, stack) + end + + table.insert(self.holdingpoints, hp) + + ArrivalZone:DrawZone() + + return self +end + + +--- Set the parking guard group. This group is used to block (AI) aircraft from taxiing until they get clearance. It should contain of only one unit, *e.g.* a simple soldier. +-- @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 + +--- Is flight in queue of this flightcontrol. +-- @param #FLIGHTCONTROL self +-- @param Ops.FlightGroup#FLIGHTGROUP Flight Flight group. +-- @return #boolean If `true`, flight is in queue. +function FLIGHTCONTROL:IsFlight(Flight) + + for _,_flight in pairs(self.flights) do + local flight=_flight --Ops.FlightGroup#FLIGHTGROUP + if flight.groupname==Flight.groupname then + return true + end + end + + return false +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) + + -- 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 NQreadytx=self:CountFlights(FLIGHTCONTROL.FlightStatus.READYTX) + 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+NQreadytx+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. + local text=string.format("State %s - Runway %s - Parking F=%d/O=%d/R=%d of %d - Flights=%s: Qpark=%d Qtxout=%d Qready=%d Qto=%d | Qinbound=%d Qhold=%d Qland=%d Qtxinb=%d Qarr=%d", + self:GetState(), runway.idx, Nfree, Noccu, Nresv, self.Nparkingspots, Nflights, NQparking, NQtaxiout, NQreadyto, NQtakeoff, NQinbound, NQholding, NQlanding, NQtaxiinb, NQarrived) + self:I(self.lid..text) + + 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 + + -- Next status update in ~30 seconds. + self:__Status(-30) +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 + + self:I(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 + + -- We delay this, to have all elements of the group in the game. + if unit:IsAir() then + + local bornhere=EventData.Place and EventData.Place:GetName()==self.airbasename or false + env.info("FF born here ".. tostring(bornhere)) + + -- We got a player? + local playerunit, playername=self:_GetPlayerUnitAndName(EventData.IniUnitName) + + if playername or bornhere then + + -- Create player menu. + self:ScheduleOnce(0.5, self._CreateFlightGroup, self, EventData.IniGroup) + + end + + -- Spawn parking guard. + if bornhere then + self:SpawnParkingGuard(unit) + end + + 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 +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check takeoff and landing queues. +-- @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 -- + -------------------- + + env.info("FF next flight holding") + + -- No other flight is taking off and number of landing flights is below threshold. + if ntakeoff==0 and nlanding=self.dTlanding then + + -- Get callsign. + local callsign=flight:GetCallsignName() + + -- Message. + local text=string.format("%s, %s, you are cleared to land.", callsign, self.alias) + + -- Transmit message. + self:TransmissionTower(text, flight) + + -- Give AI the landing signal. + if flight.isAI then + --TODO: transmit confirmation of AI + self:_LandAI(flight, parking) + else + -- TODO: Humans have to confirm via F10 menu. + 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 + + -- Get callsign. + local callsign=flight:GetCallsignName() + + -- Runway. + local runway=self:GetActiveRunwayText() + + -- Message. + local text=string.format("%s, %s, taxi to holding point, runway %s", callsign, self.alias, runway) + + if self:GetFlightStatus(flight)==FLIGHTCONTROL.FlightStatus.READYTO then + text=string.format("%s, %s, cleared for take-off, runway %s, hold short", callsign, self.alias, runway) + end + + -- Transmit message. + self:TransmissionTower(text, flight) + + -- Check if flight is AI. Humans have to request taxi via F10 menu. + if flight.isAI then + + --- + -- AI + --- + + --TODO: AI answer. + + -- Start uncontrolled aircraft. + if flight:IsUncontrolled() then + flight:StartUncontrolled() + end + + -- 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 + + -- Set flight to takeoff. No way we can stop the AI now. + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.TAKEOFF) + + else + + --- + -- PLAYER + --- + + if self:GetFlightStatus(flight)==FLIGHTCONTROL.FlightStatus.READYTO then + + -- Player is ready for takeoff + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.TAKEOFF) + + else + + -- Remove parking guards. + for _,_element in pairs(flight.elements) do + local element=_element --Ops.FlightGroup#FLIGHTGROUP.Element + if element.parking then + local spot=self:GetParkingSpotByID(element.parking.TerminalID) + if element.ai then + self:RemoveParkingGuard(spot, 35) + else + self:RemoveParkingGuard(spot, 10) + end + end + end + + end + + end + + else + -- Debug message. + self:I(self.lid..string.format("FYI: Take off for flight %s denied as other flights are taking off (N=%d) or landing (N=%d).", flight.groupname, ntakeoff, nlanding)) + end + end + else + -- Debug message. + 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 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 + self:T(self.lid..string.format("Next flight that is not holding")) + 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 + -- First come, first serve. + return QreadyTO[1] + end + + -- Get flights ready to taxi. + local QreadyTX=self:GetFlights(FLIGHTCONTROL.FlightStatus.READYTX, OPSGROUP.GroupStatus.PARKING) + + -- First check human players. + if #QreadyTX>0 then + -- First come, first serve. + return QreadyTX[1] + end + + -- Get AI flights parking. + local Qparking=self:GetFlights(FLIGHTCONTROL.FlightStatus.PARKING, nil, true) + + local Nparking=#Qparking + + -- Check special cases where only up to one flight is waiting for takeoff. + if Nparking==0 then + return nil + elseif Nparking==1 then + return Qparking[1] + 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. +function FLIGHTCONTROL:SetFlightStatus(flight, status) + + -- Debug message. + self:T(self.lid..string.format("New status %s-->%s for flight %s", flight.controlstatus or "unknown", status, flight:GetName())) + + -- Set new status + flight.controlstatus=status + +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 +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 flightcontrol status, e.g. `FLIGHTCONTROL.Status.XXX`. +-- @param #string GroupStatus Return only flights in this FSM status, e.g. `OPSGROUP.GroupStatus.TAXIING`. +-- @param #boolean AI If `true` only AI flights are returned. If `false`, only flights with clients are returned. If `nil` (default), all flights are returned. +-- @return #table Table of flights. +function FLIGHTCONTROL:GetFlights(Status, GroupStatus, AI) + + if Status~=nil or GroupStatus~=nil or AI~=nil 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 + if AI==nil or AI==flight.isAI then + if GroupStatus==nil or GroupStatus==flight:GetState() then + table.insert(flights, flight) + end + end + 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. +-- @param #string GroupStatus Count only flights in this FSM status, e.g. `OPSGROUP.GroupStatus.TAXIING`. +-- @param #boolean AI If `true` only AI flights are counted. If `false`, only flights with clients are counted. If `nil` (default), all flights are counted. +-- @return #number Number of flights. +function FLIGHTCONTROL:CountFlights(Status, GroupStatus, AI) + + if Status~=nil or GroupStatus~=nil or AI~=nil then + + local flights=self:GetFlights(Status, GroupStatus, AI) + + 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 + + -- Set parking occupied. + self:SetParkingOccupied(spot, unitname) + + -- Spawn parking guard. + 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) + + 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) + + 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) + + 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 + + self:_CreatePlayerMenu(flight, flight.menu.atc) + + 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) + + -- Get flight group. + local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP + + if flight then + + MESSAGE:New("Abort takeoff", 5):ToAll() + + -- Flight status. + local status=self:GetFlightStatus(flight) + + if status==FLIGHTCONTROL.FlightStatus.TAKEOFF or status==FLIGHTCONTROL.FlightStatus.READYTO then + + if flight:IsParking() then + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.PARKING) + elseif flight:IsTaxiing() then + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.TAXIOUT) + else + env.info(self.lid.."ERROR") + end + + self:_CreatePlayerMenu(flight, flight.menu.atc) + + + else + MESSAGE:New("Negative, You are NOT in the takeoff queue", 5):ToAll() + end + + else + --TODO: Error message. + 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 + + 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.HoldingStack 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) + + ]] + + for i,_hp in pairs(self.holdingpoints) do + local holdingpoint=_hp --#FLIGHTCONTROL.HoldingPoint + + for j,_stack in pairs(holdingpoint.stacks) do + local stack=_stack --#FLIGHTCONTROL.HoldingStack + if not stack.flightgroup then + return stack + end + end + + end + + return nil +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:Transmission2(sound, interval, subtitle, path) + self.radioqueue:NewTransmission(sound.filename, sound.duration, path or self.soundpath, nil, interval, subtitle, self.subduration) +end + + +--- Radio transmission from tower. +-- @param #FLIGHTCONTROL self +-- @param #string Text The text to transmit. +-- @param Ops.FlightGroup#FLIGHTGROUP Flight The flight. +-- @param #number Delay Delay in seconds before the text is transmitted. Default 0 sec. +function FLIGHTCONTROL:TransmissionTower(Text, Flight, Delay) + + self.msrsTower:PlayText(Text, Delay) + + if Flight and not Flight.isAI then + self:TextMessageToFlight(Text, Flight, 5, false, Delay) + end + + -- Debug message. + self:T(self.lid..string.format("Radio Tower: %s", Text)) + +end + +--- Radio transmission. +-- @param #FLIGHTCONTROL self +-- @param #string Text The text to transmit. +-- @param Ops.FlightGroup#FLIGHTGROUP Flight The flight. +-- @param #number Delay Delay in seconds before the text is transmitted. Default 0 sec. +function FLIGHTCONTROL:TransmissionPilot(Text, Flight, Delay) + + self.msrsPilot:PlayText(Text, Delay) + + if Flight and not Flight.isAI then + self:TextMessageToFlight(Text, Flight, 5, false, Delay) + end + + + -- Debug message. + self:T(self.lid..string.format("Radio Pilot: %s", Text)) + +end + + +--- Text message to group. +-- @param #FLIGHTCONTROL self +-- @param #string Text The text to transmit. +-- @param Ops.FlightGroup#FLIGHTGROUP Flight The flight. +-- @param #number Duration Duration in seconds. Default 5. +-- @param #boolean Clear Clear screen. +-- @param #number Delay Delay in seconds before the text is transmitted. Default 0 sec. +function FLIGHTCONTROL:TextMessageToFlight(Text, Flight, Duration, Clear, Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, FLIGHTCONTROL.TextMessageToFlight, self, Text, Flight, Duration, Clear, 0) + else + + if Flight and Flight.group and Flight.group:IsAlive() then + + -- Group ID. + local gid=Flight.group:GetID() + + -- Out text. + trigger.action.outTextForGroup(gid, self:_CleanText(Text), Duration or 5, Clear) + + end + + end + +end + +--- Clean text. Remove control sequences. +-- @param #FLIGHTCONTROL self +-- @param #string Text The text. +-- @param #string Cleaned text. +function FLIGHTCONTROL:_CleanText(Text) + + local text=Text:gsub("\n$",""):gsub("\n$","") + + return text +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) + + 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 + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index c3914282f..33715404b 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -54,6 +54,7 @@ -- @field #boolean despawnAfterLanding If `true`, group is despawned after landed at an airbase. -- @field #boolean despawnAfterHolding If `true`, group is despawned after reaching the holding point. -- @field #number RTBRecallCount Number that counts RTB calls. +-- @field Ops.FlightControl#FLIGHTCONTROL.HoldingStack stack Holding stack. -- -- @extends Ops.OpsGroup#OPSGROUP @@ -379,7 +380,9 @@ function FLIGHTGROUP:SetFlightControl(flightcontrol) self.flightcontrol=flightcontrol -- Add flight to all flights. - table.insert(flightcontrol.flights, self) + if not flightcontrol:IsFlight(self) then + table.insert(flightcontrol.flights, self) + end -- Update flight's F10 menu. if self.isAI==false then @@ -799,11 +802,13 @@ function FLIGHTGROUP:Status() -- Get distance to assigned parking spot. local dist=element.unit:GetCoordinate():Get2DDistance(element.parking.Coordinate) + + env.info(string.format("FF dist to parking spot %d = %.1f meters", element.parking.TerminalID, dist)) -- If distance >10 meters, we consider the unit as taxiing. -- TODO: Check distance threshold! If element is taxiing, the parking spot is free again. -- When the next plane is spawned on this spot, collisions should be avoided! - if dist>10 then + if dist>5 then if element.status==OPSGROUP.ElementStatus.ENGINEON then self:ElementTaxiing(element) end @@ -1582,10 +1587,20 @@ function FLIGHTGROUP:onafterSpawned(From, Event, To) else env.info("FF Spawned update menu") - - -- F10 other menu. - self:_UpdateMenu() - + + -- Set flightcontrol. + if self.currbase then + local flightcontrol=_DATABASE:GetFlightControl(self.currbase:GetName()) + if flightcontrol then + self:SetFlightControl(flightcontrol) + else + -- F10 other menu. + self:_UpdateMenu() + end + else + self:_UpdateMenu() + end + end end @@ -1620,6 +1635,8 @@ function FLIGHTGROUP:onafterParking(From, Event, To) local flightcontrol=_DATABASE:GetFlightControl(airbasename) if flightcontrol then + + env.info("FF flight control!") -- Set FC for this flight self:SetFlightControl(flightcontrol) @@ -1635,6 +1652,9 @@ function FLIGHTGROUP:onafterParking(From, Event, To) end end + + else + env.info("FF no flight control!") end end @@ -2517,15 +2537,22 @@ function FLIGHTGROUP:_LandAtAirbase(airbase, SpeedTo, SpeedHold, SpeedLand) -- 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 false then - p0:MarkToAll("Holding point P0") - p1:MarkToAll("Holding point P1") + + if HoldingPoint then + + -- Race track points. + p0=HoldingPoint.pos0 + p1=HoldingPoint.pos1 + + -- Debug marks. + if true then + p0:MarkToAll(string.format("%s: Holding point P0, alt=%d meters", self:GetName(), p0.y)) + p1:MarkToAll(string.format("%s: Holding point P1, alt=%d meters", self:GetName(), p0.y)) + end + end -- Set flightcontrol for this flight. diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index c2bb3b5b1..2a5f4178e 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -205,6 +205,7 @@ OPSGROUP = { -- @field DCS#Controller controller The DCS controller of the unit. -- @field #boolean ai If true, element is AI. -- @field #string skill Skill level. +-- @field #string playerName Name of player if this is a client. -- -- @field Core.Zone#ZONE_POLYGON_BASE zoneBoundingbox Bounding box zone of the element unit. -- @field Core.Zone#ZONE_POLYGON_BASE zoneLoad Loading zone. @@ -270,6 +271,28 @@ OPSGROUP.ElementStatus={ DEAD="dead", } +--- Status of group. +-- @type OPSGROUP.GroupStatus +-- @field #string INUTERO Not spawned yet or its status is unknown so far. +-- @field #string PARKING Parking after spawned on ramp. +-- @field #string TAXIING Taxiing after engine startup. +-- @field #string AIRBORNE Element is airborne. Either after takeoff or after air start. +-- @field #string LANDING Landing. +-- @field #string LANDED Landed and is taxiing to its parking spot. +-- @field #string ARRIVED 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.GroupStatus={ + INUTERO="InUtero", + PARKING="Parking", + TAXIING="Taxiing", + AIRBORNE="Airborne", + INBOUND="Inbound", + LANDING="Landing", + LANDED="Landed", + ARRIVED="Arrived", + DEAD="Dead", +} + --- Ops group task status. -- @type OPSGROUP.TaskStatus -- @field #string SCHEDULED Task is scheduled. diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index 4ade13812..0523f12be 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -96,6 +96,7 @@ Ops/Chief.lua Ops/CSAR.lua Ops/CTLD.lua Ops/Awacs.lua +Ops/FlightControl.lua AI/AI_Balancer.lua AI/AI_Air.lua