diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index 9ccb7ebb5..bac21d429 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -85,8 +85,7 @@ __Moose.Include( 'Scripts/Moose/Ops/Squadron.lua' ) __Moose.Include( 'Scripts/Moose/Ops/AirWing.lua' ) __Moose.Include( 'Scripts/Moose/Ops/Intelligence.lua' ) __Moose.Include( 'Scripts/Moose/Ops/ChiefOfStaff.lua' ) -__Moose.Include( 'Scripts/Moose/Ops/Army.lua' ) -__Moose.Include( 'Scripts/Moose/Ops/Platoon.lua' ) +__Moose.Include( 'Scripts/Moose/Ops/FlightControl.lua' ) __Moose.Include( 'Scripts/Moose/Ops/OpsTransport.lua' ) __Moose.Include( 'Scripts/Moose/Ops/CSAR.lua' ) __Moose.Include( 'Scripts/Moose/Ops/CTLD.lua' ) diff --git a/Moose Development/Moose/Ops/Army.lua b/Moose Development/Moose/Ops/Army.lua deleted file mode 100644 index 884a6ed22..000000000 --- a/Moose Development/Moose/Ops/Army.lua +++ /dev/null @@ -1,434 +0,0 @@ ---- **Ops** - Army Warehouse. --- --- **Main Features:** --- --- * Manage platoons --- --- === --- --- ### Author: **funkyfranky** --- @module Ops.Army --- @image OPS_AirWing.png - - ---- ARMY class. --- @type ARMY --- @field #string ClassName Name of the class. --- @field #number verbose Verbosity of output. --- @field #string lid Class id string for output to DCS log file. --- @field #table menu Table of menu items. --- @field #table squadrons Table of squadrons. --- @field #table missionqueue Mission queue table. --- @field #table payloads Playloads for specific aircraft and mission types. --- @field #number payloadcounter Running index of payloads. --- @field Core.Set#SET_ZONE zonesetCAP Set of CAP zones. --- @field Core.Set#SET_ZONE zonesetTANKER Set of TANKER zones. --- @field Core.Set#SET_ZONE zonesetAWACS Set of AWACS zones. --- @field #number nflightsCAP Number of CAP flights constantly in the air. --- @field #number nflightsAWACS Number of AWACS flights constantly in the air. --- @field #number nflightsTANKERboom Number of TANKER flights with BOOM constantly in the air. --- @field #number nflightsTANKERprobe Number of TANKER flights with PROBE constantly in the air. --- @field #number nflightsRescueHelo Number of Rescue helo flights constantly in the air. --- @field #table pointsCAP Table of CAP points. --- @field #table pointsTANKER Table of Tanker points. --- @field #table pointsAWACS Table of AWACS points. --- @field Ops.WingCommander#WINGCOMMANDER wingcommander The wing commander responsible for this airwing. --- --- @field Ops.RescueHelo#RESCUEHELO rescuehelo The rescue helo. --- @field Ops.RecoveryTanker#RECOVERYTANKER recoverytanker The recoverytanker. --- --- @extends Functional.Warehouse#WAREHOUSE - ---- Be surprised! --- --- === --- --- ![Banner Image](..\Presentations\OPS\AirWing\_Main.png) --- --- # The ARMY Concept --- --- An ARMY consists of multiple SQUADRONS. These squadrons "live" in a WAREHOUSE, i.e. a physical structure that is connected to an airbase (airdrome, FRAP or ship). --- For an airwing to be operational, it needs airframes, weapons/fuel and an airbase. --- --- # Create an Army --- --- ## Constructing the Army --- --- airwing=ARMY:New("Warehouse Batumi", "8th Fighter Wing") --- airwing:Start() --- --- The first parameter specified the warehouse, i.e. the static building housing the airwing (or the name of the aircraft carrier). The second parameter is optional --- and sets an alias. --- --- ## Adding Squadrons --- --- At this point the airwing does not have any assets (aircraft). In order to add these, one needs to first define SQUADRONS. --- --- VFA151=SQUADRON:New("F-14 Group", 8, "VFA-151 (Vigilantes)") --- VFA151:AddMissionCapability({AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT}) --- --- airwing:AddSquadron(VFA151) --- --- This adds eight Tomcat groups beloning to VFA-151 to the airwing. This squadron has the ability to perform combat air patrols and intercepts. --- --- ## Adding Payloads --- --- Adding pure airframes is not enough. The aircraft also need weapons (and fuel) for certain missions. These must be given to the airwing from template groups --- defined in the Mission Editor. --- --- -- F-14 payloads for CAP and INTERCEPT. Phoenix are first, sparrows are second choice. --- airwing:NewPayload(GROUP:FindByName("F-14 Payload AIM-54C"), 2, {AUFTRAG.Type.INTERCEPT, AUFTRAG.Type.GCICAP}, 80) --- airwing:NewPayload(GROUP:FindByName("F-14 Payload AIM-7M"), 20, {AUFTRAG.Type.INTERCEPT, AUFTRAG.Type.GCICAP}) --- --- This will add two AIM-54C and 20 AIM-7M payloads. --- --- If the airwing gets an intercept or patrol mission assigned, it will first use the AIM-54s. Once these are consumed, the AIM-7s are attached to the aircraft. --- --- When an airwing does not have a payload for a certain mission type, the mission cannot be carried out. --- --- You can set the number of payloads to "unlimited" by setting its quantity to -1. --- --- # Adding Missions --- --- Various mission types can be added easily via the AUFTRAG class. --- --- Once you created an AUFTRAG you can add it to the ARMY with the :AddMission(mission) function. --- --- This mission will be put into the ARMY queue. Once the mission start time is reached and all resources (airframes and pylons) are available, the mission is started. --- If the mission stop time is over (and the mission is not finished), it will be cancelled and removed from the queue. This applies also to mission that were not even --- started. --- --- # Command an Army --- --- An airwing can receive missions from a WINGCOMMANDER. See docs of that class for details. --- --- However, you are still free to add missions at anytime. --- --- --- @field #ARMY -ARMY = { - ClassName = "ARMY", - verbose = 0, - lid = nil, - menu = nil, - squadrons = {}, - missionqueue = {}, - payloads = {}, - payloadcounter = 0, - pointsCAP = {}, - pointsTANKER = {}, - pointsAWACS = {}, - wingcommander = nil, -} - ---- Squadron asset. --- @type ARMY.SquadronAsset --- @field #ARMY.Payload payload The payload of the asset. --- @field Ops.FlightGroup#FLIGHTGROUP flightgroup The flightgroup object. --- @field #string squadname Name of the squadron this asset belongs to. --- @field #number Treturned Time stamp when asset returned to the airwing. --- @extends Functional.Warehouse#WAREHOUSE.Assetitem - ---- ARMY class version. --- @field #string version -ARMY.version="0.0.1" - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- ToDo list -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - --- TODO: A lot! - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Constructor -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Create a new ARMY class object. --- @param #ARMY self --- @param #string warehousename Name of the warehouse static or unit object representing the warehouse. --- @param #string airwingname Name of the air wing, e.g. "ARMY-8". --- @return #ARMY self -function ARMY:New(warehousename, airwingname) - - -- Inherit everything from WAREHOUSE class. - local self=BASE:Inherit(self, WAREHOUSE:New(warehousename, airwingname)) -- #ARMY - - -- Nil check. - if not self then - BASE:E(string.format("ERROR: Could not find warehouse %s!", warehousename)) - return nil - end - - -- Set some string id for output to DCS.log file. - self.lid=string.format("ARMY %s | ", self.alias) - - -- Add FSM transitions. - -- From State --> Event --> To State - self:AddTransition("*", "MissionRequest", "*") -- Add a (mission) request to the warehouse. - self:AddTransition("*", "MissionCancel", "*") -- Cancel mission. - - ------------------------ - --- Pseudo Functions --- - ------------------------ - - --- Triggers the FSM event "Start". Starts the ARMY. Initializes parameters and starts event handlers. - -- @function [parent=#ARMY] Start - -- @param #ARMY self - - --- Triggers the FSM event "Start" after a delay. Starts the ARMY. Initializes parameters and starts event handlers. - -- @function [parent=#ARMY] __Start - -- @param #ARMY self - -- @param #number delay Delay in seconds. - - --- Triggers the FSM event "Stop". Stops the ARMY and all its event handlers. - -- @param #ARMY self - - --- Triggers the FSM event "Stop" after a delay. Stops the ARMY and all its event handlers. - -- @function [parent=#ARMY] __Stop - -- @param #ARMY self - -- @param #number delay Delay in seconds. - - return self -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- User Functions -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Start & Status -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Start AIRWING FSM. --- @param #AIRWING self -function AIRWING:onafterStart(From, Event, To) - - -- Start parent Warehouse. - self:GetParent(self).onafterStart(self, From, Event, To) - - -- Info. - self:I(self.lid..string.format("Starting AIRWING v%s", AIRWING.version)) - -end - ---- Update status. --- @param #AIRWING self -function AIRWING:onafterStatus(From, Event, To) - - -- Status of parent Warehouse. - self:GetParent(self).onafterStatus(self, From, Event, To) - - local fsmstate=self:GetState() - - - -- General info: - if self.verbose>=1 then - - -- Count missions not over yet. - local Nmissions=self:CountMissionsInQueue() - - -- Assets tot - local Npq, Np, Nq=self:CountAssetsOnMission() - - local assets=string.format("%d (OnMission: Total=%d, Active=%d, Queued=%d)", self:CountAssets(), Npq, Np, Nq) - - -- Output. - local text=string.format("%s: Missions=%d, Payloads=%d (%d), Squads=%d, Assets=%s", fsmstate, Nmissions, Npayloads, #self.payloads, #self.squadrons, assets) - self:I(self.lid..text) - end - - ------------------ - -- Mission Info -- - ------------------ - if self.verbose>=2 then - local text=string.format("Missions Total=%d:", #self.missionqueue) - for i,_mission in pairs(self.missionqueue) do - local mission=_mission --Ops.Auftrag#AUFTRAG - - local prio=string.format("%d/%s", mission.prio, tostring(mission.importance)) ; if mission.urgent then prio=prio.." (!)" end - local assets=string.format("%d/%d", mission:CountOpsGroups(), mission.nassets) - local target=string.format("%d/%d Damage=%.1f", mission:CountMissionTargets(), mission:GetTargetInitialNumber(), mission:GetTargetDamage()) - - text=text..string.format("\n[%d] %s %s: Status=%s, Prio=%s, Assets=%s, Targets=%s", i, mission.name, mission.type, mission.status, prio, assets, target) - end - self:I(self.lid..text) - end - - ------------------- - -- Squadron Info -- - ------------------- - if self.verbose>=3 then - local text="Squadrons:" - for i,_squadron in pairs(self.squadrons) do - local squadron=_squadron --Ops.Squadron#SQUADRON - - local callsign=squadron.callsignName and UTILS.GetCallsignName(squadron.callsignName) or "N/A" - local modex=squadron.modex and squadron.modex or -1 - local skill=squadron.skill and tostring(squadron.skill) or "N/A" - - -- Squadron text - text=text..string.format("\n* %s %s: %s*%d/%d, Callsign=%s, Modex=%d, Skill=%s", squadron.name, squadron:GetState(), squadron.aircrafttype, squadron:CountAssetsInStock(), #squadron.assets, callsign, modex, skill) - end - self:I(self.lid..text) - end - - -------------- - -- Mission --- - -------------- - - -- Check if any missions should be cancelled. - self:_CheckMissions() - - -- Get next mission. - local mission=self:_GetNextMission() - - -- Request mission execution. - if mission then - self:MissionRequest(mission) - end - -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Stuff -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Check if mission is not over and ready to cancel. --- @param #AIRWING self -function AIRWING:_CheckMissions() - - -- Loop over missions in queue. - for _,_mission in pairs(self.missionqueue) do - local mission=_mission --Ops.Auftrag#AUFTRAG - - if mission:IsNotOver() and mission:IsReadyToCancel() then - mission:Cancel() - end - end - -end ---- Get next mission. --- @param #AIRWING self --- @return Ops.Auftrag#AUFTRAG Next mission or *nil*. -function AIRWING:_GetNextMission() - - -- Number of missions. - local Nmissions=#self.missionqueue - - -- Treat special cases. - if Nmissions==0 then - return nil - end - - -- Sort results table wrt prio and start time. - local function _sort(a, b) - local taskA=a --Ops.Auftrag#AUFTRAG - local taskB=b --Ops.Auftrag#AUFTRAG - return (taskA.prio0 then - self:E(self.lid..string.format("ERROR: mission %s of type %s has already assets attached!", mission.name, mission.type)) - end - mission.assets={} - - -- Assign assets to mission. - for i=1,mission.nassets do - local asset=assets[i] --#AIRWING.SquadronAsset - - -- Should not happen as we just checked! - if not asset.payload then - self:E(self.lid.."ERROR: No payload for asset! This should not happen!") - end - - -- Add asset to mission. - mission:AddAsset(asset) - end - - -- Now return the remaining payloads. - for i=mission.nassets+1,#assets do - local asset=assets[i] --#AIRWING.SquadronAsset - for _,uid in pairs(gotpayload) do - if uid==asset.uid then - self:ReturnPayloadFromAsset(asset) - break - end - end - end - - return mission - end - - end -- mission due? - end -- mission loop - - return nil -end \ No newline at end of file diff --git a/Moose Development/Moose/Ops/FlightControl.lua b/Moose Development/Moose/Ops/FlightControl.lua new file mode 100644 index 000000000..ded207415 --- /dev/null +++ b/Moose Development/Moose/Ops/FlightControl.lua @@ -0,0 +1,2209 @@ +--- **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 #boolean Debug Debug mode. Messages to all about status. +-- @field #string theatre The DCS map used in the mission. +-- @field #string lid Class id string for output to DCS log file. +-- @field #string airbasename Name of airbase. +-- @field #number airbasetype Type of airbase. +-- @field Wrapper.Airbase#AIRBASE airbase Airbase object. +-- @field Core.Zone#ZONE zoneAirbase Zone around the airbase. +-- @field #table parking Parking spots table. +-- @field #table runways Runway table. +-- @field #table 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", + 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, +} + +--- 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.4.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +-- +-- TODO: Define holding zone +-- TODO: +-- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- 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. + + -- Debug trace. + if false then + self.Debug=true + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(1) + end + + -- 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. + 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(-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 + + 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 + + self:ScheduleOnce(0.5, self._CreateFlightGroup, self, EventData.IniGroup) + + end + + 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 +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- 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.ai 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.ai 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.ai 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) + + 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 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) + + 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 + + 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:GetFlightGroup(group:GetName()) + + -- If it does not exist yet, create one. + if not flight then + flight=FLIGHTGROUP:New(group:GetName()) + end + + --if flight.destination and flight.destination:GetName()==self.airbasename then + 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.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 + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Platoon.lua b/Moose Development/Moose/Ops/Platoon.lua deleted file mode 100644 index e69de29bb..000000000 diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index 13c6da66a..ae1a85fc9 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -83,6 +83,7 @@ Ops/AirWing.lua Ops/Intelligence.lua Ops/WingCommander.lua Ops/ChiefOfStaff.lua +Ops/FlightControl.lua Ops/CSAR.lua Ops/CTLD.lua