From ddcc851951e45c6d055f04ef55f26bda62650a26 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 12 May 2022 08:03:08 +0200 Subject: [PATCH 01/20] 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 From 97a4b79713060afe0362288b2bd708edc4cbd432 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 12 May 2022 21:19:55 +0200 Subject: [PATCH 02/20] OPS FC - Improved holding point --- Moose Development/Moose/Ops/Auftrag.lua | 11 +- Moose Development/Moose/Ops/FlightControl.lua | 130 +++++++++++++----- Moose Development/Moose/Ops/FlightGroup.lua | 18 ++- 3 files changed, 120 insertions(+), 39 deletions(-) diff --git a/Moose Development/Moose/Ops/Auftrag.lua b/Moose Development/Moose/Ops/Auftrag.lua index 430a032b0..c956636a9 100644 --- a/Moose Development/Moose/Ops/Auftrag.lua +++ b/Moose Development/Moose/Ops/Auftrag.lua @@ -2505,8 +2505,17 @@ function AUFTRAG:GetRequiredAssets(Legion) --if Legion and self.Nassets[Legion.alias] then -- N=self.Nassets[Legion.alias] --end + + local Nmin=self.NassetsMin + local Nmax=self.NassetsMax + + if self.type==AUFTRAG.Type.RELOCATECOHORT then + local cohort=self.DCStask.params.cohort --Ops.Cohort#COHORT + Nmin=#cohort.assets + Nmax=Nmin + end - return self.NassetsMin, self.NassetsMax + return Nmin, Nmax end --- **[LEGION, COMMANDER, CHIEF]** Define how many assets are required that escort the mission assets. diff --git a/Moose Development/Moose/Ops/FlightControl.lua b/Moose Development/Moose/Ops/FlightControl.lua index a38f6fe0d..736dbbf14 100644 --- a/Moose Development/Moose/Ops/FlightControl.lua +++ b/Moose Development/Moose/Ops/FlightControl.lua @@ -32,13 +32,14 @@ -- @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 frequency ATC radio frequency in MHz. +-- @field #number modulation ATC radio modulation, *e.g.* `radio.modulation.AM`. -- @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 #number hpcounter Counter for holding zones. -- @field Sound.SRS#MSRS msrsTower Moose SRS wrapper. -- @field Sound.SRS#MSRS msrsPilot Moose SRS wrapper. -- @extends Core.Fsm#FSM @@ -66,6 +67,7 @@ -- * 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. +-- * Only FLIGHTGROUPS are controlled. -- -- -- @field #FLIGHTCONTROL @@ -91,10 +93,14 @@ FLIGHTCONTROL = { dTlanding = nil, Nparkingspots = nil, holdingpoints = {}, + hpcounter = 0, } --- Holding point. Contains holding stacks. -- @type FLIGHTCONTROL.HoldingPoint +-- @field Core.Zone#ZONE arrivalzone Zone where aircraft should arrive. +-- @field #number uid Unique ID. +-- @field #string name Name of the zone, which is -. -- @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. @@ -107,6 +113,7 @@ FLIGHTCONTROL = { -- @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. +-- @field #number heading Heading. --- Player menu data. -- @type FLIGHTCONTROL.PlayerMenu @@ -149,18 +156,6 @@ FLIGHTCONTROL.FlightStatus={ -- @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" @@ -169,10 +164,9 @@ FLIGHTCONTROL.version="0.5.0" -- TODO list -- --- TODO: +-- TODO: Support airwings. Dont give clearance for Alert5 or if mission has not started. -- 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. @@ -180,6 +174,7 @@ FLIGHTCONTROL.version="0.5.0" -- TODO: ATC voice overs. -- TODO: Check runways and clean up. -- TODO: Accept and forbit parking spots. +-- DONE: Add SRS TTS. -- DONE: Add parking guard. -- NOGO: Add FARPS? -- DONE: Interface with FLIGHTGROUP. @@ -354,6 +349,12 @@ function FLIGHTCONTROL:AddHoldingPoint(ArrivalZone, Heading, Length, Flightlevel hp.arrivalzone=ArrivalZone + self.hpcounter=self.hpcounter+1 + + hp.uid=self.hpcounter + + hp.name=string.format("%s-%d", ArrivalZone:GetName(), hp.uid) + hp.pos0=ArrivalZone:GetCoordinate() Length=UTILS.NMToMeters(Length or 15) @@ -1859,7 +1860,7 @@ end --- Player calls inbound. -- @param #FLIGHTCONTROL self -- @param #string groupname Name of the flight group. -function FLIGHTCONTROL:_PlayerInbound(groupname) +function FLIGHTCONTROL:_PlayerRequestInbound(groupname) -- Get flight group. local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP @@ -1877,6 +1878,15 @@ function FLIGHTCONTROL:_PlayerInbound(groupname) end + -- Call sign. + local callsign=flight:GetCallsignName() + + -- Pilot calls inbound for landing. + local text=string.format("%s, %s, inbound for landing", self.alias, callsign) + + -- Radio message. + self:TransmissionPilot(text, flight) + -- Distance from player to airbase. local dist=flight:GetCoordinate():Get2DDistance(self:GetCoordinate()) @@ -1889,12 +1899,12 @@ function FLIGHTCONTROL:_PlayerInbound(groupname) self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.INBOUND) -- Get holding point. - local stack=self:_GetHoldingpoint(flight) + local stack=self:_GetHoldingpoint(flight) if stack then -- Set flight. - stack.flightgroup=self + stack.flightgroup=flight -- Stack. flight.stack=stack @@ -1911,10 +1921,11 @@ function FLIGHTCONTROL:_PlayerInbound(groupname) local dist=UTILS.MetersToNM(distance) -- Message text. - local text=string.format("Roger, Fly heading %03d for %d nautical miles and report status when entering the holding pattern", heading, dist) + local text=string.format("%s, %s, roger, fly heading %03d for %d nautical miles, hold at angels %d. Report status when entering the pattern", + callsign, self.alias, heading, dist, stack.angels) -- Send message. - self:TransmissionTower(text, flight) + self:TransmissionTower(text, flight, 15) else self:E(self.lid..string.format("WARNING: Could not get holding stack for flight %s", flight:GetName())) @@ -1926,7 +1937,7 @@ function FLIGHTCONTROL:_PlayerInbound(groupname) local text=string.format("Negative, you have to be withing 50 nautical miles of the airbase to request inbound!") -- Send message. - self:TransmissionTower(text) + self:TextMessageToFlight(text, flight, 10) end @@ -1935,7 +1946,56 @@ function FLIGHTCONTROL:_PlayerInbound(groupname) local text=string.format("Negative, you must be AIRBORNE to call INBOUND!") -- Send message. - self:TransmissionTower(text) + self:TextMessageToFlight(text, flight, 10) + end + + else + MESSAGE:New(string.format("Cannot find flight group %s.", tostring(groupname)), 5):ToAll() + end + +end + +--- Player calls inbound. +-- @param #FLIGHTCONTROL self +-- @param #string groupname Name of the flight group. +function FLIGHTCONTROL:_PlayerAbortInbound(groupname) + + -- Get flight group. + local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP + + if flight then + + if flight:IsInbound() and self:IsControlling(flight) then + + -- Call sign. + local callsign=flight:GetCallsignName() + + -- Pilot calls inbound for landing. + local text=string.format("%s, %s, abort inbound", self.alias, callsign) + + -- Radio message. + self:TransmissionPilot(text, flight) + + -- Add flight to inbound queue. + self:_RemoveFlight(flight) + + -- Set flight. + flight.stack.flightgroup=nil + flight.stack=nil + + -- Message text. + local text=string.format("%s, %s, roger, have a nice day!", callsign, self.alias) + + -- Send message. + self:TransmissionTower(text, flight, 15) + + else + + -- Error you are not airborne! + local text=string.format("Negative, you must be INBOUND and CONTROLLED by us!") + + -- Send message. + self:TextMessageToFlight(text, flight, 10) end else @@ -2273,7 +2333,7 @@ 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 Ops.OpsGroup#OPSGROUP.Element 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) @@ -2292,7 +2352,7 @@ function FLIGHTCONTROL:_GetFlightElement(unitname) -- Loop over all elements in flight group. for i,_element in pairs(flight.elements) do - local element=_element --#FLIGHTCONTROL.FlightElement + local element=_element --Ops.OpsGroup#OPSGROUP.Element if element.unit:GetName()==unitname then return element, i, flight @@ -2448,11 +2508,18 @@ function FLIGHTCONTROL:_GetHoldingpoint(flight) ]] + -- Debug message. + self:T(self.lid..string.format("Getting holding point for flight %s", flight:GetName())) + for i,_hp in pairs(self.holdingpoints) do local holdingpoint=_hp --#FLIGHTCONTROL.HoldingPoint - + + self:T(self.lid..string.format("Checking holding point %s", holdingpoint.name)) + for j,_stack in pairs(holdingpoint.stacks) do local stack=_stack --#FLIGHTCONTROL.HoldingStack + local name=stack.flightgroup and stack.flightgroup:GetName() or "empty" + self:T(self.lid..string.format("Stack %d: %s", j, name)) if not stack.flightgroup then return stack end @@ -2467,17 +2534,6 @@ 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. diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index 55ebe0f3c..3433c019f 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -713,8 +713,17 @@ function FLIGHTGROUP:ClearToLand(Delay) else if self:IsHolding() then + + -- Set flag. self:T(self.lid..string.format("Clear to land ==> setting holding flag to 1 (true)")) self.flaghold:Set(1) + + -- Clear holding stack. + if self.stack then + self.stack.flightgroup=nil + self.stack=nil + end + end end @@ -2536,12 +2545,16 @@ 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) - if HoldingPoint then + if HoldingPoint then + + HoldingPoint.flightgroup=self + self.stack=HoldingPoint -- Race track points. p0=HoldingPoint.pos0 @@ -2553,6 +2566,8 @@ function FLIGHTGROUP:_LandAtAirbase(airbase, SpeedTo, SpeedHold, SpeedLand) p1:MarkToAll(string.format("%s: Holding point P1, alt=%d meters", self:GetName(), p0.y)) end + else + end -- Set flightcontrol for this flight. @@ -2809,6 +2824,7 @@ function FLIGHTGROUP:onafterHolding(From, Event, To) -- Holding time stamp. self.Tholding=timer.getAbsTime() + -- Debug message. local text=string.format("Flight group %s is HOLDING now", self.groupname) self:T(self.lid..text) From 7c4cb5ea7f9987a681c79cc5b7c9430a36f80815 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 18 May 2022 12:51:25 +0200 Subject: [PATCH 03/20] FC - Improvements --- Moose Development/Moose/Ops/FlightControl.lua | 548 +++++++++++++----- Moose Development/Moose/Ops/FlightGroup.lua | 12 +- Moose Development/Moose/Ops/OpsGroup.lua | 2 +- Moose Development/Moose/Wrapper/Airbase.lua | 20 +- 4 files changed, 437 insertions(+), 145 deletions(-) diff --git a/Moose Development/Moose/Ops/FlightControl.lua b/Moose Development/Moose/Ops/FlightControl.lua index 736dbbf14..dcdb3388d 100644 --- a/Moose Development/Moose/Ops/FlightControl.lua +++ b/Moose Development/Moose/Ops/FlightControl.lua @@ -6,7 +6,9 @@ -- -- * Manage aircraft departure and arrival -- * Handles AI and human players +-- * Limit number of AI groups taxiing and landing simultaniously -- * Immersive voice overs via SRS text-to-speech +-- * Define holding zones for airdromes -- -- === -- @@ -67,7 +69,7 @@ -- * 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. --- * Only FLIGHTGROUPS are controlled. +-- * Only FLIGHTGROUPS are controlled. This means some older classes, *e.g.* RAT are not supported (yet). -- -- -- @field #FLIGHTCONTROL @@ -158,7 +160,7 @@ FLIGHTCONTROL.FlightStatus={ --- FlightControl class version. -- @field #string version -FLIGHTCONTROL.version="0.5.0" +FLIGHTCONTROL.version="0.5.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -529,8 +531,8 @@ function FLIGHTCONTROL:OnEventBirth(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))) + self:T3(self.lid..string.format("BIRTH: unit = %s", tostring(EventData.IniUnitName))) + self:T3(self.lid..string.format("BIRTH: group = %s", tostring(EventData.IniGroupName))) -- Unit that was born. local unit=EventData.IniUnit @@ -539,7 +541,7 @@ function FLIGHTCONTROL:OnEventBirth(EventData) if unit:IsAir() then local bornhere=EventData.Place and EventData.Place:GetName()==self.airbasename or false - env.info("FF born here ".. tostring(bornhere)) + --env.info("FF born here ".. tostring(bornhere)) -- We got a player? local playerunit, playername=self:_GetPlayerUnitAndName(EventData.IniUnitName) @@ -684,8 +686,6 @@ function FLIGHTCONTROL:_CheckQueues() -- 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() + local callsign=flight:GetCallsignName() + + -- Runway. + local runway=self:GetActiveRunwayText() -- Message. - local text=string.format("%s, %s, you are cleared to land.", callsign, self.alias) - + local text=string.format("%s, %s, you are cleared to land, runway %s", callsign, self.alias, runway) + -- Transmit message. self:TransmissionTower(text, flight) @@ -712,10 +715,12 @@ function FLIGHTCONTROL:_CheckQueues() self:_LandAI(flight, parking) else -- TODO: Humans have to confirm via F10 menu. + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.LANDING) + flight:_UpdateMenu() end -- Set time last flight got landing clearance. - self.Tlanding=timer.getAbsTime() + --self.Tlanding=timer.getAbsTime() end else @@ -738,10 +743,10 @@ function FLIGHTCONTROL:_CheckQueues() local runway=self:GetActiveRunwayText() -- Message. - local text=string.format("%s, %s, taxi to holding point, runway %s", callsign, self.alias, runway) + local text=string.format("%s, %s, taxi to runway %s, hold short", 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) + text=string.format("%s, %s, cleared for take-off, runway %s", callsign, self.alias, runway) end -- Transmit message. @@ -822,12 +827,15 @@ end -- @return #table Parking data for holding flights or nil. function FLIGHTCONTROL:_GetNextFlight() + -- Get flight that is holding. local flightholding=self:_GetNextFightHolding() + + -- Get flight that is parking. 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")) + --self:T(self.lid..string.format("Next flight that is not holding")) return flightparking, false, nil end @@ -1543,7 +1551,7 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Human Player Functions +-- Payer Menu ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Create player menu. @@ -1552,13 +1560,18 @@ end -- @param #table atcmenu ATC root menu table. function FLIGHTCONTROL:_CreatePlayerMenu(flight, atcmenu) + -- Group info. local group=flight.group local groupname=flight.groupname local gid=group:GetID() + -- Flight status. local flightstatus=self:GetFlightStatus(flight) + + -- Are we controlling this flight. local gotcontrol=self:IsControlling(flight) + -- Debug info. self:I(self.lid..string.format("Creating ATC player menu for flight %s: in state=%s status=%s, gotcontrol=%s", tostring(flight.groupname), flight:GetState(), flightstatus, tostring(gotcontrol))) local airbasename=self.airbasename @@ -1572,26 +1585,39 @@ function FLIGHTCONTROL:_CreatePlayerMenu(flight, atcmenu) atcmenu[airbasename] = atcmenu[airbasename] or {} + -- Remove menu if it exists. if atcmenu[airbasename].root then local rootmenu=atcmenu[airbasename].root --Core.Menu#MENU_GROUP rootmenu:Remove() end -- Airbase root menu. - atcmenu[airbasename].root = MENU_GROUP:New(group, airbaseName2, atcmenu.root) --:SetTime(Tnow):SetTag(Tag) + atcmenu[airbasename].root = MENU_GROUP:New(group, airbaseName2, atcmenu.root) + -- Shortcut to root menu. local rootmenu=atcmenu[airbasename].root --Core.Menu#MENU_GROUP - -- Help Menu. - local helpmenu=MENU_GROUP:New(group, "Help", rootmenu)--:SetTime(Tnow):SetTag(Tag) + --- + -- Help Menu + --- + local helpmenu=MENU_GROUP:New(group, "Help", rootmenu) + MENU_GROUP_COMMAND:New(group, "Mark Holding", helpmenu, self._PlayerNotImplemented, self, groupname) + MENU_GROUP_COMMAND:New(group, "Skill Level", helpmenu, self._PlayerNotImplemented, self, groupname) + MENU_GROUP_COMMAND:New(group, "Subtitles On/Off", helpmenu, self._PlayerNotImplemented, self, groupname) + MENU_GROUP_COMMAND:New(group, "My Voice On/Off", helpmenu, self._PlayerNotImplemented, self, groupname) + MENU_GROUP_COMMAND:New(group, "My Status", helpmenu, self._PlayerMyStatus, self, groupname) - -- Some info. - local infomenu=MENU_GROUP:New(group, "Info", rootmenu)--:SetTime(Tnow):SetTag(Tag) - MENU_GROUP_COMMAND:New(group, "Airbase", infomenu, self._PlayerRequestInfo, self, groupname)--:SetTime(Tnow):SetTag(Tag) - MENU_GROUP_COMMAND:New(group, "Queues", infomenu, self._PlayerRequestInfoQueues, self, groupname)--:SetTime(Tnow):SetTag(Tag) - MENU_GROUP_COMMAND:New(group, "ATIS", infomenu, self._PlayerRequestInfoATIS, self, groupname)--:SetTime(Tnow):SetTag(Tag) + --- + -- Info Menu + --- + local infomenu=MENU_GROUP:New(group, "Info", rootmenu) + MENU_GROUP_COMMAND:New(group, "Airbase", infomenu, self._PlayerRequestInfo, self, groupname) + MENU_GROUP_COMMAND:New(group, "Queues", infomenu, self._PlayerRequestInfoQueues, self, groupname) + MENU_GROUP_COMMAND:New(group, "ATIS", infomenu, self._PlayerRequestInfoATIS, self, groupname) - -- Root Commands. + --- + -- Root Menu + --- if gotcontrol then local status=self:GetFlightStatus(flight) @@ -1606,9 +1632,9 @@ function FLIGHTCONTROL:_CreatePlayerMenu(flight, atcmenu) --- if status==FLIGHTCONTROL.FlightStatus.READYTX then - MENU_GROUP_COMMAND:New(group, "Abort Taxi", rootmenu, self._PlayerAbortTaxi, self, groupname)--:SetTime(Tnow):SetTag(Tag) + MENU_GROUP_COMMAND:New(group, "Abort Taxi", rootmenu, self._PlayerAbortTaxi, self, groupname) else - MENU_GROUP_COMMAND:New(group, "Request Taxi", rootmenu, self._PlayerRequestTaxi, self, groupname)--:SetTime(Tnow):SetTag(Tag) + MENU_GROUP_COMMAND:New(group, "Request Taxi", rootmenu, self._PlayerRequestTaxi, self, groupname) end elseif flight:IsTaxiing() then @@ -1617,36 +1643,52 @@ function FLIGHTCONTROL:_CreatePlayerMenu(flight, atcmenu) --- if status==FLIGHTCONTROL.FlightStatus.READYTO then - MENU_GROUP_COMMAND:New(group, "Abort Takeoff", rootmenu, self._PlayerAbortTakeoff, self, groupname)--:SetTime(Tnow):SetTag(Tag) + MENU_GROUP_COMMAND:New(group, "Abort Takeoff", rootmenu, self._PlayerAbortTakeoff, self, groupname) elseif status==FLIGHTCONTROL.FlightStatus.TAKEOFF then - MENU_GROUP_COMMAND:New(group, "Abort Takeoff", rootmenu, self._PlayerAbortTakeoff, self, groupname)--:SetTime(Tnow):SetTag(Tag) + MENU_GROUP_COMMAND:New(group, "Abort Takeoff", rootmenu, self._PlayerAbortTakeoff, self, groupname) elseif status==FLIGHTCONTROL.FlightStatus.READYTX or status==FLIGHTCONTROL.FlightStatus.TAXIOUT then - MENU_GROUP_COMMAND:New(group, "Abort Taxi", rootmenu, self._PlayerAbortTaxi, self, groupname)--:SetTime(Tnow):SetTag(Tag) - MENU_GROUP_COMMAND:New(group, "Request Takeoff", rootmenu, self._PlayerRequestTakeoff, self, groupname)--:SetTime(Tnow):SetTag(Tag) + MENU_GROUP_COMMAND:New(group, "Abort Taxi", rootmenu, self._PlayerAbortTaxi, self, groupname) + MENU_GROUP_COMMAND:New(group, "Request Takeoff", rootmenu, self._PlayerRequestTakeoff, self, groupname) elseif status==FLIGHTCONTROL.FlightStatus.TAXIINB then -- Could be after "abort taxi" call and we changed our mind (again) MENU_GROUP_COMMAND:New(group, "Request Taxi", rootmenu, self._PlayerRequestTaxi, self, groupname) MENU_GROUP_COMMAND:New(group, "Request Parking", rootmenu, self._PlayerRequestParking, self, groupname) end - MENU_GROUP_COMMAND:New(group, "Arrived at Parking", rootmenu, self._PlayerArrived, self, groupname) - - - + MENU_GROUP_COMMAND:New(group, "Arrived and Parking", rootmenu, self._PlayerArrived, self, groupname) + elseif flight:IsAirborne() then --- -- Airborne --- elseif flight:IsInbound() then + --- + -- Inbound + --- - MENU_GROUP_COMMAND:New(group, "Holding", rootmenu, self._PlayerHolding, self, groupname)--:SetTime(Tnow):SetTag(Tag) - --TODO: abort inbound + MENU_GROUP_COMMAND:New(group, "Abort Inbound", rootmenu, self._PlayerAbortInbound, self, groupname) + MENU_GROUP_COMMAND:New(group, "Holding", rootmenu, self._PlayerHolding, self, groupname) + elseif flight:IsHolding() then + --- + -- Holding + --- + + MENU_GROUP_COMMAND:New(group, "Abort Holding", rootmenu, self._PlayerAbortHolding, self, groupname) + MENU_GROUP_COMMAND:New(group, "Landing", rootmenu, self._PlayerConfirmLanding, self, groupname) + + elseif flight:IsLanding() then + --- + -- Landing + --- + + MENU_GROUP_COMMAND:New(group, "Abort Landing", rootmenu, self._PlayerAbortLanding, self, groupname) + end if flight:IsInbound() or flight:IsHolding() or flight:IsLanding() or flight:IsLanded() then - MENU_GROUP_COMMAND:New(group, "Request Parking", rootmenu, self._PlayerRequestParking, self, groupname)--:SetTime(Tnow):SetTag(Tag) + MENU_GROUP_COMMAND:New(group, "Request Parking", rootmenu, self._PlayerRequestParking, self, groupname) end else @@ -1656,111 +1698,66 @@ function FLIGHTCONTROL:_CreatePlayerMenu(flight, atcmenu) --- if flight:IsAirborne() then - MENU_GROUP_COMMAND:New(group, "Inbound", rootmenu, self._PlayerInbound, self, groupname)--:SetTime(Tnow):SetTag(Tag) + MENU_GROUP_COMMAND:New(group, "Inbound", rootmenu, self._PlayerRequestInbound, self, groupname) end end - - if gotcontrol then - MENU_GROUP_COMMAND:New(group, "My Status", rootmenu, self._PlayerMyStatus, self, groupname)--:SetTime(Tnow):SetTag(Tag) - end - - -- Reset the menu. - --rootmenu:Remove(Tnow, Tag) - --rootmenu:Set() end ---- Player menu request info. +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Player Menu: Help +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Player menu not implemented. -- @param #FLIGHTCONTROL self -- @param #string groupname Name of the flight group. -function FLIGHTCONTROL:_PlayerRequestParking(groupname) +function FLIGHTCONTROL:_PlayerNotImplemented(groupname) -- Get flight group. local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP if flight then - local group=flight:GetGroup() - local coord=flight:GetCoordinate() - - --TODO: terminal type for helos! - local spot=self:GetClosestParkingSpot(coord, nil, true) - - -- Get callsign. - local callsign=flight:GetCallsignName() - - -- Message text. - local text=string.format("%s, tower, your assigned parking position is terminal ID %d. Check the F10 map for details.", callsign, spot.TerminalID) - - -- Transmit message. - self:TransmissionTower(text, flight) - - -- Create mark on F10 map. - if spot.Marker then - spot.Marker:Remove() - end - spot.Marker:SetText("Your assigned parking spot!"):ToGroup(group) - - -- Set parking of player element. - for _,_element in pairs(flight.elements) do - local element=_element --Ops.FlightGroup#FLIGHTGROUP.Element - if not element.ai then - element.parking=spot - end - end - + local text=string.format("Sorry, this feature is not implemented yet!") + self:TextMessageToFlight(text, flight) + end end ---- Player arrived at parking position. +--- Player status. -- @param #FLIGHTCONTROL self -- @param #string groupname Name of the flight group. -function FLIGHTCONTROL:_PlayerArrived(groupname) +function FLIGHTCONTROL:_PlayerMyStatus(groupname) -- Get flight group. local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP if flight then + + local fc=flight.flightcontrol - local group=flight:GetGroup() - local coord=flight:GetCoordinate() + -- Status text. + local text=string.format("My Status:") + text=text..string.format("\nCallsign: %s", tostring(flight:GetCallsignName())) + text=text..string.format("\nFlight status: %s", tostring(flight:GetState())) + text=text..string.format("\nFlight control: %s status=%s", tostring(fc and fc.airbasename or "N/A"), tostring(fc and fc:GetFlightStatus(flight) or "N/A")) - --Closest parking spot. - local spot=self:GetClosestParkingSpot(coord, nil, true) - - -- Get callsign. - local callsign=flight:GetCallsignName() - - -- Message text. - local text=string.format("Tower, %s, arrived at parking position. Terminal ID %d.", callsign, spot.TerminalID) - - -- Transmit message. - self:TransmissionTower(text, flight) - - self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.PARKING) - - self:_CreatePlayerMenu(flight, flight.menu.atc) - - -- Create mark on F10 map. - if spot.Marker then - spot.Marker:Remove() - end - spot.Marker:SetText("Your current parking spot!"):ToGroup(group) - - -- Set parking of player element. - for _,_element in pairs(flight.elements) do - local element=_element --Ops.OpsGroup#OPSGROUP.Element - if not element.ai then - element.parking=spot - end - end - + -- Send message. + self:TextMessageToFlight(text, flight, 10, true) + + else + MESSAGE:New(string.format("Cannot find flight group %s.", tostring(groupname)), 5):ToAll() end end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Player Menu: Info +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + --- Player menu request info. -- @param #FLIGHTCONTROL self -- @param #string groupname Name of the flight group. @@ -1824,7 +1821,6 @@ function FLIGHTCONTROL:_PlayerRequestInfoATIS(groupname) end - --- Player menu request info. -- @param #FLIGHTCONTROL self -- @param #string groupname Name of the flight group. @@ -1857,6 +1853,10 @@ function FLIGHTCONTROL:_PlayerRequestInfoQueues(groupname) end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Player Menu: Inbound +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + --- Player calls inbound. -- @param #FLIGHTCONTROL self -- @param #string groupname Name of the flight group. @@ -1894,6 +1894,9 @@ function FLIGHTCONTROL:_PlayerRequestInbound(groupname) -- Call RTB event. flight:RTB(self.airbase) + + -- Set flightcontrol for this flight. + flight:SetFlightControl(self) -- Add flight to inbound queue. self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.INBOUND) @@ -1927,6 +1930,9 @@ function FLIGHTCONTROL:_PlayerRequestInbound(groupname) -- Send message. self:TransmissionTower(text, flight, 15) + -- Create player menu. + self:_CreatePlayerMenu(flight, flight.menu.atc) + else self:E(self.lid..string.format("WARNING: Could not get holding stack for flight %s", flight:GetName())) end @@ -1955,7 +1961,7 @@ function FLIGHTCONTROL:_PlayerRequestInbound(groupname) end ---- Player calls inbound. +--- Player aborts inbound. -- @param #FLIGHTCONTROL self -- @param #string groupname Name of the flight group. function FLIGHTCONTROL:_PlayerAbortInbound(groupname) @@ -1975,20 +1981,27 @@ function FLIGHTCONTROL:_PlayerAbortInbound(groupname) -- Radio message. self:TransmissionPilot(text, flight) - - -- Add flight to inbound queue. - self:_RemoveFlight(flight) - -- Set flight. - flight.stack.flightgroup=nil - flight.stack=nil - -- Message text. local text=string.format("%s, %s, roger, have a nice day!", callsign, self.alias) -- Send message. - self:TransmissionTower(text, flight, 15) + self:TransmissionTower(text, flight, 5) + + -- Set flight. + if flight.stack then + flight.stack.flightgroup=nil + flight.stack=nil + else + self:E(self.lid.."ERROR: No stack!") + end + -- Set flight to cruise. + flight:Cruise() + + -- Remove flight. This also updates the menu. + self:_RemoveFlight(flight) + else -- Error you are not airborne! @@ -2004,6 +2017,10 @@ function FLIGHTCONTROL:_PlayerAbortInbound(groupname) end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Player Menu: Holding +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + --- Player calls holding. -- @param #FLIGHTCONTROL self -- @param #string groupname Name of the flight group. @@ -2027,7 +2044,7 @@ function FLIGHTCONTROL:_PlayerHolding(groupname) local dist=stack.pos0:Get2DDistance(Coordinate) - if dist<5000 then + if dist<5000 or true then -- Message to flight local text=string.format("Roger, you are added to the holding queue!") @@ -2065,35 +2082,194 @@ function FLIGHTCONTROL:_PlayerHolding(groupname) self:TextMessageToFlight(text, flight, 10, true) end else + --TODO: Error end + end ---- Create player menu. +--- Player aborts holding. -- @param #FLIGHTCONTROL self -- @param #string groupname Name of the flight group. -function FLIGHTCONTROL:_PlayerMyStatus(groupname) +function FLIGHTCONTROL:_PlayerAbortHolding(groupname) -- Get flight group. local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP if flight then - - local fc=flight.flightcontrol - - -- Status text. - local text=string.format("My Status:") - text=text..string.format("\nFlight status: %s", tostring(flight:GetState())) - text=text..string.format("\nFlight control: %s status=%s", tostring(fc and fc.airbasename or "N/A"), tostring(fc and fc:GetFlightStatus(flight) or "N/A")) + + if flight:IsHolding() and self:IsControlling(flight) then + + -- Call sign. + local callsign=flight:GetCallsignName() + + -- Pilot calls inbound for landing. + local text=string.format("%s, %s, abort holding", self.alias, callsign) + + -- Radio message. + self:TransmissionPilot(text, flight) + + -- Message text. + local text=string.format("%s, %s, roger, have a nice day!", callsign, self.alias) + + -- Send message. + self:TransmissionTower(text, flight, 10) - -- Send message. - self:TextMessageToFlight(text, flight, 10, true) - + -- Not holding any more. + flight.Tholding=nil + + -- Set flight to cruise. + flight:Cruise() + + -- Set flight. + if flight.stack then + flight.stack.flightgroup=nil + flight.stack=nil + else + self:E(self.lid.."ERROR: No stack!") + end + + -- Remove flight. This also updates the menu. + self:_RemoveFlight(flight) + + else + + -- Error you are not airborne! + local text=string.format("Negative, you must be HOLDING and CONTROLLED by us!") + + -- Send message. + self:TextMessageToFlight(text, flight, 10) + end + else MESSAGE:New(string.format("Cannot find flight group %s.", tostring(groupname)), 5):ToAll() end end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Player Menu: Landing +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Player confirms landing. +-- @param #FLIGHTCONTROL self +-- @param #string groupname Name of the flight group. +function FLIGHTCONTROL:_PlayerConfirmLanding(groupname) + + -- Get flight group. + local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP + + if flight then + + if flight:IsHolding() and self:IsControlling(flight) then + + -- Call sign. + local callsign=flight:GetCallsignName() + + -- Pilot calls inbound for landing. + local text=string.format("%s, %s, leaving pattern for landing", self.alias, callsign) + + -- Radio message. + self:TransmissionPilot(text, flight) + + -- Set flight. + if flight.stack then + flight.stack.flightgroup=nil + flight.stack=nil + else + self:E(self.lid.."ERROR: No stack!") + end + + -- Not holding any more. + flight.Tholding=nil + + -- Set flight to landing. + flight:Landing() + + -- Message text. + local text=string.format("%s, continue approach", callsign) + + -- Send message. + self:TransmissionTower(text, flight, 10) + + -- Create player menu. + self:_CreatePlayerMenu(flight, flight.menu.atc) + + else + + -- Error you are not airborne! + local text=string.format("Negative, you must be HOLDING and CONTROLLED by us!") + + -- Send message. + self:TextMessageToFlight(text, flight, 10) + end + + else + MESSAGE:New(string.format("Cannot find flight group %s.", tostring(groupname)), 5):ToAll() + end + +end + +--- Player aborts landing. +-- @param #FLIGHTCONTROL self +-- @param #string groupname Name of the flight group. +function FLIGHTCONTROL:_PlayerAbortLanding(groupname) + + -- Get flight group. + local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP + + if flight then + + if flight:IsLanding() and self:IsControlling(flight) then + + -- Call sign. + local callsign=flight:GetCallsignName() + + -- Pilot calls inbound for landing. + local text=string.format("%s, %s, abort landing", self.alias, callsign) + + -- Radio message. + self:TransmissionPilot(text, flight) + + -- Message text. + local text=string.format("%s, %s, roger, have a nice day!", callsign, self.alias) + + -- Send message. + self:TransmissionTower(text, flight, 10) + + -- Set flight. + if flight.stack then + flight.stack.flightgroup=nil + flight.stack=nil + end + + -- Not holding any more. + flight.Tholding=nil + + -- Set flight to cruise. + flight:Cruise() + + -- Remove flight. This also updates the menu. + self:_RemoveFlight(flight) + + else + + -- Error you are not airborne! + local text=string.format("Negative, you must be LANDING and CONTROLLED by us!") + + -- Send message. + self:TextMessageToFlight(text, flight, 10) + end + + else + MESSAGE:New(string.format("Cannot find flight group %s.", tostring(groupname)), 5):ToAll() + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Player Menu: Taxi +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + --- Player requests taxi. -- @param #FLIGHTCONTROL self -- @param #string groupname Name of the flight group. @@ -2183,6 +2359,10 @@ function FLIGHTCONTROL:_PlayerAbortTaxi(groupname) end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Player Menu: Takeoff +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + --- Player requests takeoff. -- @param #FLIGHTCONTROL self -- @param #string groupname Name of the flight group. @@ -2257,6 +2437,102 @@ function FLIGHTCONTROL:_PlayerAbortTakeoff(groupname) end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Player Menu: Parking +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Player menu request info. +-- @param #FLIGHTCONTROL self +-- @param #string groupname Name of the flight group. +function FLIGHTCONTROL:_PlayerRequestParking(groupname) + + -- Get flight group. + local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP + + if flight then + + local group=flight:GetGroup() + local coord=flight:GetCoordinate() + + --TODO: terminal type for helos! + local spot=self:GetClosestParkingSpot(coord, nil, true) + + -- Get callsign. + local callsign=flight:GetCallsignName() + + -- Message text. + local text=string.format("%s, your assigned parking position is terminal ID %d. Check the F10 map for details.", callsign, spot.TerminalID) + + -- Transmit message. + self:TransmissionTower(text, flight) + + -- Create mark on F10 map. + if spot.Marker then + spot.Marker:Remove() + end + spot.Marker:SetText("Your assigned parking spot!"):ToGroup(group) + + -- Set parking of player element. + for _,_element in pairs(flight.elements) do + local element=_element --Ops.FlightGroup#FLIGHTGROUP.Element + if not element.ai then + element.parking=spot + end + end + + end + +end + +--- Player arrived at parking position. +-- @param #FLIGHTCONTROL self +-- @param #string groupname Name of the flight group. +function FLIGHTCONTROL:_PlayerArrived(groupname) + + -- Get flight group. + local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP + + if flight then + + local group=flight:GetGroup() + local coord=flight:GetCoordinate() + + --Closest parking spot. + local spot=self:GetClosestParkingSpot(coord, nil, true) + + -- Get callsign. + local callsign=flight:GetCallsignName() + + -- Message text. + local text=string.format("Tower, %s, arrived at parking position. Terminal ID %d.", callsign, spot.TerminalID) + + -- Transmit message. + self:TransmissionTower(text, flight) + + -- Set flight status to PARKING. + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.PARKING) + + -- Create player menu. + self:_CreatePlayerMenu(flight, flight.menu.atc) + + -- Create mark on F10 map. + if spot.Marker then + spot.Marker:Remove() + end + spot.Marker:SetText("Your current parking spot!"):ToGroup(group) + + -- Set parking of player element. + for _,_element in pairs(flight.elements) do + local element=_element --Ops.OpsGroup#OPSGROUP.Element + if not element.ai then + element.parking=spot + end + end + + end + +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Flight and Element Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2297,8 +2573,10 @@ end -- @param Ops.FlightGroup#FLIGHTGROUP flight The flight to be removed. function FLIGHTCONTROL:_RemoveFlight(flight) - self:_RemoveFlightFromQueue(self.flights, flight, "flights") + flight.flightcontrol=nil + self:_RemoveFlightFromQueue(self.flights, flight, "flights") + end --- Get flight from group. diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index 3433c019f..3beccc6e9 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -812,12 +812,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)) + --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. + -- At least for fighters, the initial distance seems to be 1.8 meters. -- 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>5 then + if dist>10 then if element.status==OPSGROUP.ElementStatus.ENGINEON then self:ElementTaxiing(element) end @@ -1678,10 +1679,7 @@ function FLIGHTGROUP:onafterTaxiing(From, Event, To) -- Parking over. self.Tparking=nil - -- TODO: need a better check for the airbase. - local airbase=self:GetClosestAirbase() - - if self.flightcontrol and airbase and self.flightcontrol.airbasename==airbase:GetName() then + if self.flightcontrol and self.flightcontrol:IsControlling(self) then -- Add AI flight to takeoff queue. if self.isAI then @@ -2546,7 +2544,7 @@ function FLIGHTGROUP:_LandAtAirbase(airbase, SpeedTo, SpeedHold, SpeedLand) -- Do we have a flight control? local fc=_DATABASE:GetFlightControl(airbase:GetName()) - if fc then + if fc and self.isAI then -- Get holding point from flight control. local HoldingPoint=fc:_GetHoldingpoint(self) diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index ea09416c0..40855ef92 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -7176,13 +7176,13 @@ function OPSGROUP:onafterStop(From, Event, To) -- Flightcontrol. if self.flightcontrol then - self.flightcontrol:_RemoveFlight(self) for _,_element in pairs(self.elements) do local element=_element --#OPSGROUP.Element if element.parking then self.flightcontrol:SetParkingFree(element.parking) end end + self.flightcontrol:_RemoveFlight(self) end if self:IsAlive() and not (self:IsDead() or self:IsStopped()) then diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 04b474db9..f4d4b78a8 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -1000,6 +1000,23 @@ function AIRBASE:_InitParkingSpots() self.NparkingTerminal[terminalType]=0 end + -- Get client coordinates. + local function isClient(coord) + local clients=_DATABASE.CLIENTS + for clientname, client in pairs(clients) do + local template=_DATABASE:GetGroupTemplateFromUnitName(clientname) + local units=template.units + for i,unit in pairs(units) do + local Coord=COORDINATE:New(unit.x, unit.alt, unit.y) + local dist=Coord:Get2DDistance(coord) + if dist<2 then + return true + end + end + end + return false + end + -- Put coordinates of parking spots into table. for _,spot in pairs(parkingdata) do @@ -1013,6 +1030,7 @@ function AIRBASE:_InitParkingSpots() park.TerminalID0=spot.Term_Index_0 park.TerminalType=spot.Term_Type park.TOAC=spot.TO_AC + park.ClientSpot=isClient(park.Coordinate) self.NparkingTotal=self.NparkingTotal+1 @@ -1072,7 +1090,6 @@ function AIRBASE:GetParkingSpotsTable(termtype) spot.Free=_isfree(_spot) -- updated spot.TOAC=_spot.TO_AC -- updated spot.AirbaseName=self.AirbaseName - spot.ClientSpot=nil --TODO table.insert(spots, spot) @@ -1110,7 +1127,6 @@ function AIRBASE:GetFreeParkingSpotsTable(termtype, allowTOAC) spot.Free=true -- updated spot.TOAC=_spot.TO_AC -- updated spot.AirbaseName=self.AirbaseName - spot.ClientSpot=nil --TODO table.insert(freespots, spot) From 6279f1920ed2d4e7b35b119c27558d598bd20d15 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 20 May 2022 20:08:28 +0200 Subject: [PATCH 04/20] FC **FLIGHTCONTROL** - Improved menus - Added more menus - Improved radio messages overlap - Added options to limit number of fights taxiing and taking off --- Moose Development/Moose/Ops/FlightControl.lua | 951 +++++++++++++----- Moose Development/Moose/Ops/FlightGroup.lua | 151 ++- Moose Development/Moose/Ops/OpsGroup.lua | 8 +- Moose Development/Moose/Wrapper/Marker.lua | 10 + 4 files changed, 852 insertions(+), 268 deletions(-) diff --git a/Moose Development/Moose/Ops/FlightControl.lua b/Moose Development/Moose/Ops/FlightControl.lua index dcdb3388d..e66bf2a2f 100644 --- a/Moose Development/Moose/Ops/FlightControl.lua +++ b/Moose Development/Moose/Ops/FlightControl.lua @@ -36,14 +36,21 @@ -- @field #number activerwyno Number of active runway. -- @field #number frequency ATC radio frequency in MHz. -- @field #number modulation ATC radio modulation, *e.g.* `radio.modulation.AM`. --- @field #number Nlanding Max number of aircraft groups in the landing pattern. +-- @field #number NlandingTot Max number of aircraft groups in the landing pattern. +-- @field #number NlandingTakeoff Max number of groups taking off to allow landing clearance. +-- @field #number NtaxiTot Max number of aircraft groups taxiing to runway for takeoff. +-- @field #boolean NtaxiInbound Include inbound taxiing groups. +-- @field #number NtaxiLanding Max number of aircraft landing for groups taxiing to runway for takeoff. -- @field #number dTlanding Time interval in seconds between landing clearance. +-- @field #number Tlanding Time stamp (abs.) when last flight got landing clearance. -- @field #number Nparkingspots Total number of parking spots. -- @field Core.Spawn#SPAWN parkingGuard Parking guard spawner. -- @field #table holdingpoints Holding points. -- @field #number hpcounter Counter for holding zones. -- @field Sound.SRS#MSRS msrsTower Moose SRS wrapper. -- @field Sound.SRS#MSRS msrsPilot Moose SRS wrapper. +-- @field #number Tlastmessage Time stamp (abs.) of last radio transmission. +-- @field #number dTmessage Time interval between messages. -- @extends Core.Fsm#FSM --- **Ground Control**: Airliner X, Good news, you are clear to taxi to the active. @@ -60,7 +67,6 @@ -- ## Prerequisites -- -- * SRS is used for radio communications --- * -- -- ## Limitations -- @@ -70,6 +76,8 @@ -- * 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. -- * Only FLIGHTGROUPS are controlled. This means some older classes, *e.g.* RAT are not supported (yet). +-- * So far only airdromes are handled, *i.e.* no FARPs or ships. +-- * Only fixed wing aircraft are handled until now, *i.e.* no helos. -- -- -- @field #FLIGHTCONTROL @@ -139,16 +147,16 @@ FLIGHTCONTROL = { -- @field #string READYTO Flight is ready for takeoff. -- @field #string TAKEOFF Flight is taking off. FLIGHTCONTROL.FlightStatus={ + PARKING="Parking", + READYTX="Ready To Taxi", + TAXIOUT="Taxi to runway", + READYTO="Ready For Takeoff", + TAKEOFF="Takeoff", 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. @@ -160,7 +168,7 @@ FLIGHTCONTROL.FlightStatus={ --- FlightControl class version. -- @field #string version -FLIGHTCONTROL.version="0.5.1" +FLIGHTCONTROL.version="0.5.2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -168,14 +176,14 @@ FLIGHTCONTROL.version="0.5.1" -- TODO: Support airwings. Dont give clearance for Alert5 or if mission has not started. -- TODO: Switch to enable/disable AI messages. --- TODO: Improve TTS messages. --- TODO: Define holding zone. +-- TODO: Improve ATC TTS messages. -- 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. +-- TODO: Define holding zone. +-- DONE: Basic ATC voice overs. -- DONE: Add SRS TTS. -- DONE: Add parking guard. -- NOGO: Add FARPS? @@ -230,7 +238,8 @@ function FLIGHTCONTROL:New(AirbaseName, Frequency, Modulation, PathToSRS) self.alias=self.airbasename.." Tower" -- Defaults: - self:SetLandingMax() + self:SetLimitLanding(2, 0) + self:SetLimitTaxi(1, false, 0) self:SetLandingInterval() self:SetFrequency(Frequency, Modulation) @@ -243,6 +252,9 @@ function FLIGHTCONTROL:New(AirbaseName, Frequency, Modulation, PathToSRS) self.msrsPilot:SetGender("male") self.msrsPilot:SetCulture("en-US") self.msrsPilot:SetLabel("Pilot") + + -- Wait at least 10 seconds after last radio message before calling the next status update. + self.dTmessage=10 -- Init runways. self:_InitRunwayData() @@ -254,10 +266,45 @@ function FLIGHTCONTROL:New(AirbaseName, Frequency, Modulation, PathToSRS) -- From State --> Event --> To State self:AddTransition("Stopped", "Start", "Running") -- Start FSM. self:AddTransition("*", "Status", "*") -- Update status. + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. -- Add to data base. _DATABASE:AddFlightControl(self) + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". + -- @function [parent=#FLIGHTCONTROL] Start + -- @param #FLIGHTCONTROL self + + --- Triggers the FSM event "Start" after a delay. + -- @function [parent=#FLIGHTCONTROL] __Start + -- @param #FLIGHTCONTROL self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Stop". + -- @function [parent=#FLIGHTCONTROL] Stop + -- @param #FLIGHTCONTROL self + + --- Triggers the FSM event "Stop" after a delay. + -- @function [parent=#FLIGHTCONTROL] __Stop + -- @param #FLIGHTCONTROL self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Status". + -- @function [parent=#FLIGHTCONTROL] Status + -- @param #FLIGHTCONTROL self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#FLIGHTCONTROL] __Status + -- @param #FLIGHTCONTROL self + -- @param #number delay Delay in seconds. + return self end @@ -279,12 +326,16 @@ end --- Set the number of aircraft groups, that are allowed to land simultaniously. +-- Note that this restricts AI and human players. -- @param #FLIGHTCONTROL self --- @param #number n Max number of aircraft landing simultaniously. Default 2. +-- @param #number Nlanding Max number of aircraft landing simultaniously. Default 2. +-- @param #number Ntakeoff Allowed number of aircraft taking off for groups to get landing clearance. Default 0. -- @return #FLIGHTCONTROL self -function FLIGHTCONTROL:SetLandingMax(n) +function FLIGHTCONTROL:SetLimitLanding(Nlanding, Ntakeoff) - self.Nlanding=n or 2 + self.NlandingTot=Nlanding or 2 + + self.NlandingTakeoff=Ntakeoff or 0 return self end @@ -301,6 +352,35 @@ function FLIGHTCONTROL:SetLandingInterval(dt) end +--- Set the number of **AI** aircraft groups, that are allowed to taxi simultaniously. +-- If the limit is reached, other AI groups not get taxi clearance to taxi to the runway. +-- +-- By default, this only counts the number of AI that taxi from their parking position to the runway. +-- You can also include inbound AI that taxi from the runway to their parking position. +-- This can be handy for problematic (usually smaller) airdromes, where there is only one taxiway inbound and outbound flights. +-- +-- By default, AI will not get cleared for taxiing if at least one other flight is currently landing. If this is an unproblematic airdrome, you can +-- also allow groups to taxi if planes are landing, *e.g.* if there are two separate runways. +-- +-- NOTE that human players are *not* restricted as they should behave better (hopefully) than the AI. +-- +-- @param #FLIGHTCONTROL self +-- @param #number Ntaxi Max number of groups allowed to taxi. Default 2. +-- @param #boolean IncludeInbound If `true`, the above +-- @param #number Nlanding Max number of landing flights. Default 0. +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:SetLimitTaxi(Ntaxi, IncludeInbound, Nlanding) + + self.NtaxiTot=Ntaxi or 2 + + self.NtaxiInbound=IncludeInbound + + self.NtaxiLanding=Nlanding or 0 + + return self +end + + --- Set runway. This clears all auto generated runways. -- @param #FLIGHTCONTROL self -- @param #FLIGHTCONTROL.Runway Runway. @@ -337,36 +417,33 @@ function FLIGHTCONTROL:SetActiveRunwayNumber(no) return self end ---- Add holding zone. +--- Add a holding point. +-- This is a zone where the aircraft... -- @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. +-- @param #number FlightlevelMin Min flight level. Default 5. +-- @param #number FlightlevelMax Max flight level. Default 15. -- @return #FLIGHTCONTROL self function FLIGHTCONTROL:AddHoldingPoint(ArrivalZone, Heading, Length, FlightlevelMin, FlightlevelMax) - local hp={} --#FLIGHTCONTROL.HoldingPoint - - hp.arrivalzone=ArrivalZone + -- Get ZONE if passed as string. + if type(ArrivalZone)=="string" then + ArrivalZone=ZONE:New(ArrivalZone) + end + -- Increase counter. self.hpcounter=self.hpcounter+1 - + + local hp={} --#FLIGHTCONTROL.HoldingPoint + hp.arrivalzone=ArrivalZone hp.uid=self.hpcounter - hp.name=string.format("%s-%d", ArrivalZone:GetName(), hp.uid) - 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.pos1=hp.pos0:Translate(UTILS.NMToMeters(Length or 15), Heading) hp.angelsmin=FlightlevelMin or 5 - hp.angelsmax=FlightlevelMax or 10 + hp.angelsmax=FlightlevelMax or 15 hp.stacks={} for i=hp.angelsmin, hp.angelsmax do @@ -381,8 +458,11 @@ function FLIGHTCONTROL:AddHoldingPoint(ArrivalZone, Heading, Length, Flightlevel table.insert(hp.stacks, stack) end + -- Add to table. table.insert(self.holdingpoints, hp) + -- Mark holding point. + hp.pos0:ArrowToAll(hp.pos1, nil, {1,0,0}, 1, {1,1,0}, 0.5, 2, true) ArrivalZone:DrawZone() return self @@ -399,8 +479,20 @@ function FLIGHTCONTROL:SetParkingGuard(TemplateGroupName) -- Need spawn with alias for multiple FCs. self.parkingGuard=SPAWN:NewWithAlias(TemplateGroupName, alias) - - --self.parkingGuard=SPAWNSTATIC:NewFromStatic("Parking Guard"):InitNamePrefix(alias) + + return self +end + +--- Set the parking guard static. This static is used to block (AI) aircraft from taxiing until they get clearance. +-- @param #FLIGHTCONTROL self +-- @param #string TemplateStaticName Name of the template static. +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:SetParkingGuardStatic(TemplateStaticName) + + local alias=string.format("Parking Guard %s", self.airbasename) + + -- Need spawn with alias for multiple FCs. + self.parkingGuard=SPAWNSTATIC:NewFromStatic(TemplateStaticName):InitNamePrefix(alias) return self end @@ -447,10 +539,45 @@ function FLIGHTCONTROL:onafterStart() self:__Status(-1) end +--- Update status. +-- @param #FLIGHTCONTROL self +function FLIGHTCONTROL:onbeforeStatus() + + if self.Tlastmessage then + local Tnow=timer.getAbsTime() + + -- Time interval between last radio message. + local dT=Tnow-self.Tlastmessage + + if dT%d sec ago. Status update allowed", dT, self.dTmessage)) + end + end + + return true +end + --- Update status. -- @param #FLIGHTCONTROL self function FLIGHTCONTROL:onafterStatus() + -- Debug message. + self:I(self.lid.."Status update") + -- Check status of all registered flights. self:_CheckFlights() @@ -463,6 +590,7 @@ function FLIGHTCONTROL:onafterStatus() -- Get runway. local runway=self:GetActiveRunway() + -- Count flights. local Nflights= self:CountFlights() local NQparking=self:CountFlights(FLIGHTCONTROL.FlightStatus.PARKING) local NQreadytx=self:CountFlights(FLIGHTCONTROL.FlightStatus.READYTX) @@ -504,19 +632,18 @@ function FLIGHTCONTROL:onafterStatus() self:__Status(-30) end ---- Start FLIGHTCONTROL FSM. Handle events. +--- Stop FLIGHTCONTROL FSM. -- @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() + -- Unhandle events. + self:UnHandleEvent(EVENTS.Birth) + self:UnHandleEvent(EVENTS.EngineStartup) + self:UnHandleEvent(EVENTS.Takeoff) + self:UnHandleEvent(EVENTS.Land) + self:UnHandleEvent(EVENTS.EngineShutdown) + self:UnHandleEvent(EVENTS.Crash) + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -686,8 +813,8 @@ function FLIGHTCONTROL:_CheckQueues() -- Holding flight -- -------------------- - -- No other flight is taking off and number of landing flights is below threshold. - if ntakeoff==0 and nlandingself.NtaxiLanding then + self:I(self.lid..string.format("AI flight %s [status=%s] NOT cleared for taxi/takeoff as %d>%d flight(s) landing", flight.groupname, status, nlanding, self.NtaxiLanding)) + return false + end + + -- Number of AI flights taxiing/takeoff. + local ntakeoff=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAKEOFF, nil, true) + + local ninbound=0 + if self.NtaxiInbound then + ninbound=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAXIINB, nil, true) + end + + if ntakeoff+ninbound>self.NtaxiTot then + self:I(self.lid..string.format("AI flight %s [status=%s] NOT cleared for taxi/takeoff as %s flight(s) taxi/takeoff", flight.groupname, status, ntakeoff)) + return false + end + + self:I(self.lid..string.format("AI flight %s [status=%s] cleared for taxi/takeoff", flight.groupname, status)) + return true + else + --- + -- Player + -- + -- We allow unlimited number of players to taxi to runway. + -- We do not allow takeoff if at least one flight is landing. + --- + + if status==FLIGHTCONTROL.FlightStatus.READYTO then + + if nlanding>0 then + -- Traffic landing. No permission to + self:I(self.lid..string.format("Player flight %s [status=%s] not cleared for taxi/takeoff as %s flight(s) landing", flight.groupname, status, nlanding)) + return false + end + + end + + self:I(self.lid..string.format("Player flight %s [status=%s] cleared for taxi/takeoff", flight.groupname, status)) + return true + end + + +end + +--- Check if a flight can get clearance for taxi/takeoff. +-- @param #FLIGHTCONTROL self +-- @param Ops.FlightGroup#FLIGHTGROUP flight Flight.. +-- @return #boolean If true, flight can. +function FLIGHTCONTROL:_CheckFlightLanding(flight) + + -- Number of groups landing. + local nlanding=self:CountFlights(FLIGHTCONTROL.FlightStatus.LANDING) + + -- Number of groups taking off. + local ntakeoff=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAKEOFF, nil, true) + + -- Current status. + local status=self:GetFlightStatus(flight) + + if flight.isAi then + --- + -- AI + --- + + if ntakeoff==0 and nlanding60 then + return fg + end end -- Sort flights by low fuel. @@ -929,8 +1189,17 @@ function FLIGHTCONTROL:_GetNextFightHolding() -- Return flight waiting longest. table.sort(Qholding, _sortByTholding) + + -- First flight in line. + local fg=Qholding[1] --Ops.FlightGroup#FLIGHTGROUP + + -- Check holding time. + local T=fg:GetHoldingTime() + if T>60 then + return fg + end - return Qholding[1] + return nil end @@ -983,7 +1252,7 @@ function FLIGHTCONTROL:_GetNextFightParking() local text="Parking flights:" for i,_flight in pairs(Qparking) do local flight=_flight --Ops.FlightGroup#FLIGHTGROUP - text=text..string.format("\n[%d] %s %.1f", i, flight.groupname, flight.Tparking) + text=text..string.format("\n[%d] %s %.1f", i, flight.groupname, flight:GetParkingTime()) end self:I(self.lid..text) @@ -1039,12 +1308,16 @@ function FLIGHTCONTROL:_PrintQueue(queue, name) parking="X" end + -- Number of elements. + local nunits=flight:CountElements() - local nunits=flight.nunits or 1 + -- Status. + local state=flight:GetState() + local status=self:GetFlightStatus(flight) -- 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) + text=text..string.format("\n[%d] %s (%s*%d): status=%s | %s, ai=%s, fuel=%d, holding=%s, parking=%s", + i, flight.groupname, actype, nunits, state, status, ai, fuel, holding, parking) -- Elements info. for j,_element in pairs(flight.elements) do @@ -1081,7 +1354,7 @@ function FLIGHTCONTROL:_RemoveFlightFromQueue(queue, flight, queuename) -- 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)) + 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 @@ -1092,7 +1365,7 @@ function FLIGHTCONTROL:_RemoveFlightFromQueue(queue, flight, queuename) end end - self:I(self.lid..string.format("Could NOT remove flight group %s from %s queue.", flight.groupname, queuename)) + self:I(self.lid..string.format("Could NOT remove flight group %s from %s queue", flight.groupname, queuename)) return false, nil end @@ -1269,8 +1542,9 @@ function FLIGHTCONTROL:_InitParkingSpots() self.parking[spot.TerminalID]=spot spot.Marker=MARKER:New(spot.Coordinate, "Spot"):ReadOnly() - spot.Marker.tocoaliton=true - spot.Marker.coalition=self:GetCoalition() + spot.Marker:ToCoalition(self:GetCoalition()) + --spot.Marker.tocoaliton=true + --spot.Marker.coalition=self:GetCoalition() -- Check if spot is initially free or occupied. if spot.Free then @@ -1374,7 +1648,7 @@ function FLIGHTCONTROL:SetParkingOccupied(spot, unitname) end ---- Get free parking spots. +--- Update parking markers. -- @param #FLIGHTCONTROL self -- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot The parking spot data table. function FLIGHTCONTROL:UpdateParkingMarker(spot) @@ -1493,11 +1767,11 @@ 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. +-- @param Core.Point#COORDINATE Coordinate Reference coordinate. +-- @param #number TerminalType (Optional) Check only this terminal type. +-- @param #boolean Status (Optional) Only consider spots that have this status. -- @return #FLIGHTCONTROL.ParkingSpot Closest parking spot. -function FLIGHTCONTROL:GetClosestParkingSpot(coordinate, terminaltype, free) +function FLIGHTCONTROL:GetClosestParkingSpot(Coordinate, TerminalType, Status) local distmin=math.huge local spotmin=nil @@ -1505,19 +1779,19 @@ function FLIGHTCONTROL:GetClosestParkingSpot(coordinate, terminaltype, free) 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 + --env.info(self.lid..string.format("FF Spot %d: %s", spot.TerminalID, spot.Status)) + + if (Status==nil or Status==spot.Status) and AIRBASE._CheckTerminalType(spot.TerminalType, TerminalType) then - -- Get distance from coordinate to spot. - local dist=coordinate:Get2DDistance(spot.Coordinate) - - -- Check if distance is smaller. - if dist0 then + text=text..string.format("\n- Parking %d", NQparking) + end + if NQreadytx>0 then + text=text..string.format("\n- Ready to taxi %d", NQreadytx) + end + if NQtaxiout>0 then + text=text..string.format("\n- Taxi to runway %d", NQtaxiout) + end + if NQreadyto>0 then + text=text..string.format("\n- Ready for takeoff %d", NQreadyto) + end + if NQtakeoff>0 then + text=text..string.format("\n- Taking off %d", NQtakeoff) + end + if NQinbound>0 then + text=text..string.format("\n- Inbound %d", NQinbound) + end + if NQholding>0 then + text=text..string.format("\n- Holding pattern %d", NQholding) + end + if NQlanding>0 then + text=text..string.format("\n- Landing %d", NQlanding) + end + if NQtaxiinb>0 then + text=text..string.format("\n- Taxi to parking %d", NQtaxiinb) + end + if NQarrived>0 then + text=text..string.format("\n- Arrived at parking %d", NQarrived) + end + -- Message to flight - self:TextMessageToFlight(text, flight, 10, true) + self:TextMessageToFlight(text, flight, 15, true) else - MESSAGE:New(string.format("Cannot find flight group %s.", tostring(groupname)), 5):ToAll() + self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) end end @@ -1931,7 +2271,7 @@ function FLIGHTCONTROL:_PlayerRequestInbound(groupname) self:TransmissionTower(text, flight, 15) -- Create player menu. - self:_CreatePlayerMenu(flight, flight.menu.atc) + flight:_UpdateMenu() else self:E(self.lid..string.format("WARNING: Could not get holding stack for flight %s", flight:GetName())) @@ -1956,7 +2296,7 @@ function FLIGHTCONTROL:_PlayerRequestInbound(groupname) end else - MESSAGE:New(string.format("Cannot find flight group %s.", tostring(groupname)), 5):ToAll() + self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) end end @@ -2012,7 +2352,7 @@ function FLIGHTCONTROL:_PlayerAbortInbound(groupname) end else - MESSAGE:New(string.format("Cannot find flight group %s.", tostring(groupname)), 5):ToAll() + self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) end end @@ -2044,7 +2384,7 @@ function FLIGHTCONTROL:_PlayerHolding(groupname) local dist=stack.pos0:Get2DDistance(Coordinate) - if dist<5000 or true then + if dist<5000 then -- Message to flight local text=string.format("Roger, you are added to the holding queue!") @@ -2081,8 +2421,9 @@ function FLIGHTCONTROL:_PlayerHolding(groupname) -- Message to flight self:TextMessageToFlight(text, flight, 10, true) end + else - --TODO: Error + self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) end end @@ -2141,7 +2482,7 @@ function FLIGHTCONTROL:_PlayerAbortHolding(groupname) end else - MESSAGE:New(string.format("Cannot find flight group %s.", tostring(groupname)), 5):ToAll() + self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) end end @@ -2192,7 +2533,8 @@ function FLIGHTCONTROL:_PlayerConfirmLanding(groupname) self:TransmissionTower(text, flight, 10) -- Create player menu. - self:_CreatePlayerMenu(flight, flight.menu.atc) + flight:_UpdateMenu() + --self:_CreatePlayerMenu(flight, flight.menu.atc) else @@ -2204,7 +2546,7 @@ function FLIGHTCONTROL:_PlayerConfirmLanding(groupname) end else - MESSAGE:New(string.format("Cannot find flight group %s.", tostring(groupname)), 5):ToAll() + self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) end end @@ -2261,7 +2603,7 @@ function FLIGHTCONTROL:_PlayerAbortLanding(groupname) end else - MESSAGE:New(string.format("Cannot find flight group %s.", tostring(groupname)), 5):ToAll() + self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) end end @@ -2284,7 +2626,7 @@ function FLIGHTCONTROL:_PlayerRequestTaxi(groupname) local callsign=flight:GetCallsignName() -- Pilot request for taxi. - local text=string.format("%s, %s, ready for departure. Request taxi to runway.", self.alias, callsign) + local text=string.format("%s, %s, request taxi to runway.", self.alias, callsign) self:TransmissionPilot(text, flight) if flight:IsParking() or flight:IsTaxiing() then @@ -2293,18 +2635,18 @@ function FLIGHTCONTROL:_PlayerRequestTaxi(groupname) local text=string.format("%s, %s, hold position until further notice.", callsign, self.alias) self:TransmissionTower(text, flight, 10) - -- Set flight status to "Ready for Take-off". + -- Set flight status to "Ready to Taxi". self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.READYTX) - self:_CreatePlayerMenu(flight, flight.menu.atc) - + -- Update menu. + flight:_UpdateMenu() + else MESSAGE:New(string.format("Negative, you must be PARKING to request TAXI!"), 5):ToAll() end else - -- Error message. - self:E(self.lid..string.format("Could not clear group %s for taxi!", groupname)) + self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) end end @@ -2335,7 +2677,8 @@ function FLIGHTCONTROL:_PlayerAbortTaxi(groupname) -- Set flight status to "Ready for Take-off". self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.PARKING) - self:_CreatePlayerMenu(flight, flight.menu.atc) + -- Update menu. + flight:_UpdateMenu() elseif flight:IsTaxiing() then @@ -2343,18 +2686,18 @@ function FLIGHTCONTROL:_PlayerAbortTaxi(groupname) local text=string.format("%s, %s, roger, return to your parking position.", callsign, self.alias) self:TransmissionTower(text, flight, 10) - -- Set flight status to "Ready for Take-off". + -- Set flight status to "Taxi Inbound". self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.TAXIINB) - self:_CreatePlayerMenu(flight, flight.menu.atc) + -- Update menu. + flight:_UpdateMenu() else MESSAGE:New(string.format("Negative, you must be PARKING or TAXIING to abort TAXI!"), 5):ToAll() end else - -- Error message. - self:E(self.lid..string.format("Could not clear group %s for taxi!", groupname)) + self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) end end @@ -2367,8 +2710,6 @@ end -- @param #FLIGHTCONTROL self -- @param #string groupname Name of the flight group. function FLIGHTCONTROL:_PlayerRequestTakeoff(groupname) - - MESSAGE:New("Request takeoff", 5):ToAll() local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP @@ -2376,25 +2717,65 @@ function FLIGHTCONTROL:_PlayerRequestTakeoff(groupname) if flight:IsTaxiing() then - local Nlanding=self:CountFlights(FLIGHTCONTROL.FlightStatus.LANDING) - local Ntakeoff=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAKEOFF) + -- Get callsign. + local callsign=flight:GetCallsignName() + + -- Pilot request for taxi. + local text=string.format("%s, %s, ready for departure. Request takeoff.", self.alias, callsign) + self:TransmissionPilot(text, flight) + -- Get number of flights landing. + local Nlanding=self:CountFlights(FLIGHTCONTROL.FlightStatus.LANDING) + + -- Get number of flights taking off. + local Ntakeoff=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAKEOFF) + + --[[ + local text="" if Nlanding==0 and Ntakeoff==0 then - MESSAGE:New("You are cleared for takeoff as there is no one else landing or queueing for takeoff", 5):ToAll() + text="No current traffic. You are cleared for takeoff." self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.TAKEOFF) + elseif Nlanding>0 and Ntakeoff>0 then + text=string.format("Negative, we got %d flights inbound and %d outbound ahead of you. Hold position until futher notice.", Nlanding, Ntakeoff) + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.READYTO) elseif Nlanding>0 then - MESSAGE:New("Negative ghostrider, other flights are currently landing. Talk to you soon.", 5):ToAll() + if Nlanding==1 then + text=string.format("Negative, we got %d flight inbound before it's your turn. Wait until futher notice.", Nlanding) + else + text=string.format("Negative, we got %d flights inbound. Wait until futher notice.", Nlanding) + end 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() + text=string.format("Negative, %d flights ahead of you are waiting for takeoff. Talk to you soon.", Ntakeoff) self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.READYTO) end + ]] - self:_CreatePlayerMenu(flight, flight.menu.atc) + -- We only check for landing flights. + local text="" + if Nlanding==0 then + text="No current traffic. You are cleared for takeoff." + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.TAKEOFF) + elseif Nlanding>0 then + if Nlanding==1 then + text=string.format("Negative, we got %d flight inbound before it's your turn. Hold position until futher notice.", Nlanding) + else + text=string.format("Negative, we got %d flights inbound. Hold positon until futher notice.", Nlanding) + end + end + + -- Message from tower. + self:TransmissionTower(text, flight, 10) + + -- Update menu. + flight:_UpdateMenu() else MESSAGE:New(string.format("Negative, you must request TAXI before you can request TAKEOFF!"), 5):ToAll() end + + else + self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) end end @@ -2408,31 +2789,42 @@ function FLIGHTCONTROL:_PlayerAbortTakeoff(groupname) local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP if flight then - - MESSAGE:New("Abort takeoff", 5):ToAll() -- Flight status. local status=self:GetFlightStatus(flight) - + + -- Check that we are taking off or ready for takeoff. if status==FLIGHTCONTROL.FlightStatus.TAKEOFF or status==FLIGHTCONTROL.FlightStatus.READYTO then + -- Get callsign. + local callsign=flight:GetCallsignName() + + -- Pilot request for taxi. + local text=string.format("%s, %s, abort takeoff.", self.alias, callsign) + self:TransmissionPilot(text, flight) + if flight:IsParking() then + text=string.format("%s, %s, affirm, remain on your parking position.", callsign, self.alias) self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.PARKING) elseif flight:IsTaxiing() then + text=string.format("%s, %s, roger, report whether you want to taxi back or takeoff later.", callsign, self.alias) self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.TAXIOUT) else env.info(self.lid.."ERROR") end - self:_CreatePlayerMenu(flight, flight.menu.atc) + -- Message from tower. + self:TransmissionTower(text, flight, 10) + -- Update menu. + flight:_UpdateMenu() else MESSAGE:New("Negative, You are NOT in the takeoff queue", 5):ToAll() end else - --TODO: Error message. + self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) end end @@ -2450,36 +2842,65 @@ function FLIGHTCONTROL:_PlayerRequestParking(groupname) local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP if flight then - - local group=flight:GetGroup() - local coord=flight:GetCoordinate() - - --TODO: terminal type for helos! - local spot=self:GetClosestParkingSpot(coord, nil, true) - - -- Get callsign. + + -- Get callsign. local callsign=flight:GetCallsignName() - -- Message text. - local text=string.format("%s, your assigned parking position is terminal ID %d. Check the F10 map for details.", callsign, spot.TerminalID) + -- Get player element. + local player=flight:GetPlayerElement() - -- Transmit message. - self:TransmissionTower(text, flight) + --TODO: Check if player has already a parking spot assigned. If so, remind him. Should we stick to it or give him a new position? - -- Create mark on F10 map. - if spot.Marker then - spot.Marker:Remove() - end - spot.Marker:SetText("Your assigned parking spot!"):ToGroup(group) - - -- Set parking of player element. - for _,_element in pairs(flight.elements) do - local element=_element --Ops.FlightGroup#FLIGHTGROUP.Element - if not element.ai then - element.parking=spot + --TODO: Check if player is currently parking on a spot. If so, he first needs to leave it. + + -- Set terminal type. + local TerminalType=AIRBASE.TerminalType.FighterAircraft + if flight.isHelo then + TerminalType=AIRBASE.TerminalType.HelicopterUsable + end + -- Current coordinate. + local coord=flight:GetCoordinate(nil, player.name) + + -- Get closest FREE parking spot. + local spot=self:GetClosestParkingSpot(coord, TerminalType, AIRBASE.SpotStatus.FREE) + + if spot then + + -- Message text. + local text=string.format("%s, your assigned parking position is terminal ID %d. Check the F10 map for details.", callsign, spot.TerminalID) + + -- Transmit message. + self:TransmissionTower(text, flight) + + -- Create mark on F10 map. + --[[ + if spot.Marker then + spot.Marker:Remove() end + spot.Marker:SetText("Your assigned parking spot!"):ReadWrite():ToGroup(flight.group) + ]] + + -- If player already has a spot. + if player.parking then + self:SetParkingFree(player.parking) + end + + -- Reserve parking for player. + player.parking=spot + self:SetParkingReserved(spot, player.name) + + else + + -- Message text. + local text=string.format("%s, no free parking spot available. Try again later.", callsign) + + -- Transmit message. + self:TransmissionTower(text, flight) + end + else + self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) end end @@ -2493,42 +2914,78 @@ function FLIGHTCONTROL:_PlayerArrived(groupname) local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP if flight then - - local group=flight:GetGroup() - local coord=flight:GetCoordinate() + + -- Player element. + local player=flight:GetPlayerElement() + + -- Get current coordinate. + local coord=flight:GetCoordinate(player.name) --Closest parking spot. - local spot=self:GetClosestParkingSpot(coord, nil, true) + local spot=self:GetClosestParkingSpot(coord) - -- Get callsign. - local callsign=flight:GetCallsignName() + if spot then - -- Message text. - local text=string.format("Tower, %s, arrived at parking position. Terminal ID %d.", callsign, spot.TerminalID) - - -- Transmit message. - self:TransmissionTower(text, flight) - - -- Set flight status to PARKING. - self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.PARKING) - - -- Create player menu. - self:_CreatePlayerMenu(flight, flight.menu.atc) - - -- Create mark on F10 map. - if spot.Marker then - spot.Marker:Remove() - end - spot.Marker:SetText("Your current parking spot!"):ToGroup(group) + -- Get callsign. + local callsign=flight:GetCallsignName() + + -- Distance to parking spot. + local dist=coord:Get2DDistance(spot.Coordinate) + + if dist<20 then + + -- Message text. + local text=string.format("%s, %s, arrived at parking position. Terminal ID %d.", self.alias, callsign, spot.TerminalID) - -- Set parking of player element. - for _,_element in pairs(flight.elements) do - local element=_element --Ops.OpsGroup#OPSGROUP.Element - if not element.ai then - element.parking=spot + -- Transmit message. + self:TransmissionPilot(text, flight) + + -- Set flight status to PARKING. + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.PARKING) + + -- Create player menu. + flight:_UpdateMenu() + + -- Create mark on F10 map. + --[[ + if spot.Marker then + spot.Marker:Remove() + end + spot.Marker:ReadWrite():SetText("Your current parking spot!"):ToGroup(flight.group) + ]] + + -- Set parking of player element. + player.parking=spot + self:SetParkingOccupied(spot, player.name) + + -- Message text. + local text=string.format("%s, %s, roger. Enjoy a cool bevarage in the officers' club.", callsign, self.alias) + + -- Transmit message. + self:TransmissionTower(text, flight, 10) + + else + + -- Message text. + local text=string.format("%s, %s, arrived at parking position", self.alias, callsign) + + -- Transmit message. + self:TransmissionPilot(text, flight) + + -- Message text. + local text=string.format("%s, %s, you are still %d meters away from the closest parking position. Continue taxiing to a proper spot!", callsign, self.alias, dist) + + -- Transmit message. + self:TransmissionTower(text, flight, 10) + end + + else + -- TODO: No spot end + else + self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) end end @@ -2819,12 +3276,17 @@ end -- @param #number Delay Delay in seconds before the text is transmitted. Default 0 sec. function FLIGHTCONTROL:TransmissionTower(Text, Flight, Delay) + -- Tower radio call. self.msrsTower:PlayText(Text, Delay) + -- "Subtitle". if Flight and not Flight.isAI then self:TextMessageToFlight(Text, Flight, 5, false, Delay) end + -- Set time stamp. Can be in the future. + self.Tlastmessage=timer.getAbsTime() + (Delay or 0) + -- Debug message. self:T(self.lid..string.format("Radio Tower: %s", Text)) @@ -2837,12 +3299,16 @@ end -- @param #number Delay Delay in seconds before the text is transmitted. Default 0 sec. function FLIGHTCONTROL:TransmissionPilot(Text, Flight, Delay) + -- Pilot radio call. self.msrsPilot:PlayText(Text, Delay) + -- "Subtitle". if Flight and not Flight.isAI then self:TextMessageToFlight(Text, Flight, 5, false, Delay) end + -- Set time stamp. + self.Tlastmessage=timer.getAbsTime() + (Delay or 0) -- Debug message. self:T(self.lid..string.format("Radio Pilot: %s", Text)) @@ -2920,7 +3386,12 @@ function FLIGHTCONTROL:SpawnParkingGuard(unit) local lookat=heading-180 -- Set heading and AI off to save resources. - self.parkingGuard:InitHeading(lookat):InitAIOff() + self.parkingGuard:InitHeading(lookat) + + -- Turn AI Off. + if self.parkingGuard:IsInstanceOf("SPAWN") then + self.parkingGuard:InitAIOff() + end -- Group that is spawned. spot.ParkingGuard=self.parkingGuard:SpawnFromCoordinate(Coordinate) diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index 3beccc6e9..fe67072c0 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -182,6 +182,18 @@ FLIGHTGROUP.RadioMessage = { TAXIING={normal="Taxiing", enhanced="Taxiing"}, } +--- Player skill. +-- @type FLIGHTGROUP.PlayerSkill +-- @field #string NOVICE Novice +FLIGHTGROUP.PlayerSkill = { + NOVICE="Novice", +} + +--- Player settings. +-- @type FLIGHTGROUP.PlayerSettings +-- @field #boolean subtitles Display subtitles. +-- @field #string skill Skill level. + --- FLIGHTGROUP class version. -- @field #string version FLIGHTGROUP.version="0.7.3" @@ -1645,8 +1657,6 @@ 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) @@ -3167,6 +3177,7 @@ function FLIGHTGROUP:_InitGroup(Template) -- Set callsign. Default is set on spawn if not modified by user. local callsign=template.units[1].callsign + self:I({callsign=callsign}) if type(callsign)=="number" then -- Sometimes callsign is just "101". local cs=tostring(callsign) callsign={} @@ -3174,8 +3185,8 @@ function FLIGHTGROUP:_InitGroup(Template) callsign[2]=cs:sub(2,2) callsign[3]=cs:sub(3,3) end - self.callsign.NumberSquad=callsign[1] - self.callsign.NumberGroup=callsign[2] + self.callsign.NumberSquad=tonumber(callsign[1]) + self.callsign.NumberGroup=tonumber(callsign[2]) self.callsign.NameSquad=UTILS.GetCallsignName(self.callsign.NumberSquad) -- Set default formation. @@ -3195,8 +3206,9 @@ function FLIGHTGROUP:_InitGroup(Template) -- Create Menu. if not self.isAI then self.menu=self.menu or {} - self.menu.atc=self.menu.atc or {} - self.menu.atc.root=self.menu.atc.root or MENU_GROUP:New(self.group, "ATC") + self.menu.atc=self.menu.atc or {} --#table + self.menu.atc.root=self.menu.atc.root or MENU_GROUP:New(self.group, "ATC") --Core.Menu#MENU_GROUP + self.menu.atc.help=self.menu.atc.help or MENU_GROUP:New(self.group, "Help", self.menu.atc.root) --Core.Menu#MENU_GROUP end -- Units of the group. @@ -3624,6 +3636,20 @@ function FLIGHTGROUP:AddWaypointLanding(Airbase, Speed, AfterWaypointWithID, Alt return waypoint end +--- Get player element. +-- @param #FLIGHTGROUP self +-- @return Ops.OpsGroup#OPSGROUP.Element The element. +function FLIGHTGROUP:GetPlayerElement() + + for _,_element in pairs(self.elements) do + local element=_element --Ops.OpsGroup#OPSGROUP.Element + if not element.ai then + return element + end + end + + return nil +end --- Set parking spot of element. -- @param #FLIGHTGROUP self @@ -4119,40 +4145,36 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- MENU FUNCTIONS ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Get the proper terminal type based on generalized attribute of the group. +--- Update menu. --@param #FLIGHTGROUP self --@param #number delay Delay in seconds. function FLIGHTGROUP:_UpdateMenu(delay) if delay and delay>0 then - self:T(self.lid..string.format("FF updating menu in %.1f sec", delay)) + -- Delayed call. self:ScheduleOnce(delay, FLIGHTGROUP._UpdateMenu, self) else - self:T(self.lid.."FF updating menu NOW") - -- Get current position of group. local position=self:GetCoordinate() -- Get all FLIGHTCONTROLS local fc={} for airbasename,_flightcontrol in pairs(_DATABASE.FLIGHTCONTROLS) do + local flightcontrol=_flightcontrol --Ops.FlightControl#FLIGHTCONTROL - local airbase=AIRBASE:FindByName(airbasename) - - local coord=airbase:GetCoordinate() + -- Get coord of airbase. + local coord=flightcontrol:GetCoordinate() + -- Distance to flight. local dist=coord:Get2DDistance(position) - local fcitem={airbasename=airbasename, dist=dist} - - table.insert(fc, fcitem) + -- Add to table. + table.insert(fc, {airbasename=airbasename, dist=dist}) end -- Sort table wrt distance to airbases. @@ -4161,18 +4183,20 @@ function FLIGHTGROUP:_UpdateMenu(delay) end table.sort(fc, _sort) - for _,_menu in pairs(self.menu.atc or {}) do - local menu=_menu - - end + -- Remove all submenus. + self.menu.atc.root:RemoveSubMenus() + + self:_CreateMenuAtcHelp(self.menu.atc.root) + + -- Max menu entries. + local N=7 -- If there is a designated FC, we put it first. - local N=8 local gotairbase=nil if self.flightcontrol then - self.flightcontrol:_CreatePlayerMenu(self, self.menu.atc) + self.flightcontrol:_CreatePlayerMenu(self, self.menu.atc.root) gotairbase=self.flightcontrol.airbasename - N=7 + N=N-1 end -- Max 8 entries in F10 menu. @@ -4180,13 +4204,88 @@ function FLIGHTGROUP:_UpdateMenu(delay) local airbasename=fc[i].airbasename if gotairbase==nil or airbasename~=gotairbase then local flightcontrol=_DATABASE:GetFlightControl(airbasename) - flightcontrol:_CreatePlayerMenu(self, self.menu.atc) + flightcontrol:_CreatePlayerMenu(self, self.menu.atc.root) end end end end +--- Create player menu. +-- @param #FLIGHTGROUP self +-- @param #table rootmenu ATC root menu table. +function FLIGHTGROUP:_CreateMenuAtcHelp(rootmenu) + + -- Help menu. + local helpmenu=MENU_GROUP:New(self.group, "Help", rootmenu) + + -- Group name. + local groupname=self.groupname + + --- + -- Skill level menu + --- + local skillmenu=MENU_GROUP:New(self.group, "Skill Level", helpmenu) + MENU_GROUP_COMMAND:New(self.group, "Beginner", skillmenu, self._MenuNotImplemented, self, groupname) + MENU_GROUP_COMMAND:New(self.group, "Student", skillmenu, self._MenuNotImplemented, self, groupname) + MENU_GROUP_COMMAND:New(self.group, "Professional", skillmenu, self._MenuNotImplemented, self, groupname) + + --- + -- Commands + --- + MENU_GROUP_COMMAND:New(self.group, "Subtitles On/Off", helpmenu, self._MenuNotImplemented, self, groupname) + MENU_GROUP_COMMAND:New(self.group, "My Voice On/Off", helpmenu, self._MenuNotImplemented, self, groupname) + MENU_GROUP_COMMAND:New(self.group, "My Status", helpmenu, self._PlayerMyStatus, self, groupname) + +end + +--- Player menu not implemented. +-- @param #FLIGHTGROUP self +-- @param #string groupname Name of the flight group. +function FLIGHTGROUP:_MenuNotImplemented(groupname) + + -- Get flight group. + local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP + + if flight then + + local text=string.format("Sorry, this feature is not implemented yet!") + + MESSAGE:New(text, 10, nil, true):ToGroup(flight.group) + --self:TextMessageToFlight(text, flight) + + end + +end + +--- Player status. +-- @param #FLIGHTGROUP self +-- @param #string groupname Name of the flight group. +function FLIGHTGROUP:_PlayerMyStatus(groupname) + + -- Get flight group. + local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP + + if flight then + + local fc=flight.flightcontrol + + -- Status text. + local text=string.format("My Status:") + text=text..string.format("\nCallsign: %s", tostring(flight:GetCallsignName())) + text=text..string.format("\nFlight status: %s", tostring(flight:GetState())) + text=text..string.format("\nFlight control: %s status=%s", tostring(fc and fc.airbasename or "N/A"), tostring(fc and fc:GetFlightStatus(flight) or "N/A")) + + -- Send message. + --self:TextMessageToFlight(text, flight, 10, true) + MESSAGE:New(text, 10, nil, true):ToGroup(flight.group) + + else + --TODO: Error + end + +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index 40855ef92..cc52c2c02 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -730,6 +730,7 @@ function OPSGROUP:New(group) ------------------------ --- Triggers the FSM event "Stop". Stops the OPSGROUP and all its event handlers. + -- @function [parent=#OPSGROUP] Stop -- @param #OPSGROUP self --- Triggers the FSM event "Stop" after a delay. Stops the OPSGROUP and all its event handlers. @@ -1610,10 +1611,11 @@ end --- Get current coordinate of the group. If the current position cannot be determined, the last known position is returned. -- @param #OPSGROUP self -- @param #boolean NewObject Create a new coordiante object. +-- @param #string UnitName (Optional) Get position of a specifc unit of the group. Default is the first existing unit in the group. -- @return Core.Point#COORDINATE The coordinate (of the first unit) of the group. -function OPSGROUP:GetCoordinate(NewObject) +function OPSGROUP:GetCoordinate(NewObject, UnitName) - local vec3=self:GetVec3() or self.position --DCS#Vec3 + local vec3=self:GetVec3(UnitName) or self.position --DCS#Vec3 if vec3 then @@ -11260,6 +11262,8 @@ function OPSGROUP:SetDefaultCallsign(CallsignName, CallsignNumber) self.callsignDefault.NumberSquad=CallsignName self.callsignDefault.NumberGroup=CallsignNumber or 1 self.callsignDefault.NameSquad=UTILS.GetCallsignName(self.callsign.NumberSquad) + + self:I(self.lid..string.format("Default callsign=%s", self.callsignDefault.NameSquad)) return self end diff --git a/Moose Development/Moose/Wrapper/Marker.lua b/Moose Development/Moose/Wrapper/Marker.lua index 3dbffa91e..462fde061 100644 --- a/Moose Development/Moose/Wrapper/Marker.lua +++ b/Moose Development/Moose/Wrapper/Marker.lua @@ -322,6 +322,16 @@ function MARKER:ReadOnly() return self end +--- Marker is readonly. Text cannot be changed and marker cannot be removed. +-- @param #MARKER self +-- @return #MARKER self +function MARKER:ReadWrite() + + self.readonly=false + + return self +end + --- Set message that is displayed on screen if the marker is added. -- @param #MARKER self -- @param #string Text Message displayed when the marker is added. From 15994e7be8c7d46e606844a2a8ebdb9cb9598736 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 20 May 2022 20:53:45 +0200 Subject: [PATCH 05/20] FC - Changed holding point to holding pattern or stack - Replaced MESSAGE by TextMessageToFlight --- Moose Development/Moose/Ops/FlightControl.lua | 54 +++++++++---------- Moose Development/Moose/Ops/FlightGroup.lua | 20 +++---- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/Moose Development/Moose/Ops/FlightControl.lua b/Moose Development/Moose/Ops/FlightControl.lua index e66bf2a2f..8a3d99321 100644 --- a/Moose Development/Moose/Ops/FlightControl.lua +++ b/Moose Development/Moose/Ops/FlightControl.lua @@ -45,7 +45,7 @@ -- @field #number Tlanding Time stamp (abs.) when last flight got landing clearance. -- @field #number Nparkingspots Total number of parking spots. -- @field Core.Spawn#SPAWN parkingGuard Parking guard spawner. --- @field #table holdingpoints Holding points. +-- @field #table holdingpatterns Holding points. -- @field #number hpcounter Counter for holding zones. -- @field Sound.SRS#MSRS msrsTower Moose SRS wrapper. -- @field Sound.SRS#MSRS msrsPilot Moose SRS wrapper. @@ -102,17 +102,17 @@ FLIGHTCONTROL = { Nlanding = nil, dTlanding = nil, Nparkingspots = nil, - holdingpoints = {}, + holdingpatterns = {}, hpcounter = 0, } --- Holding point. Contains holding stacks. --- @type FLIGHTCONTROL.HoldingPoint +-- @type FLIGHTCONTROL.HoldingPattern -- @field Core.Zone#ZONE arrivalzone Zone where aircraft should arrive. -- @field #number uid Unique ID. -- @field #string name Name of the zone, which is -. --- @field Core.Point#COORDINATE pos0 First position of racetrack holding point. --- @field Core.Point#COORDINATE pos1 Second position of racetrack holding point. +-- @field Core.Point#COORDINATE pos0 First position of racetrack holding pattern. +-- @field Core.Point#COORDINATE pos1 Second position of racetrack holding pattern. -- @field #number angelsmin Smallest holding altitude in angels. -- @field #number angelsmax Largest holding alitude in angels. -- @field #table stacks Holding stacks. @@ -121,8 +121,8 @@ FLIGHTCONTROL = { -- @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. +-- @field Core.Point#COORDINATE pos0 First position of racetrack holding pattern. +-- @field Core.Point#COORDINATE pos1 Second position of racetrack holding pattern. -- @field #number heading Heading. --- Player menu data. @@ -417,7 +417,7 @@ function FLIGHTCONTROL:SetActiveRunwayNumber(no) return self end ---- Add a holding point. +--- Add a holding pattern. -- This is a zone where the aircraft... -- @param #FLIGHTCONTROL self -- @param Core.Zone#ZONE ArrivalZone Zone where planes arrive. @@ -426,7 +426,7 @@ end -- @param #number FlightlevelMin Min flight level. Default 5. -- @param #number FlightlevelMax Max flight level. Default 15. -- @return #FLIGHTCONTROL self -function FLIGHTCONTROL:AddHoldingPoint(ArrivalZone, Heading, Length, FlightlevelMin, FlightlevelMax) +function FLIGHTCONTROL:AddHoldingPattern(ArrivalZone, Heading, Length, FlightlevelMin, FlightlevelMax) -- Get ZONE if passed as string. if type(ArrivalZone)=="string" then @@ -436,7 +436,7 @@ function FLIGHTCONTROL:AddHoldingPoint(ArrivalZone, Heading, Length, Flightlevel -- Increase counter. self.hpcounter=self.hpcounter+1 - local hp={} --#FLIGHTCONTROL.HoldingPoint + local hp={} --#FLIGHTCONTROL.HoldingPattern hp.arrivalzone=ArrivalZone hp.uid=self.hpcounter hp.name=string.format("%s-%d", ArrivalZone:GetName(), hp.uid) @@ -459,9 +459,9 @@ function FLIGHTCONTROL:AddHoldingPoint(ArrivalZone, Heading, Length, Flightlevel end -- Add to table. - table.insert(self.holdingpoints, hp) + table.insert(self.holdingpatterns, hp) - -- Mark holding point. + -- Mark holding pattern. hp.pos0:ArrowToAll(hp.pos1, nil, {1,0,0}, 1, {1,1,0}, 0.5, 2, true) ArrivalZone:DrawZone() @@ -2242,7 +2242,7 @@ function FLIGHTCONTROL:_PlayerRequestInbound(groupname) self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.INBOUND) -- Get holding point. - local stack=self:_GetHoldingpoint(flight) + local stack=self:_GetHoldingStack(flight) if stack then @@ -2642,7 +2642,7 @@ function FLIGHTCONTROL:_PlayerRequestTaxi(groupname) flight:_UpdateMenu() else - MESSAGE:New(string.format("Negative, you must be PARKING to request TAXI!"), 5):ToAll() + self:TextMessageToFlight(string.format("Negative, you must be PARKING to request TAXI!"), flight) end else @@ -2693,7 +2693,7 @@ function FLIGHTCONTROL:_PlayerAbortTaxi(groupname) flight:_UpdateMenu() else - MESSAGE:New(string.format("Negative, you must be PARKING or TAXIING to abort TAXI!"), 5):ToAll() + self:TextMessageToFlight(string.format("Negative, you must be PARKING or TAXIING to abort TAXI!"), flight) end else @@ -2771,7 +2771,7 @@ function FLIGHTCONTROL:_PlayerRequestTakeoff(groupname) flight:_UpdateMenu() else - MESSAGE:New(string.format("Negative, you must request TAXI before you can request TAKEOFF!"), 5):ToAll() + self:TextMessageToFlight(string.format("Negative, you must request TAXI before you can request TAKEOFF!"), flight) end else @@ -2820,7 +2820,7 @@ function FLIGHTCONTROL:_PlayerAbortTakeoff(groupname) flight:_UpdateMenu() else - MESSAGE:New("Negative, You are NOT in the takeoff queue", 5):ToAll() + self:TextMessageToFlight("Negative, You are NOT in the takeoff queue", flight) end else @@ -3207,7 +3207,7 @@ function FLIGHTCONTROL:_LandAI(flight, parking) end -- Debug message. - MESSAGE:New(string.format("Respawning group %s", flight.groupname)):ToAll() + self:TextMessageToFlight(string.format("Respawning group %s", flight.groupname), flight) --Respawn the group. flight:Respawn(Template) @@ -3221,14 +3221,14 @@ function FLIGHTCONTROL:_LandAI(flight, parking) end ---- Get holding point. +--- Get holding stack. -- @param #FLIGHTCONTROL self -- @param Ops.FlightGroup#FLIGHTGROUP flight Flight group. -- @return #FLIGHTCONTROL.HoldingStack Holding point. -function FLIGHTCONTROL:_GetHoldingpoint(flight) +function FLIGHTCONTROL:_GetHoldingStack(flight) --[[ - local holdingpoint={} --#FLIGHTCONTROL.HoldingPoint + local holdingpattern={} --#FLIGHTCONTROL.HoldingPattern local runway=self:GetActiveRunway() @@ -3238,20 +3238,20 @@ function FLIGHTCONTROL:_GetHoldingpoint(flight) 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) + holdingpattern.pos0=runway.position:Translate(dx, hdg):SetAltitude(angels) + holdingpattern.pos1=holdingpattern.pos0:Translate(dz, runway.heading):SetAltitude(angels) ]] -- Debug message. self:T(self.lid..string.format("Getting holding point for flight %s", flight:GetName())) - for i,_hp in pairs(self.holdingpoints) do - local holdingpoint=_hp --#FLIGHTCONTROL.HoldingPoint + for i,_hp in pairs(self.holdingpatterns) do + local holdingpattern=_hp --#FLIGHTCONTROL.HoldingPattern - self:T(self.lid..string.format("Checking holding point %s", holdingpoint.name)) + self:T(self.lid..string.format("Checking holding point %s", holdingpattern.name)) - for j,_stack in pairs(holdingpoint.stacks) do + for j,_stack in pairs(holdingpattern.stacks) do local stack=_stack --#FLIGHTCONTROL.HoldingStack local name=stack.flightgroup and stack.flightgroup:GetName() or "empty" self:T(self.lid..string.format("Stack %d: %s", j, name)) diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index fe67072c0..b1d8e5396 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -2556,22 +2556,22 @@ function FLIGHTGROUP:_LandAtAirbase(airbase, SpeedTo, SpeedHold, SpeedLand) if fc and self.isAI then - -- Get holding point from flight control. - local HoldingPoint=fc:_GetHoldingpoint(self) + -- Get holding stack from flight control. + local stack=fc:_GetHoldingStack(self) - if HoldingPoint then + if stack then - HoldingPoint.flightgroup=self - self.stack=HoldingPoint + stack.flightgroup=self + self.stack=stack -- Race track points. - p0=HoldingPoint.pos0 - p1=HoldingPoint.pos1 + p0=stack.pos0 + p1=stack.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)) + if false then + p0:MarkToAll(string.format("%s: Holding stack P0, alt=%d meters", self:GetName(), p0.y)) + p1:MarkToAll(string.format("%s: Holding stack P1, alt=%d meters", self:GetName(), p0.y)) end else From dd81823e29db68ad2501f6799e3bc60a32bc53ec Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 21 May 2022 22:25:08 +0200 Subject: [PATCH 06/20] FC **FLIGHTCONTROL** - Less output to dcs log file **FLIGHTGROUP** - Added function :SetReadyForTakeoff --- .../Moose/Functional/Warehouse.lua | 9 +- Moose Development/Moose/Ops/FlightControl.lua | 197 ++++++------------ Moose Development/Moose/Ops/FlightGroup.lua | 15 ++ Moose Development/Moose/Ops/OpsGroup.lua | 5 + 4 files changed, 93 insertions(+), 133 deletions(-) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 42e8d61cb..7a44c2820 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -6311,10 +6311,11 @@ function WAREHOUSE:_RouteAir(aircraft) self:T2(self.lid..string.format("RouteAir aircraft group %s alive=%s", aircraft:GetName(), tostring(aircraft:IsAlive()))) -- Give start command to activate uncontrolled aircraft within the next 60 seconds. - if not self.flightcontrol then - local starttime=math.random(60) - - aircraft:StartUncontrolled(starttime) + if self.flightcontrol then + local fg=FLIGHTGROUP:New(aircraft) + fg:SetReadyForTakeoff(true) + else + aircraft:StartUncontrolled(math.random(60)) end -- Debug info. diff --git a/Moose Development/Moose/Ops/FlightControl.lua b/Moose Development/Moose/Ops/FlightControl.lua index 8a3d99321..cd0a2305f 100644 --- a/Moose Development/Moose/Ops/FlightControl.lua +++ b/Moose Development/Moose/Ops/FlightControl.lua @@ -20,7 +20,7 @@ --- FLIGHTCONTROL class. -- @type FLIGHTCONTROL -- @field #string ClassName Name of the class. --- @field #boolean Debug Debug mode. Messages to all about status. +-- @field #boolean verbose Verbosity level. -- @field #string theatre The DCS map used in the mission. -- @field #string lid Class id string for output to DCS log file. -- @field #string airbasename Name of airbase. @@ -83,7 +83,7 @@ -- @field #FLIGHTCONTROL FLIGHTCONTROL = { ClassName = "FLIGHTCONTROL", - Debug = false, + verbose = 0, lid = nil, theatre = nil, airbasename = nil, @@ -312,6 +312,15 @@ end -- User API Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Set verbosity level. +-- @param #FLIGHTCONTROL self +-- @param #number VerbosityLevel Level of output (higher=more). Default 0. +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:SetVerbosity(VerbosityLevel) + self.verbose=VerbosityLevel or 0 + return self +end + --- Set the tower frequency. -- @param #FLIGHTCONTROL self -- @param #number Frequency Frequency in MHz. Default 305 MHz. @@ -322,9 +331,9 @@ function FLIGHTCONTROL:SetFrequency(Frequency, Modulation) self.frequency=Frequency or 305 self.modulation=Modulation or radio.modulation.AM + return self end - --- Set the number of aircraft groups, that are allowed to land simultaniously. -- Note that this restricts AI and human players. -- @param #FLIGHTCONTROL self @@ -380,43 +389,6 @@ function FLIGHTCONTROL:SetLimitTaxi(Ntaxi, IncludeInbound, Nlanding) 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 a holding pattern. -- This is a zone where the aircraft... -- @param #FLIGHTCONTROL self @@ -425,7 +397,7 @@ end -- @param #number Length Length in nautical miles. Default 15 NM. -- @param #number FlightlevelMin Min flight level. Default 5. -- @param #number FlightlevelMax Max flight level. Default 15. --- @return #FLIGHTCONTROL self +-- @return #FLIGHTCONTROL.HoldingPattern Holding pattern table. function FLIGHTCONTROL:AddHoldingPattern(ArrivalZone, Heading, Length, FlightlevelMin, FlightlevelMax) -- Get ZONE if passed as string. @@ -556,7 +528,7 @@ function FLIGHTCONTROL:onbeforeStatus() -- Debug info. local text=string.format("Last message sent %d sec ago. Will call status again in %d sec", dT, dt) - self:I(self.lid..text) + self:T(self.lid..text) -- Call status again in dt seconds. self:__Status(-dt) @@ -564,7 +536,7 @@ function FLIGHTCONTROL:onbeforeStatus() -- Deny transition. return false else - self:I(self.lid..string.format("Last radio sent %d>%d sec ago. Status update allowed", dT, self.dTmessage)) + self:T2(self.lid..string.format("Last radio sent %d>%d sec ago. Status update allowed", dT, self.dTmessage)) end end @@ -576,7 +548,7 @@ end function FLIGHTCONTROL:onafterStatus() -- Debug message. - self:I(self.lid.."Status update") + self:T2(self.lid.."Status update") -- Check status of all registered flights. self:_CheckFlights() @@ -618,9 +590,11 @@ function FLIGHTCONTROL:onafterStatus() 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 self.verbose>=1 then + 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) + end if Nflights==Nqueues then --Check! @@ -698,7 +672,7 @@ 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))) + self:T3(self.lid..string.format("LAND: group = %s", tostring(EventData.IniGroupName))) end @@ -709,7 +683,7 @@ 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))) + self:T3(self.lid..string.format("TAKEOFF: group = %s", tostring(EventData.IniGroupName))) -- This would be the closest airbase. local airbase=EventData.Place @@ -731,17 +705,9 @@ end 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 - + self:T2(self.lid..string.format("ENGINESTARTUP: unit = %s", tostring(EventData.IniUnitName))) + self:T3(self.lid..string.format("ENGINESTARTUP: group = %s", tostring(EventData.IniGroupName))) + end --- Event handler for event engine shutdown. @@ -750,16 +716,8 @@ end 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 + self:T2(self.lid..string.format("ENGINESHUTDOWN: unit = %s", tostring(EventData.IniUnitName))) + self:T3(self.lid..string.format("ENGINESHUTDOWN: group = %s", tostring(EventData.IniGroupName))) end @@ -770,7 +728,7 @@ 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))) + self:T3(self.lid..string.format("CRASH: group = %s", tostring(EventData.IniGroupName))) end @@ -783,23 +741,10 @@ end function FLIGHTCONTROL:_CheckQueues() -- Print queue. - if true then + if self.verbose>=2 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() @@ -856,7 +801,7 @@ function FLIGHTCONTROL:_CheckQueues() end else - self:T3(self.lid..string.format("FYI: Landing clearance for flight %s denied")) + self:T3(self.lid..string.format("FYI: Landing clearance for flight %s denied", flight.groupname)) end else @@ -954,12 +899,12 @@ function FLIGHTCONTROL:_CheckQueues() 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)) + self:T3(self.lid..string.format("FYI: Take off for flight %s denied", flight.groupname)) end end else -- Debug message. - self:I(self.lid..string.format("FYI: No flight in queue for takeoff or landing.")) + self:T2(self.lid..string.format("FYI: No flight in queue for takeoff or landing")) end end @@ -970,7 +915,6 @@ end -- @return #boolean If true, flight can. function FLIGHTCONTROL:_CheckFlightTakeoff(flight) - -- Number of groups landing. local nlanding=self:CountFlights(FLIGHTCONTROL.FlightStatus.LANDING) @@ -986,24 +930,21 @@ function FLIGHTCONTROL:_CheckFlightTakeoff(flight) --- if nlanding>self.NtaxiLanding then - self:I(self.lid..string.format("AI flight %s [status=%s] NOT cleared for taxi/takeoff as %d>%d flight(s) landing", flight.groupname, status, nlanding, self.NtaxiLanding)) + self:T(self.lid..string.format("AI flight %s [status=%s] NOT cleared for taxi/takeoff as %d>%d flight(s) landing", flight.groupname, status, nlanding, self.NtaxiLanding)) return false end - -- Number of AI flights taxiing/takeoff. - local ntakeoff=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAKEOFF, nil, true) - local ninbound=0 if self.NtaxiInbound then ninbound=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAXIINB, nil, true) end - if ntakeoff+ninbound>self.NtaxiTot then - self:I(self.lid..string.format("AI flight %s [status=%s] NOT cleared for taxi/takeoff as %s flight(s) taxi/takeoff", flight.groupname, status, ntakeoff)) + if ntakeoff+ninbound>=self.NtaxiTot then + self:T(self.lid..string.format("AI flight %s [status=%s] NOT cleared for taxi/takeoff as %d>=%d flight(s) taxi/takeoff", flight.groupname, status, ntakeoff, self.NtaxiTot)) return false end - self:I(self.lid..string.format("AI flight %s [status=%s] cleared for taxi/takeoff", flight.groupname, status)) + self:T(self.lid..string.format("AI flight %s [status=%s] cleared for taxi/takeoff! nLanding=%d, nTakeoff=%d", flight.groupname, status, nlanding, ntakeoff)) return true else --- @@ -1015,15 +956,15 @@ function FLIGHTCONTROL:_CheckFlightTakeoff(flight) if status==FLIGHTCONTROL.FlightStatus.READYTO then - if nlanding>0 then + if nlanding>self.NtaxiLanding then -- Traffic landing. No permission to - self:I(self.lid..string.format("Player flight %s [status=%s] not cleared for taxi/takeoff as %s flight(s) landing", flight.groupname, status, nlanding)) + self:T(self.lid..string.format("Player flight %s [status=%s] not cleared for taxi/takeoff as %d>%d flight(s) landing", flight.groupname, status, nlanding, self.NtaxiLanding)) return false end end - self:I(self.lid..string.format("Player flight %s [status=%s] cleared for taxi/takeoff", flight.groupname, status)) + self:T(self.lid..string.format("Player flight %s [status=%s] cleared for taxi/takeoff", flight.groupname, status)) return true end @@ -1050,7 +991,7 @@ function FLIGHTCONTROL:_CheckFlightLanding(flight) -- AI --- - if ntakeoff==0 and nlanding=2 then + local text="Parking flights:" + for i,_flight in pairs(Qparking) do + local flight=_flight --Ops.FlightGroup#FLIGHTGROUP + text=text..string.format("\n[%d] %s %.1f", i, flight.groupname, flight:GetParkingTime()) + end + self:I(self.lid..text) end - self:I(self.lid..text) -- Get the first AI flight. for i,_flight in pairs(Qparking) do local flight=_flight --Ops.FlightGroup#FLIGHTGROUP - if flight.isAI then + if flight.isAI and flight.isReadyTO then return flight end end @@ -1354,7 +1295,7 @@ function FLIGHTCONTROL:_RemoveFlightFromQueue(queue, flight, queuename) -- 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)) + self:T(self.lid..string.format("Removing flight group %s from %s queue", flight.groupname, queuename)) table.remove(queue, i) if not flight.isAI then @@ -1365,7 +1306,7 @@ function FLIGHTCONTROL:_RemoveFlightFromQueue(queue, flight, queuename) end end - self:I(self.lid..string.format("Could NOT remove flight group %s from %s queue", flight.groupname, queuename)) + self:E(self.lid..string.format("WARNING: Could NOT remove flight group %s from %s queue", flight.groupname, queuename)) return false, nil end @@ -1532,8 +1473,7 @@ function FLIGHTCONTROL:_InitParkingSpots() 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) @@ -1541,10 +1481,8 @@ function FLIGHTCONTROL:_InitParkingSpots() -- Add to table. self.parking[spot.TerminalID]=spot - spot.Marker=MARKER:New(spot.Coordinate, "Spot"):ReadOnly() - spot.Marker:ToCoalition(self:GetCoalition()) - --spot.Marker.tocoaliton=true - --spot.Marker.coalition=self:GetCoalition() + -- Marker. + spot.Marker=MARKER:New(spot.Coordinate, "Spot"):ReadOnly():ToCoalition(self:GetCoalition()) -- Check if spot is initially free or occupied. if spot.Free then @@ -1580,12 +1518,12 @@ function FLIGHTCONTROL:_InitParkingSpots() --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) + self:SetParkingFree(spot) end else - self:I(self.lid..string.format("ERROR: Parking spot is NOT FREE but no unit could be found there!")) + self:E(self.lid..string.format("ERROR: Parking spot is NOT FREE but no unit could be found there!")) end end @@ -1846,7 +1784,7 @@ function FLIGHTCONTROL:_CreatePlayerMenu(flight, mainmenu) local gotcontrol=self:IsControlling(flight) -- Debug info. - self:I(self.lid..string.format("Creating ATC player menu for flight %s: in state=%s status=%s, gotcontrol=%s", tostring(flight.groupname), flight:GetState(), flightstatus, tostring(gotcontrol))) + self:T(self.lid..string.format("Creating ATC player menu for flight %s: in state=%s status=%s, gotcontrol=%s", tostring(flight.groupname), flight:GetState(), flightstatus, tostring(gotcontrol))) -- Airbase root menu. @@ -3007,7 +2945,7 @@ function FLIGHTCONTROL:_CreateFlightGroup(group) end -- Debug info. - self:I(self.lid..string.format("Creating new flight for group %s of aircraft type %s.", group:GetName(), group:GetTypeName())) + self:T(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()) @@ -3113,7 +3051,7 @@ function FLIGHTCONTROL:_CheckFlights() 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:T(self.lid..string.format("Removing DEAD flight %s", tostring(flight.groupname))) self:_RemoveFlight(flight) end end @@ -3166,7 +3104,7 @@ end function FLIGHTCONTROL:_LandAI(flight, parking) -- Debug info. - self:I(self.lid..string.format("Landing AI flight %s.", flight.groupname)) + self:T(self.lid..string.format("Landing AI flight %s.", flight.groupname)) -- Set flight status to LANDING. self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.LANDING) @@ -3195,8 +3133,8 @@ function FLIGHTCONTROL:_LandAI(flight, parking) -- 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) + local text=string.format("Reserving parking spot %d for unit %s", spot.TerminalID, tostring(unit.name)) + self:T(self.lid..text) -- Set parking to RESERVED. self:SetParkingReserved(spot, element.name) @@ -3377,7 +3315,8 @@ function FLIGHTCONTROL:SpawnParkingGuard(unit) -- 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)) + -- Debug message. + self:T2(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) diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index b1d8e5396..431988c9b 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -55,6 +55,7 @@ -- @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. +-- @field #boolean isReadyTO Flight is ready for takeoff. This is for FLIGHTCONTROL. -- -- @extends Ops.OpsGroup#OPSGROUP @@ -370,6 +371,20 @@ function FLIGHTGROUP:SetVTOL() return self end +--- Set if group is ready for taxi/takeoff if controlled by a `FLIGHTCONTROL`. +-- @param #FLIGHTGROUP self +-- @param #boolean ReadyTO If `true`, flight is ready for takeoff. +-- @param #number Delay Delay in seconds before value is set. Default 0 sec. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetReadyForTakeoff(ReadyTO, Delay) + if Delay and Delay>0 then + self:ScheduleOnce(Delay, FLIGHTGROUP.SetReadyForTakeoff, self, ReadyTO, 0) + else + self.isReadyTO=ReadyTO + end + return self +end + --- Set the FLIGHTCONTROL controlling this flight group. -- @param #FLIGHTGROUP self -- @param Ops.FlightControl#FLIGHTCONTROL flightcontrol The FLIGHTCONTROL object. diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index cc52c2c02..741536721 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -4774,6 +4774,11 @@ function OPSGROUP:onafterMissionStart(From, Event, To, Mission) -- Set mission status to STARTED. Mission:__Started(3) + + -- Set ready for takeoff in case of FLIGHTCONTROL. + if self.isFlightgroup and Mission.type~=AUFTRAG.Type.ALERT5 then + FLIGHTGROUP.SetReadyForTakeoff(self, true) + end -- Route group to mission zone. if self.speedMax>3.6 or true then From 06d509b5ac3f01604a5472a9ac86cfd6d8719824 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 22 May 2022 22:36:43 +0200 Subject: [PATCH 07/20] Update Airboss.lua - Improved Case III entry waypoint --- Moose Development/Moose/Ops/Airboss.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index bc0838da5..2324577b8 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -6059,7 +6059,7 @@ function AIRBOSS:_MarshalAI( flight, nstack, respawn ) local radial = self:GetRadial( case, false, true ) -- Point in the middle of the race track and a 5 NM more port perpendicular. - p0 = p2:Translate( UTILS.NMToMeters( 5 ), radial + 90 ):Translate( UTILS.NMToMeters( 5 ), radial, true ) + p0 = p2:Translate( UTILS.NMToMeters( 5 ), radial + 90, true ):Translate( UTILS.NMToMeters( 5 ), radial, true ) -- Entering Case II/III marshal pattern waypoint. wp[#wp + 1] = p0:WaypointAirTurningPoint( nil, speedTransit, { TaskArrivedHolding }, "Entering Case II/III Marshal Pattern" ) From 62725b1930ef445ece9b370ed9259fa97796cf2d Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 23 May 2022 23:11:23 +0200 Subject: [PATCH 08/20] OPS **AUFTRAG** - Added *invisible* and *immortal* options **TARGET** - Added `AddResource` function **OPSGROUP** - Added *invisible* and *immortal* options **LEGION** - Fixed bug in properties requirement **COMMANDER** - Added `AddTarget` function (still **WIP**) **ARMYGROUP** - Fixed routing bug after teleporting --- Moose Development/Moose/Ops/ArmyGroup.lua | 15 +- Moose Development/Moose/Ops/Auftrag.lua | 41 ++++- Moose Development/Moose/Ops/Commander.lua | 143 +++++++++++++++++ Moose Development/Moose/Ops/FlightGroup.lua | 8 +- Moose Development/Moose/Ops/Legion.lua | 8 +- Moose Development/Moose/Ops/NavyGroup.lua | 13 +- Moose Development/Moose/Ops/OpsGroup.lua | 160 +++++++++++++++++--- Moose Development/Moose/Ops/OpsZone.lua | 34 ++++- Moose Development/Moose/Ops/Target.lua | 58 +++++++ 9 files changed, 448 insertions(+), 32 deletions(-) diff --git a/Moose Development/Moose/Ops/ArmyGroup.lua b/Moose Development/Moose/Ops/ArmyGroup.lua index 35ba2606f..7102a7aa0 100644 --- a/Moose Development/Moose/Ops/ArmyGroup.lua +++ b/Moose Development/Moose/Ops/ArmyGroup.lua @@ -63,7 +63,7 @@ ARMYGROUP = { --- Army Group version. -- @field #string version -ARMYGROUP.version="0.7.3" +ARMYGROUP.version="0.7.9" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -880,6 +880,12 @@ function ARMYGROUP:onafterSpawned(From, Event, To) -- Set default EPLRS. self:SwitchEPLRS(self.option.EPLRS) + + -- Set default Invisible. + self:SwitchInvisible(self.option.Invisible) + + -- Set default Immortal. + self:SwitchImmortal(self.option.Immortal) -- Set TACAN to default. self:_SwitchTACAN() @@ -943,6 +949,9 @@ function ARMYGROUP:onbeforeUpdateRoute(From, Event, To, n, N, Speed, Formation) elseif self:IsHolding() then self:T(self.lid.."Update route denied. Group is holding position!") return false + elseif self:IsEngaging() then + self:T(self.lid.."Update route allowed. Group is engaging!") + return true end -- Check for a current task. @@ -960,7 +969,7 @@ function ARMYGROUP:onbeforeUpdateRoute(From, Event, To, n, N, Speed, Formation) self:T2(self.lid.."Allowing update route for Task: ReconMission") elseif task.dcstask.id==AUFTRAG.SpecialTask.RELOCATECOHORT then -- For relocate - self:T2(self.lid.."Allowing update route for Task: Relocate Cohort") + self:T2(self.lid.."Allowing update route for Task: Relocate Cohort") else local taskname=task and task.description or "No description" self:T(self.lid..string.format("WARNING: Update route denied because taskcurrent=%d>0! Task description = %s", self.taskcurrent, tostring(taskname))) @@ -1117,7 +1126,7 @@ function ARMYGROUP:onafterUpdateRoute(From, Event, To, n, N, Speed, Formation) self.speedWp=wp.speed -- Debug output. - if self.verbose>=10 then + if self.verbose>=10 or true then for i,_wp in pairs(waypoints) do local wp=_wp --Ops.OpsGroup#OPSGROUP.Waypoint diff --git a/Moose Development/Moose/Ops/Auftrag.lua b/Moose Development/Moose/Ops/Auftrag.lua index a1e46b10b..2361a21d5 100644 --- a/Moose Development/Moose/Ops/Auftrag.lua +++ b/Moose Development/Moose/Ops/Auftrag.lua @@ -111,6 +111,9 @@ -- @field #number artyAngle Shooting angle in degrees (for Barrage). -- -- @field #string alert5MissionType Alert 5 mission type. This is the mission type, the alerted assets will be able to carry out. +-- +-- @field #table attributes Generalized attribute(s) of assets. +-- @field #table properties DCS attribute(s) of assets. -- -- @field Ops.Chief#CHIEF chief The CHIEF managing this mission. -- @field Ops.Commander#COMMANDER commander The COMMANDER managing this mission. @@ -168,6 +171,8 @@ -- @field #number optionRTBfuel RTB on out-of-fuel. -- @field #number optionECM ECM. -- @field #boolean optionEmission Emission is on or off. +-- @field #boolean optionInvisible Invisible is on/off. +-- @field #boolean optionImmortal Immortal is on/off. -- -- @extends Core.Fsm#FSM @@ -1763,7 +1768,7 @@ end --- **[GROUND, NAVAL]** Create an ARTY mission. -- @param #AUFTRAG self -- @param Core.Point#COORDINATE Target Center of the firing solution. --- @param #number Nshots Number of shots to be fired. Default 3. +-- @param #number Nshots Number of shots to be fired. Default `#nil`. -- @param #number Radius Radius of the shells in meters. Default 100 meters. -- @param #number Altitude Altitude in meters. Can be used to setup a Barrage. Default `#nil`. -- @return #AUFTRAG self @@ -2196,7 +2201,9 @@ function AUFTRAG:NewFromTarget(Target, MissionType) elseif MissionType==AUFTRAG.Type.STRIKE then mission=self:NewSTRIKE(Target, Altitude) elseif MissionType==AUFTRAG.Type.ARMORATTACK then - mission=self:NewARMORATTACK(Target,Speed) + mission=self:NewARMORATTACK(Target, Speed) + elseif MissionType==AUFTRAG.Type.GROUNDATTACK then + mission=self:NewGROUNDATTACK(Target, Speed, Formation) else return nil end @@ -2970,6 +2977,36 @@ function AUFTRAG:SetEmission(OnOffSwitch) return self end +--- Set invisibility setting for this mission. +-- @param #AUFTRAG self +-- @param #boolean OnOffSwitch If `true` or `nil`, invisible is on. If `false`, invisible is off. +-- @return #AUFTRAG self +function AUFTRAG:SetInvisible(OnOffSwitch) + + if OnOffSwitch==nil then + self.optionInvisible=true + else + self.optionInvisible=OnOffSwitch + end + + return self +end + +--- Set immortality setting for this mission. +-- @param #AUFTRAG self +-- @param #boolean OnOffSwitch If `true` or `nil`, immortal is on. If `false`, immortal is off. +-- @return #AUFTRAG self +function AUFTRAG:SetImmortal(OnOffSwitch) + + if OnOffSwitch==nil then + self.optionImmortal=true + else + self.optionImmortal=OnOffSwitch + end + + return self +end + --- Set formation for this mission. -- @param #AUFTRAG self -- @param #number Formation Formation. diff --git a/Moose Development/Moose/Ops/Commander.lua b/Moose Development/Moose/Ops/Commander.lua index 5049e05e9..0ec0e9886 100644 --- a/Moose Development/Moose/Ops/Commander.lua +++ b/Moose Development/Moose/Ops/Commander.lua @@ -24,6 +24,7 @@ -- @field #table legions Table of legions which are commanded. -- @field #table missionqueue Mission queue. -- @field #table transportqueue Transport queue. +-- @field #table targetqueue Target queue. -- @field #table rearmingZones Rearming zones. Each element is of type `#BRIGADE.SupplyZone`. -- @field #table refuellingZones Refuelling zones. Each element is of type `#BRIGADE.SupplyZone`. -- @field #table capZones CAP zones. Each element is of type `#AIRWING.PatrolZone`. @@ -125,6 +126,7 @@ COMMANDER = { legions = {}, missionqueue = {}, transportqueue = {}, + targetqueue = {}, rearmingZones = {}, refuellingZones = {}, capZones = {}, @@ -514,6 +516,55 @@ function COMMANDER:RemoveTransport(Transport) return self end +--- Add target. +-- @param #COMMANDER self +-- @param Ops.Target#TARGET Target Target object to be added. +-- @return #COMMANDER self +function COMMANDER:AddTarget(Target) + + if not self:IsTarget(Target) then + table.insert(self.targetqueue, Target) + end + + return self +end + +--- Check if a TARGET is already in the queue. +-- @param #COMMANDER self +-- @param Ops.Target#TARGET Target Target object to be added. +-- @return #boolean If `true`, target exists in the target queue. +function COMMANDER:IsTarget(Target) + + for _,_target in pairs(self.targetqueue) do + local target=_target --Ops.Target#TARGET + if target.uid==Target.uid or target:GetName()==Target:GetName() then + return true + end + end + + return false +end + +--- Remove target from queue. +-- @param #COMMANDER self +-- @param Ops.Target#TARGET Target The target. +-- @return #COMMANDER self +function COMMANDER:RemoveTarget(Target) + + for i,_target in pairs(self.targetqueue) do + local target=_target --Ops.Target#TARGET + + if target.uid==Target.uid then + self:T(self.lid..string.format("Removing target %s from queue", Target.name)) + table.remove(self.targetqueue, i) + break + end + + end + + return self +end + --- Add a rearming zone. -- @param #COMMANDER self -- @param Core.Zone#ZONE RearmingZone Rearming zone. @@ -789,6 +840,9 @@ function COMMANDER:onafterStatus(From, Event, To) local text=string.format("Status %s: Legions=%d, Missions=%d, Transports", fsmstate, #self.legions, #self.missionqueue, #self.transportqueue) self:T(self.lid..text) end + + -- Check target queue and add missions. + self:CheckTargetQueue() -- Check mission queue and assign one PLANNED mission. self:CheckMissionQueue() @@ -1148,6 +1202,95 @@ end -- Mission Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Check target queue and assign ONE valid target by adding it to the mission queue of the COMMANDER. +-- @param #COMMANDER self +function COMMANDER:CheckTargetQueue() + + -- Number of missions. + local Ntargets=#self.targetqueue + + -- Treat special cases. + if Ntargets==0 then + return nil + end + + -- Check if total number of missions is reached. + local NoLimit=self:_CheckMissionLimit("Total") + if NoLimit==false then + return nil + end + + -- Sort results table wrt prio and threatlevel. + local function _sort(a, b) + local taskA=a --Ops.Target#TARGET + local taskB=b --Ops.Target#TARGET + return (taskA.priotaskB.threatlevel0) + end + table.sort(self.targetqueue, _sort) + + -- Get the lowest importance value (lower means more important). + -- If a target with importance 1 exists, targets with importance 2 will not be assigned. Targets with no importance (nil) can still be selected. + local vip=math.huge + for _,_target in pairs(self.targetqueue) do + local target=_target --Ops.Target#TARGET + if target:IsAlive() and target.importance and target.importance Creating mission type %s: Nmin=%d, Nmax=%d", target:GetName(), missionType, resource.Nmin, resource.Nmax)) + + -- Create a mission. + local mission=AUFTRAG:NewFromTarget(target, missionType) + + if mission then + mission:SetRequiredAssets(resource.Nmin, resource.Nmax) + mission:SetRequiredAttribute(resource.Attributes) + mission:SetRequiredProperty(resource.Properties) + + resource.mission=mission + + -- Add mission to queue. + self:AddMission(resource.mission) + + end + + end + + end + + end + end + +end + + --- Check mission queue and assign ONE planned mission. -- @param #COMMANDER self function COMMANDER:CheckMissionQueue() diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index 50f9116fa..f94433a41 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -183,7 +183,7 @@ FLIGHTGROUP.RadioMessage = { --- FLIGHTGROUP class version. -- @field #string version -FLIGHTGROUP.version="0.7.3" +FLIGHTGROUP.version="0.7.9" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1549,6 +1549,12 @@ function FLIGHTGROUP:onafterSpawned(From, Event, To) -- Set default EPLRS. self:SwitchEPLRS(self.option.EPLRS) + + -- Set default Invisible. + self:SwitchInvisible(self.option.Invisible) + + -- Set default Immortal. + self:SwitchImmortal(self.option.Immortal) -- Set Formation self:SwitchFormation(self.option.Formation) diff --git a/Moose Development/Moose/Ops/Legion.lua b/Moose Development/Moose/Ops/Legion.lua index 66888cb66..b0076b4f7 100644 --- a/Moose Development/Moose/Ops/Legion.lua +++ b/Moose Development/Moose/Ops/Legion.lua @@ -2246,7 +2246,7 @@ function LEGION.RecruitCohortAssets(Cohorts, MissionTypeRecruit, MissionTypeOpt, local cohort=_cohort --Ops.Cohort#COHORT if Properties and #Properties>0 then for _,Property in pairs(Properties) do - for _,property in pairs(cohort.properties) do + for property,value in pairs(cohort.properties) do if Property==property then return true end @@ -2277,8 +2277,8 @@ function LEGION.RecruitCohortAssets(Cohorts, MissionTypeRecruit, MissionTypeOpt, else return true end - end - + end + -- Loops over cohorts. for _,_cohort in pairs(Cohorts) do local cohort=_cohort --Ops.Cohort#COHORT @@ -2329,7 +2329,7 @@ function LEGION.RecruitCohortAssets(Cohorts, MissionTypeRecruit, MissionTypeOpt, end -- Debug info. - cohort:T2(cohort.lid..string.format("State=%s: Capable=%s, InRange=%s, Refuel=%s, CanCarry=%s, Category=%s, Attribute=%s, Property=%s, Weapon=%s", + cohort:I(cohort.lid..string.format("State=%s: Capable=%s, InRange=%s, Refuel=%s, CanCarry=%s, Category=%s, Attribute=%s, Property=%s, Weapon=%s", cohort:GetState(), tostring(Capable), tostring(InRange), tostring(Refuel), tostring(CanCarry), tostring(RightCategory), tostring(RightAttribute), tostring(RightProperty), tostring(RightWeapon))) -- Check OnDuty, capable, in range and refueling type (if TANKER). diff --git a/Moose Development/Moose/Ops/NavyGroup.lua b/Moose Development/Moose/Ops/NavyGroup.lua index f0d5268e0..f0ca112df 100644 --- a/Moose Development/Moose/Ops/NavyGroup.lua +++ b/Moose Development/Moose/Ops/NavyGroup.lua @@ -90,7 +90,7 @@ NAVYGROUP = { --- NavyGroup version. -- @field #string version -NAVYGROUP.version="0.7.3" +NAVYGROUP.version="0.7.9" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -987,8 +987,17 @@ function NAVYGROUP:onafterSpawned(From, Event, To) -- Set default Alarm State. self:SwitchAlarmstate(self.option.Alarm) + -- Set emission. + self:SwitchEmission(self.option.Emission) + -- Set default EPLRS. - self:SwitchEPLRS(self.option.EPLRS) + self:SwitchEPLRS(self.option.EPLRS) + + -- Set default Invisible. + self:SwitchInvisible(self.option.Invisible) + + -- Set default Immortal. + self:SwitchImmortal(self.option.Immortal) -- Set TACAN beacon. self:_SwitchTACAN() diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index ebbfdf6ef..181c371b0 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -308,6 +308,18 @@ OPSGROUP.TaskType={ -- @field Core.UserFlag#USERFLAG stopflag If flag is set to 1 (=true), the task is stopped. -- @field #number backupROE Rules of engagement that are restored once the task is over. +--- Option data. +-- @type OPSGROUP.Option +-- @field #number ROE Rule of engagement. +-- @field #number ROT Reaction on threat. +-- @field #number Alarm Alarm state. +-- @field #number Formation Formation. +-- @field #boolean EPLRS data link. +-- @field #boolean Disperse Disperse under fire. +-- @field #boolean Emission Emission on/off. +-- @field #boolean Invisible Invisible on/off. +-- @field #boolean Immortal Immortal on/off. + --- Beacon data. -- @type OPSGROUP.Beacon -- @field #number Channel Channel. @@ -329,16 +341,6 @@ OPSGROUP.TaskType={ -- @field #number NumberGroup Group number. First number after name, e.g. "Uzi-**1**-1". -- @field #string NameSquad Name of the squad, e.g. "Uzi". ---- Option data. --- @type OPSGROUP.Option --- @field #number ROE Rule of engagement. --- @field #number ROT Reaction on threat. --- @field #number Alarm Alarm state. --- @field #number Formation Formation. --- @field #boolean EPLRS data link. --- @field #boolean Disperse Disperse under fire. --- @field #boolean Emission Emission on/off. - --- Weapon range data. -- @type OPSGROUP.WeaponData -- @field #number BitType Type of weapon. @@ -469,21 +471,21 @@ OPSGROUP.CargoStatus={ --- OpsGroup version. -- @field #string version -OPSGROUP.version="0.7.8" +OPSGROUP.version="0.7.9" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: AI on/off. --- TODO: Emission on/off. --- TODO: Invisible/immortal. -- TODO: F10 menu. -- TODO: Add pseudo function. -- TODO: Afterburner restrict. -- TODO: What more options? -- TODO: Shot events? -- TODO: Marks to add waypoints/tasks on-the-fly. +-- DONE: Invisible/immortal. +-- DONE: Emission on/off -- DONE: Damage? -- DONE: Options EPLRS @@ -4997,6 +4999,14 @@ function OPSGROUP:onafterMissionDone(From, Event, To, Mission) if Mission.optionEmission then self:SwitchEmission() end + -- Invisible to default. + if Mission.optionInvisible then + self:SwitchInvisible() + end + -- Immortal to default. + if Mission.optionImmortal then + self:SwitchImmortal() + end -- Formation to default. if Mission.optionFormation and self:IsFlightgroup() then self:SwitchFormation() @@ -5103,9 +5113,13 @@ function OPSGROUP:RouteToMission(mission, delay) self:MissionExecute(mission) return end + + if self.speedMax<=3.6 or mission.teleport then + --self:ClearWaypoints() + end -- ID of current waypoint. - local uid=self:GetWaypointCurrent().uid + local uid=self:GetWaypointCurrentUID() -- Ingress waypoint coordinate where the mission is executed. local waypointcoord=nil --Core.Point#COORDINATE @@ -5113,6 +5127,8 @@ function OPSGROUP:RouteToMission(mission, delay) -- Current coordinate of the group. local currentcoord=self:GetCoordinate() + currentcoord:MarkToAll(mission:GetName(),ReadOnly,Text) + -- Road connection. local roadcoord=currentcoord:GetClosestPointToRoad() @@ -5173,6 +5189,7 @@ function OPSGROUP:RouteToMission(mission, delay) -- Random coordinate. waypointcoord=targetzone:GetRandomCoordinate(nil , nil, surfacetypes) + waypointcoord:MarkToAll(mission:GetName(),ReadOnly,Text) elseif mission.type==AUFTRAG.Type.ONGUARD or mission.type==AUFTRAG.Type.ARMOREDGUARD then --- -- Guard @@ -5237,7 +5254,7 @@ function OPSGROUP:RouteToMission(mission, delay) waypointcoord=CarrierCoordinate:Translate(10000, heading-180):SetAltitude(2000) - waypointcoord:MarkToAll("Recoverytanker",ReadOnly,Text) + waypointcoord:MarkToAll("Recoverytanker") else --- @@ -5466,9 +5483,17 @@ function OPSGROUP:_SetMissionOptions(mission) self:SwitchEPLRS(mission.optionEPLRS) end -- Emission - if mission.optionEPLRS then + if mission.optionEmission then self:SwitchEmission(mission.optionEmission) - end + end + -- Invisible + if mission.optionInvisible then + self:SwitchInvisible(mission.optionInvisible) + end + -- Immortal + if mission.optionImmortal then + self:SwitchImmortal(mission.optionImmortal) + end -- Formation if mission.optionFormation and self:IsFlightgroup() then self:SwitchFormation(mission.optionFormation) @@ -6762,6 +6787,10 @@ function OPSGROUP:Teleport(Coordinate, Delay, NoPauseMission) -- Set waypoint in air for flighgroups. if self:IsFlightgroup() then Template.route.points[1]=Coordinate:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, 300, true, nil, nil, "Spawnpoint") + elseif self:IsArmygroup() then + Template.route.points[1]=Coordinate:WaypointGround() + elseif self:IsNavygroup() then + Template.route.points[1]=Coordinate:WaypointNaval() end -- Template units. @@ -10790,6 +10819,103 @@ function OPSGROUP:GetEmission() return self.option.Emission or self.optionDefault.Emission end +--- Set the default invisible for the group. +-- @param #OPSGROUP self +-- @param #boolean OnOffSwitch If `true`, group is ivisible by default. +-- @return #OPSGROUP self +function OPSGROUP:SetDefaultInvisible(OnOffSwitch) + + if OnOffSwitch==nil then + self.optionDefault.Invisible=true + else + self.optionDefault.Invisible=OnOffSwitch + end + + return self +end + +--- Switch invisibility on or off. +-- @param #OPSGROUP self +-- @param #boolean OnOffSwitch If `true` or `nil`, switch invisibliity on. If `false` invisibility switched off. +-- @return #OPSGROUP self +function OPSGROUP:SwitchInvisible(OnOffSwitch) + + if self:IsAlive() or self:IsInUtero() then + + if OnOffSwitch==nil then + + self.option.Invisible=self.optionDefault.Invisible + + else + + self.option.Invisible=OnOffSwitch + + end + + if self:IsInUtero() then + self:T2(self.lid..string.format("Setting current INVISIBLE=%s when GROUP is SPAWNED", tostring(self.option.Invisible))) + else + + self.group:SetCommandInvisible(self.option.Invisible) + self:T(self.lid..string.format("Setting current INVISIBLE=%s", tostring(self.option.Invisible))) + + end + else + self:E(self.lid.."WARNING: Cannot switch Invisible! Group is not alive") + end + + return self +end + + +--- Set the default immortal for the group. +-- @param #OPSGROUP self +-- @param #boolean OnOffSwitch If `true`, group is immortal by default. +-- @return #OPSGROUP self +function OPSGROUP:SetDefaultImmortal(OnOffSwitch) + + if OnOffSwitch==nil then + self.optionDefault.Immortal=true + else + self.optionDefault.Immortal=OnOffSwitch + end + + return self +end + +--- Switch immortality on or off. +-- @param #OPSGROUP self +-- @param #boolean OnOffSwitch If `true` or `nil`, switch immortality on. If `false` immortality switched off. +-- @return #OPSGROUP self +function OPSGROUP:SwitchImmortal(OnOffSwitch) + + if self:IsAlive() or self:IsInUtero() then + + if OnOffSwitch==nil then + + self.option.Immortal=self.optionDefault.Immortal + + else + + self.option.Immortal=OnOffSwitch + + end + + if self:IsInUtero() then + self:T2(self.lid..string.format("Setting current IMMORTAL=%s when GROUP is SPAWNED", tostring(self.option.Immortal))) + else + + self.group:SetCommandImmortal(self.option.Immortal) + self:T(self.lid..string.format("Setting current IMMORTAL=%s", tostring(self.option.Immortal))) + + end + else + self:E(self.lid.."WARNING: Cannot switch Immortal! Group is not alive") + end + + return self +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- SETTINGS FUNCTIONS ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/OpsZone.lua b/Moose Development/Moose/Ops/OpsZone.lua index 8e1a79068..ce8f3227b 100644 --- a/Moose Development/Moose/Ops/OpsZone.lua +++ b/Moose Development/Moose/Ops/OpsZone.lua @@ -2,8 +2,9 @@ -- -- **Main Features:** -- --- * Monitor if a zone is captured. --- * Monitor if an airbase is captured. +-- * Monitor if a zone is captured +-- * Monitor if an airbase is captured +-- * Define conditions under which zones are captured/held -- -- === -- @@ -79,6 +80,7 @@ OPSZONE.version="0.3.0" -- ToDo list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO: Capturing based on (total) threat level threshold. Unarmed units do not pose a threat and should not be able to hold a zone. -- TODO: Pause/unpause evaluations. -- TODO: Capture time, i.e. time how long a single coalition has to be inside the zone to capture it. -- TODO: Differentiate between ground attack and boming by air or arty. @@ -348,7 +350,7 @@ end --- Set categories of units that can capture or hold the zone. See [DCS Class Unit](https://wiki.hoggitworld.com/view/DCS_Class_Unit). -- @param #OPSZONE self -- @param #table Categories Table of unit categories. Default `{Unit.Category.GROUND_UNIT}`. --- @return #OPSZONE +-- @return #OPSZONE self function OPSZONE:SetUnitCategories(Categories) -- Ensure table. @@ -362,6 +364,32 @@ function OPSZONE:SetUnitCategories(Categories) return self end +--- Set threat level threshold that the defending units must have to hold a zone. +-- The reason why you might want to set this is that unarmed units (*e.g.* fuel trucks) should not be able to hold a zone as they do not pose a threat. +-- @param #OPSZONE self +-- @param #number Threatlevel Threat level threshod. Default 0. +-- @return #OPSZONE self +function OPSZONE:SetThreatlevelDefinding(Threatlevel) + + self.threatlevelDefending=Threatlevel or 0 + + return self +end + + +--- Set threat level threshold that the offending units must have to capture a zone. +-- The reason why you might want to set this is that unarmed units (*e.g.* fuel trucks) should not be able to capture a zone as they do not pose a threat. +-- @param #OPSZONE self +-- @param #number Threatlevel Threat level threshod. Default 0. +-- @return #OPSZONE self +function OPSZONE:SetThreatlevelOffending(Threatlevel) + + self.threatlevelOffending=Threatlevel or 0 + + return self +end + + --- Set whether *neutral* units can capture the zone. -- @param #OPSZONE self -- @param #boolean CanCapture If `true`, neutral units can. diff --git a/Moose Development/Moose/Ops/Target.lua b/Moose Development/Moose/Ops/Target.lua index e06e7df43..faba46a41 100644 --- a/Moose Development/Moose/Ops/Target.lua +++ b/Moose Development/Moose/Ops/Target.lua @@ -36,6 +36,7 @@ -- @field Ops.Auftrag#AUFTRAG mission Mission attached to this target. -- @field Ops.Intelligence#INTEL.Contact contact Contact attached to this target. -- @field #boolean isDestroyed If true, target objects were destroyed. +-- @field #table resources Resource list. -- @extends Core.Fsm#FSM --- **It is far more important to be able to hit the target than it is to haggle over who makes a weapon or who pulls a trigger** -- Dwight D Eisenhower @@ -113,6 +114,16 @@ TARGET.ObjectStatus={ ALIVE="Alive", DEAD="Dead", } + +--- Resource. +-- @type TARGET.Resource +-- @field #string MissionType Mission type, e.g. `AUFTRAG.Type.BAI`. +-- @field #number Nmin Min number of assets. +-- @field #number Nmax Max number of assets. +-- @field #table Attributes Generalized attribute, e.g. `{GROUP.Attribute.GROUND_INFANTRY}`. +-- @field #table Properties Properties ([DCS attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes)), e.g. `"Attack helicopters"` or `"Mobile AAA"`. +-- @field Ops.Auftrag#AUFTRAG mission Attached mission. + --- Target object. -- @type TARGET.Object -- @field #number ID Target unique ID. @@ -307,6 +318,53 @@ function TARGET:SetImportance(Importance) return self end +--- Add mission type and number of required assets to resource. +-- @param #TARGET self +-- @param #string MissionType Mission Type. +-- @param #number Nmin Min number of required assets. +-- @param #number Nmax Max number of requried assets. +-- @param #table Attributes Generalized attribute(s). +-- @param #table Properties DCS attribute(s). Default `nil`. +-- @return #TARGET.Resource The resource table. +function TARGET:AddResource(MissionType, Nmin, Nmax, Attributes, Properties) + + -- Ensure table. + if Attributes and type(Attributes)~="table" then + Attributes={Attributes} + end + + -- Ensure table. + if Properties and type(Properties)~="table" then + Properties={Properties} + end + + -- Create new resource table. + local resource={} --#TARGET.Resource + resource.MissionType=MissionType + resource.Nmin=Nmin or 1 + resource.Nmax=Nmax or 1 + resource.Attributes=Attributes or {} + resource.Properties=Properties or {} + + -- Init resource table. + self.resources=self.resources or {} + + -- Add to table. + table.insert(self.resources, resource) + + -- Debug output. + if self.verbose>10 then + local text="Resource:" + for _,_r in pairs(self.resources) do + local r=_r --#TARGET.Resource + text=text..string.format("\nmission=%s, Nmin=%d, Nmax=%d, attribute=%s, properties=%s", r.MissionType, r.Nmin, r.Nmax, tostring(r.Attributes[1]), tostring(r.Properties[1])) + end + self:I(self.lid..text) + end + + return resource +end + --- Check if TARGET is alive. -- @param #TARGET self -- @return #boolean If true, target is alive. From ae54cd8fde3044ac6ee1c991766a59ee66b542a9 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 27 May 2022 22:14:21 +0200 Subject: [PATCH 09/20] OPS ** ARMYGROUP** - Added suppression option **COMMANDER** - Added function to add targets with defined resources **OPSGROUP** - Added option to pause multiple missions **INTEL** - Fixed bug in cluster calc **LEGION** - Added function to get alive opsgroups **TARGET** * Added start condition --- Moose Development/Moose/Core/Database.lua | 27 ++ Moose Development/Moose/Core/Set.lua | 8 + .../Moose/Functional/Warehouse.lua | 8 +- Moose Development/Moose/Ops/ArmyGroup.lua | 220 ++++++++-- Moose Development/Moose/Ops/Auftrag.lua | 15 + Moose Development/Moose/Ops/Chief.lua | 3 +- Moose Development/Moose/Ops/Cohort.lua | 29 +- Moose Development/Moose/Ops/Commander.lua | 39 +- Moose Development/Moose/Ops/FlightGroup.lua | 28 +- Moose Development/Moose/Ops/Intelligence.lua | 31 +- Moose Development/Moose/Ops/Legion.lua | 18 + Moose Development/Moose/Ops/NavyGroup.lua | 6 +- Moose Development/Moose/Ops/OpsGroup.lua | 402 ++++++++++++++---- Moose Development/Moose/Ops/Target.lua | 93 +++- Moose Development/Moose/Wrapper/Group.lua | 7 +- 15 files changed, 774 insertions(+), 160 deletions(-) diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index 0b4fff4c7..5e2063a12 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -1543,6 +1543,33 @@ function DATABASE:FindOpsGroup(groupname) return self.FLIGHTGROUPS[groupname] end +--- Find an OPSGROUP (FLIGHTGROUP, ARMYGROUP, NAVYGROUP) in the data base for a given unit. +-- @param #DATABASE self +-- @param #string unitname Unit name. Can also be passed as UNIT object. +-- @return Ops.OpsGroup#OPSGROUP OPS group object. +function DATABASE:FindOpsGroupFromUnit(unitname) + + local unit=nil --Wrapper.Unit#UNIT + local groupname + + -- Get group and group name. + if type(unitname)=="string" then + unit=UNIT:FindByName(unitname) + else + unit=unitname + end + + if unit then + groupname=unit:GetGroup():GetName() + end + + if groupname then + return self.FLIGHTGROUPS[groupname] + else + return nil + end +end + --- Add a flight control to the data base. -- @param #DATABASE self -- @param Ops.FlightControl#FLIGHTCONTROL flightcontrol diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index 518d8ff62..1d2fe6f30 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -6393,6 +6393,14 @@ do -- SET_OPSGROUP -- Trigger Added event. self:Added(ObjectName, object) + end + + --- Adds a @{Core.Base#BASE} object in the @{Core.Set#SET_BASE}, using the Object Name as the index. + -- @param #SET_BASE self + -- @param Ops.OpsGroup#OPSGROUP Object Ops group + -- @return Core.Base#BASE The added BASE Object. + function SET_OPSGROUP:AddObject(Object) + self:Add(Object.groupname, Object) end --- Add a GROUP or OPSGROUP object to the set. diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 7a44c2820..04e5728ba 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -349,6 +349,7 @@ -- * @{#WAREHOUSE.Attribute.GROUND_APC} Infantry carriers, in particular Amoured Personell Carrier. This can be used to transport other assets. -- * @{#WAREHOUSE.Attribute.GROUND_TRUCK} Unarmed ground vehicles, which has the DCS "Truck" attribute. -- * @{#WAREHOUSE.Attribute.GROUND_INFANTRY} Ground infantry assets. +-- * @{#WAREHOUSE.Attribute.GROUND_IFV} Ground infantry fighting vehicle. -- * @{#WAREHOUSE.Attribute.GROUND_ARTILLERY} Artillery assets. -- * @{#WAREHOUSE.Attribute.GROUND_TANK} Tanks (modern or old). -- * @{#WAREHOUSE.Attribute.GROUND_TRAIN} Trains. Not that trains are **not** yet properly implemented in DCS and cannot be used currently. @@ -1704,6 +1705,7 @@ WAREHOUSE.Descriptor = { -- @field #string GROUND_APC Infantry carriers, in particular Amoured Personell Carrier. This can be used to transport other assets. -- @field #string GROUND_TRUCK Unarmed ground vehicles, which has the DCS "Truck" attribute. -- @field #string GROUND_INFANTRY Ground infantry assets. +-- @field #string GROUND_IFV Ground infantry fighting vehicle. -- @field #string GROUND_ARTILLERY Artillery assets. -- @field #string GROUND_TANK Tanks (modern or old). -- @field #string GROUND_TRAIN Trains. Not that trains are **not** yet properly implemented in DCS and cannot be used currently. @@ -1730,6 +1732,7 @@ WAREHOUSE.Attribute = { GROUND_APC="Ground_APC", GROUND_TRUCK="Ground_Truck", GROUND_INFANTRY="Ground_Infantry", + GROUND_IFV="Ground_IFV", GROUND_ARTILLERY="Ground_Artillery", GROUND_TANK="Ground_Tank", GROUND_TRAIN="Ground_Train", @@ -8352,9 +8355,10 @@ function WAREHOUSE:_GetAttribute(group) --- Ground --- -------------- -- Ground - local apc=group:HasAttribute("Infantry carriers") + local apc=group:HasAttribute("APC") --("Infantry carriers") local truck=group:HasAttribute("Trucks") and group:GetCategory()==Group.Category.GROUND local infantry=group:HasAttribute("Infantry") + local ifv=group:HasAttribute("IFV") local artillery=group:HasAttribute("Artillery") local tank=group:HasAttribute("Old Tanks") or group:HasAttribute("Modern Tanks") local aaa=group:HasAttribute("AAA") @@ -8391,6 +8395,8 @@ function WAREHOUSE:_GetAttribute(group) attribute=WAREHOUSE.Attribute.AIR_UAV elseif apc then attribute=WAREHOUSE.Attribute.GROUND_APC + elseif ifv then + attribute=WAREHOUSE.Attribute.GROUND_IFV elseif infantry then attribute=WAREHOUSE.Attribute.GROUND_INFANTRY elseif artillery then diff --git a/Moose Development/Moose/Ops/ArmyGroup.lua b/Moose Development/Moose/Ops/ArmyGroup.lua index 7102a7aa0..c40948931 100644 --- a/Moose Development/Moose/Ops/ArmyGroup.lua +++ b/Moose Development/Moose/Ops/ArmyGroup.lua @@ -34,6 +34,11 @@ -- @field #boolean isMobile If true, group is mobile. -- @field #ARMYGROUP.Target engage Engage target. -- @field Core.Set#SET_ZONE retreatZones Set of retreat zones. +-- @field #boolean suppressOn Bla +-- @field #boolean isSuppressed Bla +-- @field #number TsuppressMin Bla +-- @field #number TsuppressMax Bla +-- @field #number TsuppressAve Bla -- @extends Ops.OpsGroup#OPSGROUP --- *Your soul may belong to Jesus, but your ass belongs to the marines.* -- Eugene B Sledge @@ -122,6 +127,9 @@ function ARMYGROUP:New(group) self:AddTransition("*", "Retreat", "Retreating") -- Order a retreat. self:AddTransition("Retreating", "Retreated", "Retreated") -- Group retreated. + self:AddTransition("*", "Suppressed", "*") -- Group is suppressed + self:AddTransition("*", "Unsuppressed", "*") -- Group is unsuppressed. + self:AddTransition("Cruising", "EngageTarget", "Engaging") -- Engage a target from Cruising state self:AddTransition("Holding", "EngageTarget", "Engaging") -- Engage a target from Holding state self:AddTransition("OnDetour", "EngageTarget", "Engaging") -- Engage a target from OnDetour state @@ -280,6 +288,7 @@ function ARMYGROUP:New(group) -- @function [parent=#ARMYGROUP] EngageTarget -- @param #ARMYGROUP self -- @param Wrapper.Group#GROUP Group the group to be engaged. + -- @param #number Speed Speed in knots. -- @param #string Formation Formation used in the engagement. --- Triggers the FSM event "EngageTarget" after a delay. @@ -287,6 +296,7 @@ function ARMYGROUP:New(group) -- @param #ARMYGROUP self -- @param #number delay Delay in seconds. -- @param Wrapper.Group#GROUP Group the group to be engaged. + -- @param #number Speed Speed in knots. -- @param #string Formation Formation used in the engagement. @@ -297,6 +307,7 @@ function ARMYGROUP:New(group) -- @param #string Event Event. -- @param #string To To state. -- @param Wrapper.Group#GROUP Group the group to be engaged. + -- @param #number Speed Speed in knots. -- @param #string Formation Formation used in the engagement. @@ -386,7 +397,7 @@ function ARMYGROUP:New(group) self:HandleEvent(EVENTS.Birth, self.OnEventBirth) self:HandleEvent(EVENTS.Dead, self.OnEventDead) self:HandleEvent(EVENTS.RemoveUnit, self.OnEventRemoveUnit) - --self:HandleEvent(EVENTS.Hit, self.OnEventHit) + self:HandleEvent(EVENTS.Hit, self.OnEventHit) -- Start the status monitoring. self.timerStatus=TIMER:New(self.Status, self):Start(1, 30) @@ -572,6 +583,47 @@ function ARMYGROUP:AddRetreatZone(RetreatZone) return self end +--- Set suppression on. average, minimum and maximum time a unit is suppressed each time it gets hit. +-- @param #ARMYGROUP self +-- @param #number Tave Average time [seconds] a group will be suppressed. Default is 15 seconds. +-- @param #number Tmin (Optional) Minimum time [seconds] a group will be suppressed. Default is 5 seconds. +-- @param #number Tmax (Optional) Maximum time a group will be suppressed. Default is 25 seconds. +-- @return #ARMYGROUP self +function ARMYGROUP:SetSuppressionOn(Tave, Tmin, Tmax) + + -- Activate suppression. + self.suppressionOn=true + + -- Minimum suppression time is input or default 5 sec (but at least 1 second). + self.TsuppressMin=Tmin or 1 + self.TsuppressMin=math.max(self.TsuppressMin, 1) + + -- Maximum suppression time is input or default but at least Tmin. + self.TsuppressMax=Tmax or 15 + self.TsuppressMax=math.max(self.TsuppressMax, self.TsuppressMin) + + -- Expected suppression time is input or default but at leat Tmin and at most Tmax. + self.TsuppressAve=Tave or 10 + self.TsuppressAve=math.max(self.TsuppressMin) + self.TsuppressAve=math.min(self.TsuppressMax) + + -- Debug Info + self:T(self.lid..string.format("Set ave suppression time to %d seconds.", self.TsuppressAve)) + self:T(self.lid..string.format("Set min suppression time to %d seconds.", self.TsuppressMin)) + self:T(self.lid..string.format("Set max suppression time to %d seconds.", self.TsuppressMax)) + + return self +end + +--- Set suppression off. +-- @param #ARMYGROUP self +-- @return #ARMYGROUP self +function ARMYGROUP:SetSuppressionOff() + -- Activate suppression. + self.suppressionOn=false +end + + --- Check if the group is currently holding its positon. -- @param #ARMYGROUP self -- @return #boolean If true, group was ordered to hold. @@ -651,10 +703,14 @@ function ARMYGROUP:Status() -- Check if group is waiting. if self:IsWaiting() then if self.Twaiting and self.dTwait then - if timer.getAbsTime()>self.Twaiting+self.dTwait then + if timer.getAbsTime()>self.Twaiting+self.dTwait then self.Twaiting=nil self.dTwait=nil - self:Cruise() + if self:_CountPausedMissions()>0 then + self:UnpauseMission() + else + self:Cruise() + end end end end @@ -799,22 +855,6 @@ end -- DCS Events ==> See OPSGROUP ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Event function handling when a unit is hit. --- @param #ARMYGROUP self --- @param Core.Event#EVENTDATA EventData Event data. -function ARMYGROUP:OnEventHit(EventData) - - -- Check that this is the right group. - if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then - local unit=EventData.IniUnit - local group=EventData.IniGroup - local unitname=EventData.IniUnitName - - -- TODO: suppression - - end -end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- FSM Events ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1126,7 +1166,7 @@ function ARMYGROUP:onafterUpdateRoute(From, Event, To, n, N, Speed, Formation) self.speedWp=wp.speed -- Debug output. - if self.verbose>=10 or true then + if self.verbose>=10 then for i,_wp in pairs(waypoints) do local wp=_wp --Ops.OpsGroup#OPSGROUP.Waypoint @@ -1228,6 +1268,16 @@ end function ARMYGROUP:onafterOutOfAmmo(From, Event, To) self:T(self.lid..string.format("Group is out of ammo at t=%.3f", timer.getTime())) + -- Get current task. + local task=self:GetTaskCurrent() + + if task then + if task.dcstask.id=="FireAtPoint" or task.dcstask.id==AUFTRAG.SpecialTask.BARRAGE then + self:T(self.lid..string.format("Cancelling current %s task because out of ammo!", task.dcstask.id)) + self:TaskCancel(task) + end + end + -- Fist, check if we want to rearm once out-of-ammo. --TODO: IsMobile() check if self.rearmOnOutOfAmmo then @@ -1250,16 +1300,6 @@ function ARMYGROUP:onafterOutOfAmmo(From, Event, To) if self.rtzOnOutOfAmmo then self:__RTZ(-1) end - - -- Get current task. - local task=self:GetTaskCurrent() - - if task then - if task.dcstask.id=="FireAtPoint" or task.dcstask.id==AUFTRAG.SpecialTask.BARRAGE then - self:T(self.lid..string.format("Cancelling current %s task because out of ammo!", task.dcstask.id)) - self:TaskCancel(task) - end - end end @@ -1292,6 +1332,15 @@ function ARMYGROUP:onbeforeRearm(From, Event, To, Coordinate, Formation) allowed=false end + -- Check if coordinate is provided. + if allowed and not Coordinate then + local truck=self:FindNearestAmmoSupply() + if truck and truck:IsAlive() then + self:__Rearm(-0.1, truck:GetCoordinate(), Formation) + end + return false + end + -- Try again... if dt then self:T(self.lid..string.format("Trying Rearm again in %.2f sec", dt)) @@ -1589,7 +1638,7 @@ function ARMYGROUP:onafterEngageTarget(From, Event, To, Target, Speed, Formation self.engage.Coordinate=UTILS.DeepCopy(self.engage.Target:GetCoordinate()) -- Get a coordinate close to the target. - local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate, 0.9) + local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate, 0.95) -- Backup ROE and alarm state. self.engage.roe=self:GetROE() @@ -1752,6 +1801,21 @@ function ARMYGROUP:onafterCruise(From, Event, To, Speed, Formation) end +--- On after "Hit" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Unit#UNIT Enemy Unit that hit the element or `nil`. +function ARMYGROUP:onafterHit(From, Event, To, Enemy) + self:T(self.lid..string.format("ArmyGroup hit by %s", Enemy and Enemy:GetName() or "unknown")) + + if self.suppressionOn then + env.info(self.lid.."FF suppress") + self:_Suppress() + end +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Routing ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1996,6 +2060,100 @@ function ARMYGROUP:FindNearestAmmoSupply(Radius) return nil, nil end +--- Suppress fire of the group by setting its ROE to weapon hold. +-- @param #ARMYGROUP self +function ARMYGROUP:_Suppress() + + -- Current time. + local Tnow=timer.getTime() + + -- Current ROE + local currROE=self:GetROE() + + + -- Get randomized time the unit is suppressed. + local sigma=(self.TsuppressMax-self.TsuppressMin)/4 + + -- Gaussian distribution. + local Tsuppress=UTILS.RandomGaussian(self.TsuppressAve,sigma,self.TsuppressMin, self.TsuppressMax) + + -- Time at which the suppression is over. + local renew=true + if not self.TsuppressionOver then + + -- Group is not suppressed currently. + self.TsuppressionOver=Tnow+Tsuppress + + -- Group will hold their weapons. + self:SwitchROE(ENUMS.ROE.WeaponHold) + + -- Backup ROE. + self.suppressionROE=currROE + + else + -- Check if suppression is longer than current time. + if Tsuppress+Tnow > self.TsuppressionOver then + self.TsuppressionOver=Tnow+Tsuppress + else + renew=false + end + end + + -- Recovery event will be called in Tsuppress seconds. + if renew then + self:__Unsuppressed(self.TsuppressionOver-Tnow) + end + + -- Debug message. + self:T(self.lid..string.format("Suppressed for %d sec", Tsuppress)) + +end + +--- Before "Recovered" event. Check if suppression time is over. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @return #boolean +function ARMYGROUP:onbeforeUnsuppressed(From, Event, To) + + -- Current time. + local Tnow=timer.getTime() + + -- Debug info + self:T(self.lid..string.format("onbeforeRecovered: Time now: %d - Time over: %d", Tnow, self.TsuppressionOver)) + + -- Recovery is only possible if enough time since the last hit has passed. + if Tnow >= self.TsuppressionOver then + return true + else + return false + end + +end + +--- After "Recovered" event. Group has recovered and its ROE is set back to the "normal" unsuppressed state. Optionally the group is flared green. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARMYGROUP:onafterUnsuppressed(From, Event, To) + + -- Debug message. + local text=string.format("Group %s has recovered!", self:GetName()) + MESSAGE:New(text, 10):ToAll() + self:T(self.lid..text) + + -- Set ROE back to default. + self:SwitchROE(self.suppressionROE) + + -- Flare unit green. + if true then + self.group:FlareGreen() + end + +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Auftrag.lua b/Moose Development/Moose/Ops/Auftrag.lua index 2361a21d5..5142b87d1 100644 --- a/Moose Development/Moose/Ops/Auftrag.lua +++ b/Moose Development/Moose/Ops/Auftrag.lua @@ -2529,6 +2529,21 @@ function AUFTRAG:GetRequiredAssets(Legion) return Nmin, Nmax end +--- **[LEGION, COMMANDER, CHIEF]** Set that only alive (spawned) assets are considered. +-- @param #AUFTRAG self +-- @param #boolean Switch If true or nil, only active assets. If false +-- @return #AUFTRAG self +function AUFTRAG:SetAssetsStayAlive(Switch) + + if Switch==nil then + Switch=true + end + + self.assetStayAlive=Switch + + return self +end + --- **[LEGION, COMMANDER, CHIEF]** Define how many assets are required that escort the mission assets. -- Only used if the mission is handled by a **LEGION** (AIRWING, BRIGADE, FLEET) or higher level. -- @param #AUFTRAG self diff --git a/Moose Development/Moose/Ops/Chief.lua b/Moose Development/Moose/Ops/Chief.lua index ce5d5a65c..006fad90d 100644 --- a/Moose Development/Moose/Ops/Chief.lua +++ b/Moose Development/Moose/Ops/Chief.lua @@ -955,6 +955,7 @@ end function CHIEF:AddTarget(Target) if not self:IsTarget(Target) then + Target.chief=self table.insert(self.targetqueue, Target) end @@ -1536,7 +1537,7 @@ function CHIEF:onafterStatus(From, Event, To) for _,_target in pairs(self.targetqueue) do local target=_target --Ops.Target#TARGET - if target and target:IsAlive() and target.mission and target.mission:IsNotOver() then + if target and target:IsAlive() and target.chief and target.mission and target.mission:IsNotOver() then local inborder=self:CheckTargetInZones(target, self.borderzoneset) diff --git a/Moose Development/Moose/Ops/Cohort.lua b/Moose Development/Moose/Ops/Cohort.lua index fe90c4e2f..5b144c499 100644 --- a/Moose Development/Moose/Ops/Cohort.lua +++ b/Moose Development/Moose/Ops/Cohort.lua @@ -987,6 +987,31 @@ function COHORT:CountAssets(InStock, MissionTypes, Attributes) return N end +--- Get OPSGROUPs. +-- @param #COHORT self +-- @param #table MissionTypes (Optional) Count only assest that can perform certain mission type(s). Default is all types. +-- @param #table Attributes (Optional) Count only assest that have a certain attribute(s), e.g. `WAREHOUSE.Attribute.AIR_BOMBER`. +-- @return Core.Set#SET_OPSGROUPS Ops groups set. +function COHORT:GetOpsGroups(MissionTypes, Attributes) + + local set=SET_OPSGROUP:New() + + for _,_asset in pairs(self.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + if MissionTypes==nil or AUFTRAG.CheckMissionCapability(MissionTypes, self.missiontypes) then + if Attributes==nil or self:CheckAttribute(Attributes) then + if asset.flightgroup and asset.flightgroup:IsAlive() then + --set:AddObject(asset.flightgroup) + set:AddGroup(asset.flightgroup) + end + end + end + end + + return set +end + --- Get assets for a mission. -- @param #COHORT self -- @param #string MissionType Mission type. @@ -1021,7 +1046,7 @@ function COHORT:RecruitAssets(MissionType, Npayloads) if not (isRequested or isReserved) then -- Check if asset is currently on a mission (STARTED or QUEUED). - if self.legion:IsAssetOnMission(asset) then + if self.legion:IsAssetOnMission(asset) then --- -- Asset is already on a mission. --- @@ -1034,7 +1059,7 @@ function COHORT:RecruitAssets(MissionType, Npayloads) elseif self.legion:IsAssetOnMission(asset, AUFTRAG.Type.NOTHING) then - -- Relocation: Take all assets. Mission will be cancelled. + -- Assets on mission NOTHING are considered. table.insert(assets, asset) elseif self.legion:IsAssetOnMission(asset, AUFTRAG.Type.GCICAP) and MissionType==AUFTRAG.Type.INTERCEPT then diff --git a/Moose Development/Moose/Ops/Commander.lua b/Moose Development/Moose/Ops/Commander.lua index 0ec0e9886..42296babe 100644 --- a/Moose Development/Moose/Ops/Commander.lua +++ b/Moose Development/Moose/Ops/Commander.lua @@ -837,7 +837,7 @@ function COMMANDER:onafterStatus(From, Event, To) -- Status. if self.verbose>=1 then - local text=string.format("Status %s: Legions=%d, Missions=%d, Transports", fsmstate, #self.legions, #self.missionqueue, #self.transportqueue) + local text=string.format("Status %s: Legions=%d, Missions=%d, Targets=%d, Transports=%d", fsmstate, #self.legions, #self.missionqueue, #self.targetqueue, #self.transportqueue) self:T(self.lid..text) end @@ -1031,6 +1031,21 @@ function COMMANDER:onafterStatus(From, Event, To) end self:I(self.lid..text) end + + + --- + -- TARGETS + --- + + -- Target queue. + if self.verbose>=2 and #self.targetqueue>0 then + local text="Target queue:" + for i,_target in pairs(self.targetqueue) do + local target=_target --Ops.Target#TARGET + text=text..string.format("\n[%d] %s: status=%s, life=%d", i, target:GetName(), target:GetState(), target:GetLife()) + end + self:I(self.lid..text) + end --- -- TRANSPORTS @@ -1214,11 +1229,25 @@ function COMMANDER:CheckTargetQueue() return nil end + -- Remove done targets. + for i=#self.targetqueue,1,-1 do + local target=self.targetqueue[i] --Ops.Target#TARGET + if (not target:IsAlive()) or target:EvalConditionsAny(target.conditionStop) then + for _,_resource in pairs(target.resources) do + local resource=_resource --Ops.Target#TARGET.Resource + if resource.mission and resource.mission:IsNotOver() then + self:MissionCancel(resource.mission) + end + end + table.remove(self.targetqueue, i) + end + end + -- Check if total number of missions is reached. local NoLimit=self:_CheckMissionLimit("Total") if NoLimit==false then return nil - end + end -- Sort results table wrt prio and threatlevel. local function _sort(a, b) @@ -1248,6 +1277,9 @@ function COMMANDER:CheckTargetQueue() -- Is this target important enough. local isImportant=(target.importance==nil or target.importance<=vip) + -- Check ALL start conditions are true. + local isReadyStart=target:EvalConditionsAll(target.conditionStart) + -- Debug message. local text=string.format("Target %s: Alive=%s, Threat=%s, Important=%s", target:GetName(), tostring(isAlive), tostring(isThreat), tostring(isImportant)) self:T2(self.lid..text) @@ -1270,10 +1302,13 @@ function COMMANDER:CheckTargetQueue() local mission=AUFTRAG:NewFromTarget(target, missionType) if mission then + + -- Set mission parameters. mission:SetRequiredAssets(resource.Nmin, resource.Nmax) mission:SetRequiredAttribute(resource.Attributes) mission:SetRequiredProperty(resource.Properties) + -- Set resource mission. resource.mission=mission -- Add mission to queue. diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index ccd140c4a..8854055a4 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -2231,10 +2231,23 @@ function FLIGHTGROUP:_CheckGroupDone(delay, waittime) self:T(self.lid.."Engaging! Group NOT done...") return end + + -- Number of tasks remaining. + local nTasks=self:CountRemainingTasks() - -- First check if there is a paused mission. - if self.missionpaused then - self:T(self.lid..string.format("Found paused mission %s [%s]. Unpausing mission...", self.missionpaused.name, self.missionpaused.type)) + -- Number of mission remaining. + local nMissions=self:CountRemainingMissison() + + -- Number of cargo transports remaining. + local nTransports=self:CountRemainingTransports() + + -- Number of paused missions. + local nPaused=self:_CountPausedMissions() + + -- First check if there is a paused mission and that all remaining missions are paused. If there are other missions in the queue, we will run those. + if nPaused>0 and nPaused==nMissions then + local missionpaused=self:_GetPausedMission() + self:T(self.lid..string.format("Found paused mission %s [%s]. Unpausing mission...", missionpaused.name, missionpaused.type)) self:UnpauseMission() return end @@ -2251,15 +2264,6 @@ function FLIGHTGROUP:_CheckGroupDone(delay, waittime) return end - -- Number of tasks remaining. - local nTasks=self:CountRemainingTasks() - - -- Number of mission remaining. - local nMissions=self:CountRemainingMissison() - - -- Number of cargo transports remaining. - local nTransports=self:CountRemainingTransports() - -- Debug info. self:T(self.lid..string.format("Remaining (final=%s): missions=%d, tasks=%d, transports=%d", tostring(self.passedfinalwp), nMissions, nTasks, nTransports)) diff --git a/Moose Development/Moose/Ops/Intelligence.lua b/Moose Development/Moose/Ops/Intelligence.lua index 28f91150d..28437a98e 100644 --- a/Moose Development/Moose/Ops/Intelligence.lua +++ b/Moose Development/Moose/Ops/Intelligence.lua @@ -1453,6 +1453,9 @@ function INTEL:PaintPicture() self:AddContactToCluster(contact, cluster) else + + -- Debug info. + self:T(self.lid..string.format("Paint Picture: contact %s has no closest cluster ==> Create new cluster", contact.groupname)) -- Create a brand new cluster. local newcluster=self:_CreateClusterFromContact(contact) @@ -1817,13 +1820,13 @@ function INTEL:IsContactConnectedToCluster(contact, cluster) --local dist=Contact.position:Get2DDistance(contact.position) local dist=Contact.position:DistanceFromPointVec2(contact.position) - -- AIR - check for spatial proximity - local airprox = false + -- AIR - check for spatial proximity (corrected because airprox was always false for ctype~=INTEL.Ctype.AIRCRAFT) + local airprox = true if contact.ctype == INTEL.Ctype.AIRCRAFT then self:T(string.format("Cluster Alt=%d | Contact Alt=%d",cluster.altitude,contact.altitude)) local adist = math.abs(cluster.altitude - contact.altitude) - if adist < UTILS.FeetToMeters(10000) then -- limit to 10kft - airprox = true + if adist > UTILS.FeetToMeters(10000) then -- limit to 10kft + airprox = false end end @@ -1903,17 +1906,17 @@ function INTEL:_GetClosestClusterOfContact(Contact) local dist=self:_GetDistContactToCluster(Contact, cluster) - -- AIR - check for spatial proximity - local airprox = false + -- AIR - check for spatial proximity (ff: Changed because airprox was always false for ctype~=AIRCRAFT!) + local airprox=true if Contact.ctype == INTEL.Ctype.AIRCRAFT then - if not cluster.altitude then - cluster.altitude = self:GetClusterAltitude(cluster,true) - end - local adist = math.abs(cluster.altitude - Contact.altitude) - self:T(string.format("Cluster Alt=%d | Contact Alt=%d",cluster.altitude,Contact.altitude)) - if adist < UTILS.FeetToMeters(10000) then - airprox = true - end + if not cluster.altitude then + cluster.altitude = self:GetClusterAltitude(cluster,true) + end + local adist = math.abs(cluster.altitude - Contact.altitude) + self:T(string.format("Cluster Alt=%d | Contact Alt=%d",cluster.altitude,Contact.altitude)) + if adist > UTILS.FeetToMeters(10000) then + airprox = false + end end if distself.Twaiting+self.dTwait then self.Twaiting=nil self.dTwait=nil - self:Cruise() + if self:_CountPausedMissions()>0 then + self:UnpauseMission() + else + self:Cruise() + end end end end diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index cbca93657..f76cf8f89 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -68,9 +68,10 @@ -- @field Core.Timer#TIMER timerQueueUpdate Timer for queue updates. -- @field #boolean groupinitialized If true, group parameters were initialized. -- @field #boolean detectionOn If true, detected units of the group are analyzed. --- @field Ops.Auftrag#AUFTRAG missionpaused Paused mission. +-- @field #table pausedmissions Paused missions. -- @field #number Ndestroyed Number of destroyed units. -- @field #number Nkills Number kills of this groups. +-- @field #number Nhit Number of hits taken. -- -- @field #boolean rearmOnOutOfAmmo If `true`, group will go to rearm once it runs out of ammo. -- @@ -185,6 +186,7 @@ OPSGROUP = { callsign = {}, Ndestroyed = 0, Nkills = 0, + Nhit = 0, weaponData = {}, cargoqueue = {}, cargoBay = {}, @@ -192,6 +194,7 @@ OPSGROUP = { carrierLoader = {}, carrierUnloader = {}, useMEtasks = false, + pausedmissions = {}, } @@ -206,6 +209,7 @@ OPSGROUP = { -- @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 #number Nhit Number of times the element was hit. -- -- @field Core.Zone#ZONE_POLYGON_BASE zoneBoundingbox Bounding box zone of the element unit. -- @field Core.Zone#ZONE_POLYGON_BASE zoneLoad Loading zone. @@ -646,8 +650,9 @@ function OPSGROUP:New(group) self:AddTransition("*", "InUtero", "InUtero") -- Deactivated group goes back to mummy. self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. - self:AddTransition("*", "Destroyed", "*") -- The whole group is dead. + self:AddTransition("*", "Hit", "*") -- Someone in the group was hit. self:AddTransition("*", "Damaged", "*") -- Someone in the group took damage. + self:AddTransition("*", "Destroyed", "*") -- The whole group is dead. self:AddTransition("*", "UpdateRoute", "*") -- Update route of group. @@ -706,6 +711,7 @@ function OPSGROUP:New(group) self:AddTransition("*", "ElementDestroyed", "*") -- An element was destroyed. self:AddTransition("*", "ElementDead", "*") -- An element is dead. self:AddTransition("*", "ElementDamaged", "*") -- An element was damaged. + self:AddTransition("*", "ElementHit", "*") -- An element was hit. self:AddTransition("*", "Board", "*") -- Group is ordered to board the carrier. self:AddTransition("*", "Embarked", "*") -- Group was loaded into a cargo carrier. @@ -1176,6 +1182,105 @@ function OPSGROUP:IsTargetDetected(TargetObject) return false end +--- Check if a given coordinate is in weapon range. +-- @param #OPSGROUP self +-- @param Core.Point#COORDINATE TargetCoord Coordinate of the target. +-- @param #number WeaponBitType Weapon type. +-- @param Core.Point#COORDINATE RefCoord Reference coordinate. +-- @return #boolean If `true`, coordinate is in range. +function OPSGROUP:InWeaponRange(TargetCoord, WeaponBitType, RefCoord) + + RefCoord=RefCoord or self:GetCoordinate() + + local dist=TargetCoord:Get2DDistance(RefCoord) + + if WeaponBitType then + + local weapondata=self:GetWeaponData(WeaponBitType) + + if weapondata then + + if dist>=weapondata.RangeMin and dist<=weapondata.RangeMax then + return true + else + return false + end + + end + + else + + for _,_weapondata in pairs(self.weaponData or {}) do + local weapondata=_weapondata --#OPSGROUP.WeaponData + + if dist>=weapondata.RangeMin and dist<=weapondata.RangeMax then + return true + end + + end + + return false + end + + + return nil +end + +--- Get a coordinate, which is in weapon range. +-- @param #OPSGROUP self +-- @param Core.Point#COORDINATE TargetCoord Coordinate of the target. +-- @param #number WeaponBitType Weapon type. +-- @param Core.Point#COORDINATE RefCoord Reference coordinate. +-- @return Core.Point#COORDINATE Coordinate in weapon range +function OPSGROUP:GetCoordinateInRange(TargetCoord, WeaponBitType, RefCoord) + + local coordInRange=nil --Core.Point#COORDINATE + + RefCoord=RefCoord or self:GetCoordinate() + + -- Get weapon range. + local weapondata=self:GetWeaponData(WeaponBitType) + + if weapondata then + + -- Heading to target. + local heading=RefCoord:HeadingTo(TargetCoord) + + -- Distance to target. + local dist=RefCoord:Get2DDistance(TargetCoord) + + -- Check if we are within range. + if dist>weapondata.RangeMax then + + local d=(dist-weapondata.RangeMax)*1.05 + + -- New waypoint coord. + coordInRange=RefCoord:Translate(d, heading) + + -- Debug info. + self:T(self.lid..string.format("Out of max range = %.1f km for weapon %s", weapondata.RangeMax/1000, tostring(WeaponBitType))) + elseif dist0 then + for _,mid in pairs(self.pausedmissions) do + if mid then + local mission=self:GetMissionByID(mid) + if mission and mission:IsNotOver() then + return mission + end + end + end + end + + return nil +end + +--- Count paused mission. +-- @param #OPSGROUP self +-- @return #number Number of paused missions. +function OPSGROUP:_CountPausedMissions() + local N=0 + if self.pausedmissions and #self.pausedmissions>0 then + for _,mid in pairs(self.pausedmissions) do + local mission=self:GetMissionByID(mid) + if mission and mission:IsNotOver() then + N=N+1 + end + end + end + + return N +end + +--- Remove paused mission from the table. +-- @param #OPSGROUP self +-- @param #number AuftragsNummer Mission ID of the paused mission to remove. +-- @return #OPSGROUP self +function OPSGROUP:_RemovePausedMission(AuftragsNummer) + + if self.pausedmissions and #self.pausedmissions>0 then + for i=#self.pausedmissions,1,-1 do + local mid=self.pausedmissions[i] + if mid==AuftragsNummer then + table.remove(self.pausedmissions, i) + return self + end + end + end + + return self +end + --- Check if the group is currently boarding a carrier. -- @param #OPSGROUP self -- @param #string CarrierGroupName (Optional) Additionally check if group is boarding this particular carrier group. @@ -3213,7 +3373,36 @@ function OPSGROUP:OnEventBirth(EventData) end ---- Event function handling the crash of a unit. +--- Event function handling the hit of a unit. +-- @param #OPSGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function OPSGROUP:OnEventHit(EventData) + + -- Check that this is the right group. Here the hit group is stored as target. + if EventData and EventData.TgtGroup and EventData.TgtUnit and EventData.TgtGroupName and EventData.TgtGroupName==self.groupname then + self:T2(self.lid..string.format("EVENT: Unit %s hit!", EventData.TgtUnitName)) + + local unit=EventData.TgtUnit + local group=EventData.TgtGroup + local unitname=EventData.TgtUnitName + + -- Get element. + local element=self:GetElementByName(unitname) + + -- Increase group hit counter. + self.Nhit=self.Nhit or 0 + self.Nhit=self.Nhit + 1 + + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + -- Trigger Element Hit Event. + self:ElementHit(element, EventData.IniUnit) + end + + end + +end + +--- Event function handling the dead of a unit. -- @param #OPSGROUP self -- @param Core.Event#EVENTDATA EventData Event data. function OPSGROUP:OnEventDead(EventData) @@ -4491,8 +4680,10 @@ function OPSGROUP:RemoveMission(Mission) end -- Take care of a paused mission. - if self.missionpaused and self.missionpaused.auftragsnummer==Mission.auftragsnummer then - self.missionpaused=nil + for j,mid in pairs(self.pausedmissions) do + if Mission.auftragsnummer==mid then + table.remove(self.pausedmission, j) + end end -- Remove mission from queue. @@ -4865,7 +5056,7 @@ function OPSGROUP:onafterPauseMission(From, Event, To) self:_RemoveMissionWaypoints(Mission) -- Set mission to pause so we can unpause it later. - self.missionpaused=Mission + table.insert(self.pausedmissions, 1, Mission.auftragsnummer) end @@ -4877,19 +5068,28 @@ end -- @param #string Event Event. -- @param #string To To state. function OPSGROUP:onafterUnpauseMission(From, Event, To) + + -- Get paused mission. + local mission=self:_GetPausedMission() - -- Debug info. - self:T(self.lid..string.format("Unpausing mission")) - - if self.missionpaused then - - local mission=self:GetMissionByID(self.missionpaused.auftragsnummer) - - if mission then - self:MissionStart(mission) + if mission then + + -- Debug info. + self:T(self.lid..string.format("Unpausing mission %s [%s]", mission:GetName(), mission:GetType())) + + -- Start mission. + self:MissionStart(mission) + + -- Remove mission from + for i,mid in pairs(self.pausedmissions) do + --self:T(self.lid..string.format("Checking paused mission", mid)) + if mid==mission.auftragsnummer then + self:T(self.lid..string.format("Removing paused mission id=%d", mid)) + table.remove(self.pausedmissions, i) + break + end end - - self.missionpaused=nil + else self:T(self.lid.."ERROR: No mission to unpause!") end @@ -4911,14 +5111,15 @@ function OPSGROUP:onafterMissionCancel(From, Event, To, Mission) -- Current Mission --- - -- Alert 5 missoins dont have a task set, which could be cancelled. + -- Some missions dont have a task set, which could be cancelled. if Mission.type==AUFTRAG.Type.ALERT5 or Mission.type==AUFTRAG.Type.ONGUARD or Mission.type==AUFTRAG.Type.ARMOREDGUARD or - Mission.type==AUFTRAG.Type.NOTHING or + --Mission.type==AUFTRAG.Type.NOTHING or Mission.type==AUFTRAG.Type.AIRDEFENSE or Mission.type==AUFTRAG.Type.EWR then + -- Trigger mission don task. self:MissionDone(Mission) return @@ -5143,10 +5344,6 @@ function OPSGROUP:RouteToMission(mission, delay) self:MissionExecute(mission) return end - - if self.speedMax<=3.6 or mission.teleport then - --self:ClearWaypoints() - end -- ID of current waypoint. local uid=self:GetWaypointCurrentUID() @@ -5157,8 +5354,6 @@ function OPSGROUP:RouteToMission(mission, delay) -- Current coordinate of the group. local currentcoord=self:GetCoordinate() - currentcoord:MarkToAll(mission:GetName(),ReadOnly,Text) - -- Road connection. local roadcoord=currentcoord:GetClosestPointToRoad() @@ -5181,12 +5376,9 @@ function OPSGROUP:RouteToMission(mission, delay) surfacetypes={land.SurfaceType.WATER, land.SurfaceType.SHALLOW_WATER} end - -- Get ingress waypoint. if mission.opstransport and not mission.opstransport:IsCargoDelivered(self.groupname) then - --env.info(self.lid.."FF mission waypoint in embark zone") - -- Get transport zone combo. local tzc=mission.opstransport:GetTZCofCargo(self.groupname) @@ -5200,7 +5392,6 @@ function OPSGROUP:RouteToMission(mission, delay) else -- Get a random coordinate inside the pickup zone. waypointcoord=pickupzone:GetRandomCoordinate() - --waypointcoord:MarkToAll(self.lid.." embark here") end elseif mission.type==AUFTRAG.Type.PATROLZONE or @@ -5219,7 +5410,6 @@ function OPSGROUP:RouteToMission(mission, delay) -- Random coordinate. waypointcoord=targetzone:GetRandomCoordinate(nil , nil, surfacetypes) - waypointcoord:MarkToAll(mission:GetName(),ReadOnly,Text) elseif mission.type==AUFTRAG.Type.ONGUARD or mission.type==AUFTRAG.Type.ARMOREDGUARD then --- -- Guard @@ -5338,65 +5528,41 @@ function OPSGROUP:RouteToMission(mission, delay) -- ARTY --- - -- Coord - local coord=waypointcoord - - -- Get weapon range. - local weapondata=self:GetWeaponData(mission.engageWeaponType) - - local coordInRange=nil --Core.Point#COORDINATE - if weapondata then - - -- Get target coordinate. - local targetcoord=mission:GetTargetCoordinate() - - -- Heading to target. - local heading=coord:HeadingTo(targetcoord) - - -- Distance to target. - local dist=coord:Get2DDistance(targetcoord) - - -- Check if we are within range. - if dist>weapondata.RangeMax then - - local d=(dist-weapondata.RangeMax)*1.1 - - -- New waypoint coord. - coordInRange=coord:Translate(d, heading) - - -- Debug info. - self:T(self.lid..string.format("Out of max range = %.1f km for weapon %s", weapondata.RangeMax/1000, tostring(mission.engageWeaponType))) - elseif dist0 then @@ -6652,6 +6827,37 @@ function OPSGROUP:onafterElementDamaged(From, Event, To, Element) end +--- On after "ElementHit" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP.Element Element The flight group element. +-- @param Wrapper.Unit#UNIT Enemy Unit that hit the element or `nil`. +function OPSGROUP:onafterElementHit(From, Event, To, Element, Enemy) + + -- Increase element hit counter. + Element.Nhit=Element.Nhit+1 + + -- Debug message. + self:T(self.lid..string.format("Element hit %s by %s [n=%d, N=%d]", Element.name, Enemy and Enemy:GetName() or "unknown", Element.Nhit, self.Nhit)) + + -- Group was hit. + self:__Hit(-3, Enemy) + +end + +--- On after "Hit" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Unit#UNIT Enemy Unit that hit the element or `nil`. +function OPSGROUP:onafterHit(From, Event, To, Enemy) + self:T(self.lid..string.format("Group hit by %s", Enemy and Enemy:GetName() or "unknown")) +end + + --- On after "ElementDestroyed" event. -- @param #OPSGROUP self -- @param #string From From state. @@ -6818,9 +7024,9 @@ function OPSGROUP:Teleport(Coordinate, Delay, NoPauseMission) if self:IsFlightgroup() then Template.route.points[1]=Coordinate:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, 300, true, nil, nil, "Spawnpoint") elseif self:IsArmygroup() then - Template.route.points[1]=Coordinate:WaypointGround() + Template.route.points[1]=Coordinate:WaypointGround(0) elseif self:IsNavygroup() then - Template.route.points[1]=Coordinate:WaypointNaval() + Template.route.points[1]=Coordinate:WaypointNaval(0) end -- Template units. @@ -6892,6 +7098,7 @@ function OPSGROUP:_Respawn(Delay, Template, Reset) -- Number of destroyed units. self.Ndestroyed=0 + self.Nhit=0 -- Check if group is currently alive. if self:IsAlive() then @@ -7192,6 +7399,8 @@ function OPSGROUP:onafterStop(From, Event, To) self:UnHandleEvent(EVENTS.Ejection) self:UnHandleEvent(EVENTS.Crash) self.currbase=nil + elseif self.isArmygroup then + self:UnHandleEvent(EVENTS.Hit) end for _,_mission in pairs(self.missionqueue) do @@ -9038,7 +9247,7 @@ function OPSGROUP:onafterUnloaded(From, Event, To, OpsGroupCargo) OpsGroupCargo:Returned() end - if OpsGroupCargo.missionpaused then + if self:_CountPausedMissions()>0 then OpsGroupCargo:UnpauseMission() end @@ -9617,10 +9826,14 @@ function OPSGROUP:_CheckGroupDone(delay) -- Number of cargo transports remaining. local nTransports=self:CountRemainingTransports() + + -- Number of paused missions. + local nPaused=self:_CountPausedMissions() - -- First check if there is a paused mission that - if self.missionpaused and nMissions==1 then - self:T(self.lid..string.format("Found paused mission %s [%s]. Unpausing mission...", self.missionpaused.name, self.missionpaused.type)) + -- First check if there is a paused mission and that all remaining missions are paused. If there are other missions in the queue, we will run those. + if nPaused>0 and nPaused==nMissions then + local missionpaused=self:_GetPausedMission() + self:T(self.lid..string.format("Found paused mission %s [%s]. Unpausing mission...", missionpaused.name, missionpaused.type)) self:UnpauseMission() return end @@ -12413,7 +12626,8 @@ function OPSGROUP:_AddElementByName(unitname) element.gid=element.DCSunit:getNumber() element.uid=element.DCSunit:getID() --element.group=unit:GetGroup() - element.controller=element.DCSunit:getController() + element.controller=element.DCSunit:getController() + element.Nhit=0 element.opsgroup=self -- Skill etc. @@ -12431,7 +12645,7 @@ function OPSGROUP:_AddElementByName(unitname) element.categoryname=unit:GetCategoryName() element.typename=unit:GetTypeName() - + -- Describtors. --self:I({desc=element.descriptors}) -- Ammo. diff --git a/Moose Development/Moose/Ops/Target.lua b/Moose Development/Moose/Ops/Target.lua index faba46a41..f74268b9f 100644 --- a/Moose Development/Moose/Ops/Target.lua +++ b/Moose Development/Moose/Ops/Target.lua @@ -37,6 +37,7 @@ -- @field Ops.Intelligence#INTEL.Contact contact Contact attached to this target. -- @field #boolean isDestroyed If true, target objects were destroyed. -- @field #table resources Resource list. +-- @field #table conditionStart Start condition functions. -- @extends Core.Fsm#FSM --- **It is far more important to be able to hit the target than it is to haggle over who makes a weapon or who pulls a trigger** -- Dwight D Eisenhower @@ -65,7 +66,8 @@ TARGET = { Ndead = 0, elements = {}, casualties = {}, - threatlevel0 = 0 + threatlevel0 = 0, + conditionStart = {}, } @@ -318,6 +320,95 @@ function TARGET:SetImportance(Importance) return self end +--- Add start condition. +-- @param #TARGET self +-- @param #function ConditionFunction Function that needs to be true before the mission can be started. Must return a #boolean. +-- @param ... Condition function arguments if any. +-- @return #TARGET self +function TARGET:AddConditionStart(ConditionFunction, ...) + + local condition={} --Ops.Auftrag#AUFTRAG.Condition + + condition.func=ConditionFunction + condition.arg={} + if arg then + condition.arg=arg + end + + table.insert(self.conditionStart, condition) + + return self +end + +--- Add stop condition. +-- @param #TARGET self +-- @param #function ConditionFunction Function that needs to be true before the mission can be started. Must return a #boolean. +-- @param ... Condition function arguments if any. +-- @return #TARGET self +function TARGET:AddConditionStop(ConditionFunction, ...) + + local condition={} --Ops.Auftrag#AUFTRAG.Condition + + condition.func=ConditionFunction + condition.arg={} + if arg then + condition.arg=arg + end + + table.insert(self.conditionStop, condition) + + return self +end + +--- Check if all given condition are true. +-- @param #TARGET self +-- @param #table Conditions Table of conditions. +-- @return #boolean If true, all conditions were true. Returns false if at least one condition returned false. +function TARGET:EvalConditionsAll(Conditions) + + -- Any stop condition must be true. + for _,_condition in pairs(Conditions or {}) do + local condition=_condition --Ops.Auftrag#AUFTRAG.Condition + + -- Call function. + local istrue=condition.func(unpack(condition.arg)) + + -- Any false will return false. + if not istrue then + return false + end + + end + + -- All conditions were true. + return true +end + + +--- Check if any of the given conditions is true. +-- @param #TARGET self +-- @param #table Conditions Table of conditions. +-- @return #boolean If true, at least one condition is true. +function TARGET:EvalConditionsAny(Conditions) + + -- Any stop condition must be true. + for _,_condition in pairs(Conditions or {}) do + local condition=_condition --Ops.Auftrag#AUFTRAG.Condition + + -- Call function. + local istrue=condition.func(unpack(condition.arg)) + + -- Any true will return true. + if istrue then + return true + end + + end + + -- No condition was true. + return false +end + --- Add mission type and number of required assets to resource. -- @param #TARGET self -- @param #string MissionType Mission Type. diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index 73a306509..5484e533a 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -187,6 +187,7 @@ GROUPTEMPLATE.Takeoff = { -- @field #string GROUND_APC Infantry carriers, in particular Amoured Personell Carrier. This can be used to transport other assets. -- @field #string GROUND_TRUCK Unarmed ground vehicles, which has the DCS "Truck" attribute. -- @field #string GROUND_INFANTRY Ground infantry assets. +-- @field #string GROUND_IFV Ground Infantry Fighting Vehicle. -- @field #string GROUND_ARTILLERY Artillery assets. -- @field #string GROUND_TANK Tanks (modern or old). -- @field #string GROUND_TRAIN Trains. Not that trains are **not** yet properly implemented in DCS and cannot be used currently. @@ -213,6 +214,7 @@ GROUP.Attribute = { GROUND_APC="Ground_APC", GROUND_TRUCK="Ground_Truck", GROUND_INFANTRY="Ground_Infantry", + GROUND_IFV="Ground_IFV", GROUND_ARTILLERY="Ground_Artillery", GROUND_TANK="Ground_Tank", GROUND_TRAIN="Ground_Train", @@ -2378,13 +2380,14 @@ function GROUP:GetAttribute() --- Ground --- -------------- -- Ground - local apc=self:HasAttribute("Infantry carriers") + local apc=self:HasAttribute("APC") local truck=self:HasAttribute("Trucks") and self:GetCategory()==Group.Category.GROUND local infantry=self:HasAttribute("Infantry") local artillery=self:HasAttribute("Artillery") local tank=self:HasAttribute("Old Tanks") or self:HasAttribute("Modern Tanks") local aaa=self:HasAttribute("AAA") local ewr=self:HasAttribute("EWR") + local ifv=self:HasAttribute("IFV") local sam=self:HasAttribute("SAM elements") and (not self:HasAttribute("AAA")) -- Train local train=self:GetCategory()==Group.Category.TRAIN @@ -2432,6 +2435,8 @@ function GROUP:GetAttribute() attribute=GROUP.Attribute.GROUND_APC elseif infantry then attribute=GROUP.Attribute.GROUND_INFANTRY + elseif ifv then + attribute=GROUP.Attribute.GROUND_IFV elseif truck then attribute=GROUP.Attribute.GROUND_TRUCK elseif train then From edbfa9117d5aa2805dd1d0375bba37dec79ad95b Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 30 May 2022 11:45:56 +0200 Subject: [PATCH 10/20] OPS - Added new **CONDITION** class - Added new **OPERATION** class --- Moose Development/Moose/Core/Condition.lua | 279 +++++++++++ Moose Development/Moose/Modules.lua | 2 + Moose Development/Moose/Ops/Auftrag.lua | 10 +- Moose Development/Moose/Ops/Cohort.lua | 14 +- Moose Development/Moose/Ops/Commander.lua | 29 +- Moose Development/Moose/Ops/FlightControl.lua | 185 +++++--- Moose Development/Moose/Ops/FlightGroup.lua | 42 +- Moose Development/Moose/Ops/Operation.lua | 434 ++++++++++++++++++ Moose Development/Moose/Ops/OpsGroup.lua | 2 +- Moose Setup/Moose.files | 2 + 10 files changed, 910 insertions(+), 89 deletions(-) create mode 100644 Moose Development/Moose/Core/Condition.lua create mode 100644 Moose Development/Moose/Ops/Operation.lua diff --git a/Moose Development/Moose/Core/Condition.lua b/Moose Development/Moose/Core/Condition.lua new file mode 100644 index 000000000..d64b8c727 --- /dev/null +++ b/Moose Development/Moose/Core/Condition.lua @@ -0,0 +1,279 @@ +--- **Core** - Define any or all conditions to be evaluated. +-- +-- **Main Features:** +-- +-- * Add arbitrary numbers of conditon functions +-- * Evaluate *any* or *all* conditions +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Core.Condition +-- @image Core_Condition.png + + +--- CONDITON class. +-- @type CONDITION +-- @field #string ClassName Name of the class. +-- @field #string lid Class id string for output to DCS log file. +-- @field #boolean isAny General functions are evaluated as any condition. +-- @field #boolean negateResult Negeate result of evaluation. +-- @field #table functionsGen General condition functions. +-- @field #table functionsAny Any condition functions. +-- @field #table functionsAll All condition functions. +-- +-- @extends Core.Base#BASE + +--- *Better three hours too soon than a minute too late.* - William Shakespeare +-- +-- === +-- +-- # The CONDITION Concept +-- +-- +-- +-- @field #CONDITION +CONDITION = { + ClassName = "CONDITION", + lid = nil, + functionsGen = {}, + functionsAny = {}, + functionsAll = {}, +} + +--- Condition function. +-- @type CONDITION.Function +-- @field #function func Callback function to check for a condition. Should return a `#boolean`. +-- @field #table arg (Optional) Arguments passed to the condition callback function if any. + +--- CONDITION class version. +-- @field #string version +CONDITION.version="0.0.1" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Make FSM. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new CONDITION object. +-- @param #CONDITION self +-- @param #string Name (Optional) Name used in the logs. +-- @return #CONDITION self +function CONDITION:New(Name) + + -- Inherit BASE. + local self=BASE:Inherit(self, BASE:New()) --#CONDITION + + self.name=Name or "Condition X" + + self.lid=string.format("%s | ", self.name) + + return self +end + +--- Set that general condition functions return `true` if `any` function returns `true`. Default is that *all* functions must return `true`. +-- @param #CONDITION self +-- @param #boolean Any If `true`, *any* condition can be true. Else *all* conditions must result `true`. +-- @return #CONDITION self +function CONDITION:SetAny(Any) + self.isAny=Any + return self +end + +--- Negate result. +-- @param #CONDITION self +-- @param #boolean Negate If `true`, result is negated else not. +-- @return #CONDITION self +function CONDITION:SetNegateResult(Negate) + self.negateResult=Negate + return self +end + +--- Add a function that is evaluated. It must return a `#boolean` value, *i.e.* either `true` or `false` (or `nil`). +-- @param #CONDITION self +-- @param #function Function The function to call. +-- @param ... (Optional) Parameters passed to the function (if any). +-- +-- @usage +-- local function isAequalB(a, b) +-- return a==b +-- end +-- +-- myCondition:AddFunction(isAequalB, a, b) +-- +-- @return #CONDITION self +function CONDITION:AddFunction(Function, ...) + + -- Condition function. + local condition=self:_CreateCondition(Function, ...) + + -- Add to table. + table.insert(self.functionsGen, condition) + + return self +end + +--- Add a function that is evaluated. It must return a `#boolean` value, *i.e.* either `true` or `false` (or `nil`). +-- @param #CONDITION self +-- @param #function Function The function to call. +-- @param ... (Optional) Parameters passed to the function (if any). +-- @return #CONDITION self +function CONDITION:AddFunctionAny(Function, ...) + + -- Condition function. + local condition=self:_CreateCondition(Function, ...) + + -- Add to table. + table.insert(self.functionsAny, condition) + + return self +end + +--- Add a function that is evaluated. It must return a `#boolean` value, *i.e.* either `true` or `false` (or `nil`). +-- @param #CONDITION self +-- @param #function Function The function to call. +-- @param ... (Optional) Parameters passed to the function (if any). +-- @return #CONDITION self +function CONDITION:AddFunctionAll(Function, ...) + + -- Condition function. + local condition=self:_CreateCondition(Function, ...) + + -- Add to table. + table.insert(self.functionsAll, condition) + + return self +end + + +--- Evaluate conditon functions. +-- @param #CONDITION self +-- @param #boolean AnyTrue If `true`, evaluation return `true` if *any* condition function returns `true`. By default, *all* condition functions must return true. +-- @return #boolean Result of condition functions. +function CONDITION:Evaluate(AnyTrue) + + -- Any condition for gen. + local evalAny=self.isAny + if AnyTrue~=nil then + evalAny=AnyTrue + end + + local isGen=nil + if evalAny then + isGen=self:_EvalConditionsAny(self.functionsGen) + else + isGen=self:_EvalConditionsAll(self.functionsGen) + end + + -- Is any? + local isAny=self:_EvalConditionsAny(self.functionsAny) + + -- Is all? + local isAll=self:_EvalConditionsAll(self.functionsAll) + + -- Result. + local result=isGen and isAny and isAll + + -- Negate result. + if self.negateResult then + result=not result + end + + -- Debug message. + self:I(self.lid..string.format("Evaluate: isGen=%s, isAny=%s, isAll=%s (negate=%s) ==> result=%s", tostring(isGen), tostring(isAny), tostring(isAll), tostring(self.negateResult), tostring(result))) + + return result +end + +--- Check if all given condition are true. +-- @param #CONDITION self +-- @param #table functions Functions to evaluate. +-- @return #boolean If true, all conditions were true (or functions was empty/nil). Returns false if at least one condition returned false. +function CONDITION:_EvalConditionsAll(functions) + + -- At least one condition? + local gotone=false + + + -- Any stop condition must be true. + for _,_condition in pairs(functions or {}) do + local condition=_condition --#CONDITION.Function + + -- At least one condition was defined. + gotone=true + + -- Call function. + local istrue=condition.func(unpack(condition.arg)) + + -- Any false will return false. + if not istrue then + return false + end + + end + + -- All conditions were true. + return true +end + + +--- Check if any of the given conditions is true. +-- @param #CONDITION self +-- @param #table functions Functions to evaluate. +-- @return #boolean If true, at least one condition is true (or functions was emtpy/nil). +function CONDITION:_EvalConditionsAny(functions) + + -- At least one condition? + local gotone=false + + -- Any stop condition must be true. + for _,_condition in pairs(functions or {}) do + local condition=_condition --#CONDITION.Function + + -- At least one condition was defined. + gotone=true + + -- Call function. + local istrue=condition.func(unpack(condition.arg)) + + -- Any true will return true. + if istrue then + return true + end + + end + + -- No condition was true. + if gotone then + return false + else + -- No functions passed. + return true + end +end + +--- Create conditon fucntion object. +-- @param #CONDITION self +-- @param #function Function The function to call. +-- @param ... (Optional) Parameters passed to the function (if any). +-- @return #CONDITION.Function Condition function. +function CONDITION:_CreateCondition(Function, ...) + + local condition={} --#CONDITION.Function + + condition.func=Function + condition.arg={} + if arg then + condition.arg=arg + end + + return condition +end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- \ No newline at end of file diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index 7d15b28b1..9928e2e59 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -31,6 +31,7 @@ __Moose.Include( 'Scripts/Moose/Core/Spot.lua' ) __Moose.Include( 'Scripts/Moose/Core/Astar.lua' ) __Moose.Include( 'Scripts/Moose/Core/MarkerOps_Base.lua' ) __Moose.Include( 'Scripts/Moose/Core/TextAndSound.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Condition.lua' ) __Moose.Include( 'Scripts/Moose/Wrapper/Object.lua' ) __Moose.Include( 'Scripts/Moose/Wrapper/Identifiable.lua' ) @@ -101,6 +102,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/Operation.lua' ) __Moose.Include( 'Scripts/Moose/Ops/FlightControl.lua' ) __Moose.Include( 'Scripts/Moose/AI/AI_Balancer.lua' ) diff --git a/Moose Development/Moose/Ops/Auftrag.lua b/Moose Development/Moose/Ops/Auftrag.lua index 5142b87d1..f03539d27 100644 --- a/Moose Development/Moose/Ops/Auftrag.lua +++ b/Moose Development/Moose/Ops/Auftrag.lua @@ -4143,7 +4143,7 @@ function AUFTRAG:CheckGroupsDone() if groupdata then if not (groupdata.status==AUFTRAG.GroupStatus.DONE or groupdata.status==AUFTRAG.GroupStatus.CANCELLED) then -- At least this flight is not DONE or CANCELLED. - self:T(self.lid..string.format("CheckGroupsDone: OPSGROUP %s is not DONE or CANCELLED but in state %s. Mission NOT DONE!", groupdata.opsgroup.groupname, groupdata.status)) + self:T2(self.lid..string.format("CheckGroupsDone: OPSGROUP %s is not DONE or CANCELLED but in state %s. Mission NOT DONE!", groupdata.opsgroup.groupname, groupdata.status:upper())) return false end end @@ -4155,7 +4155,7 @@ function AUFTRAG:CheckGroupsDone() local status=self:GetLegionStatus(legion) if not status==AUFTRAG.Status.CANCELLED then -- At least one LEGION has not CANCELLED. - self:T(self.lid..string.format("CheckGroupsDone: LEGION %s is not CANCELLED but in state %s. Mission NOT DONE!", legion.alias, status)) + self:T2(self.lid..string.format("CheckGroupsDone: LEGION %s is not CANCELLED but in state %s. Mission NOT DONE!", legion.alias, status)) return false end end @@ -4163,7 +4163,7 @@ function AUFTRAG:CheckGroupsDone() -- Check commander status. if self.commander then if not self.statusCommander==AUFTRAG.Status.CANCELLED then - self:T(self.lid..string.format("CheckGroupsDone: COMMANDER is not CANCELLED but in state %s. Mission NOT DONE!", self.statusCommander)) + self:T2(self.lid..string.format("CheckGroupsDone: COMMANDER is not CANCELLED but in state %s. Mission NOT DONE!", self.statusCommander)) return false end end @@ -4171,14 +4171,14 @@ function AUFTRAG:CheckGroupsDone() -- Check chief status. if self.chief then if not self.statusChief==AUFTRAG.Status.CANCELLED then - self:T(self.lid..string.format("CheckGroupsDone: CHIEF is not CANCELLED but in state %s. Mission NOT DONE!", self.statusChief)) + self:T2(self.lid..string.format("CheckGroupsDone: CHIEF is not CANCELLED but in state %s. Mission NOT DONE!", self.statusChief)) return false end end -- These are early stages, where we might not even have a opsgroup defined to be checked. If there were any groups, we checked above. if self:IsPlanned() or self:IsQueued() or self:IsRequested() then - self:T(self.lid..string.format("CheckGroupsDone: Mission is still in state %s [FSM=%s] (PLANNED or QUEUED or REQUESTED). Mission NOT DONE!", self.status, self:GetState())) + self:T2(self.lid..string.format("CheckGroupsDone: Mission is still in state %s [FSM=%s] (PLANNED or QUEUED or REQUESTED). Mission NOT DONE!", self.status, self:GetState())) return false end diff --git a/Moose Development/Moose/Ops/Cohort.lua b/Moose Development/Moose/Ops/Cohort.lua index 5b144c499..4ffcf6c01 100644 --- a/Moose Development/Moose/Ops/Cohort.lua +++ b/Moose Development/Moose/Ops/Cohort.lua @@ -48,6 +48,7 @@ -- @field #table tacanChannel List of TACAN channels available to the cohort. -- @field #number weightAsset Weight of one assets group in kg. -- @field #number cargobayLimit Cargo bay capacity in kg. +-- @field #table operations Operations this cohort is part of. -- @extends Core.Fsm#FSM --- *I came, I saw, I conquered.* -- Julius Caesar @@ -82,6 +83,7 @@ COHORT = { cargobayLimit = 0, descriptors = {}, properties = {}, + operations = {}, } --- COHORT class version. @@ -1544,7 +1546,17 @@ function COHORT:_MissileCategoryName(categorynumber) cat="other" end return cat -end +end + +--- Add an OPERATION. +-- @param #COHORT self +-- @param Ops.Operation#OPERATION Operation The operation this cohort is part of. +-- @return #COHORT self +function COHORT:_AddOperation(Operation) + + self.operations[Operation.name]=Operation + +end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Commander.lua b/Moose Development/Moose/Ops/Commander.lua index 42296babe..3c4c18503 100644 --- a/Moose Development/Moose/Ops/Commander.lua +++ b/Moose Development/Moose/Ops/Commander.lua @@ -25,6 +25,7 @@ -- @field #table missionqueue Mission queue. -- @field #table transportqueue Transport queue. -- @field #table targetqueue Target queue. +-- @field #table opsqueue Operations queue. -- @field #table rearmingZones Rearming zones. Each element is of type `#BRIGADE.SupplyZone`. -- @field #table refuellingZones Refuelling zones. Each element is of type `#BRIGADE.SupplyZone`. -- @field #table capZones CAP zones. Each element is of type `#AIRWING.PatrolZone`. @@ -127,6 +128,7 @@ COMMANDER = { missionqueue = {}, transportqueue = {}, targetqueue = {}, + opsqueue = {}, rearmingZones = {}, refuellingZones = {}, capZones = {}, @@ -841,6 +843,9 @@ function COMMANDER:onafterStatus(From, Event, To) self:T(self.lid..text) end + -- Check Operations queue. + self:CheckOpsQueue() + -- Check target queue and add missions. self:CheckTargetQueue() @@ -1217,6 +1222,28 @@ end -- Mission Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Check OPERATIONs queue. +-- @param #COMMANDER self +function COMMANDER:CheckOpsQueue() + + -- Number of missions. + local Nops=#self.opsqueue + + -- Treat special cases. + if Nops==0 then + return nil + end + + -- Loop over operations. + for _,_ops in pairs(self.opsqueue) do + local operation=_ops --Ops.Operation#OPRATION + + --TODO: What? + + end + +end + --- Check target queue and assign ONE valid target by adding it to the mission queue of the COMMANDER. -- @param #COMMANDER self function COMMANDER:CheckTargetQueue() @@ -1281,7 +1308,7 @@ function COMMANDER:CheckTargetQueue() local isReadyStart=target:EvalConditionsAll(target.conditionStart) -- Debug message. - local text=string.format("Target %s: Alive=%s, Threat=%s, Important=%s", target:GetName(), tostring(isAlive), tostring(isThreat), tostring(isImportant)) + local text=string.format("Target %s: Alive=%s, Important=%s", target:GetName(), tostring(isAlive), tostring(isImportant)) self:T2(self.lid..text) -- Check that target is alive and not already a mission has been assigned. diff --git a/Moose Development/Moose/Ops/FlightControl.lua b/Moose Development/Moose/Ops/FlightControl.lua index cd0a2305f..039541019 100644 --- a/Moose Development/Moose/Ops/FlightControl.lua +++ b/Moose Development/Moose/Ops/FlightControl.lua @@ -149,13 +149,13 @@ FLIGHTCONTROL = { FLIGHTCONTROL.FlightStatus={ PARKING="Parking", READYTX="Ready To Taxi", - TAXIOUT="Taxi to runway", + TAXIOUT="Taxi To Runway", READYTO="Ready For Takeoff", TAKEOFF="Takeoff", INBOUND="Inbound", HOLDING="Holding", LANDING="Landing", - TAXIINB="Taxi Inbound", + TAXIINB="Taxi To Parking", ARRIVED="Arrived", } @@ -172,21 +172,20 @@ FLIGHTCONTROL.version="0.5.2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list --- +-- TODO: Runway destroyed. -- TODO: Support airwings. Dont give clearance for Alert5 or if mission has not started. -- TODO: Switch to enable/disable AI messages. -- TODO: Improve ATC TTS messages. --- TODO: Add helos. -- TODO: Talk me down option. -- TODO: ATIS option. -- TODO: Check runways and clean up. -- TODO: Accept and forbit parking spots. --- TODO: Define holding zone. +-- TODO: Add FARPS? +-- DONE: Define holding zone. -- DONE: Basic ATC voice overs. -- DONE: Add SRS TTS. -- DONE: Add parking guard. --- NOGO: Add FARPS? -- DONE: Interface with FLIGHTGROUP. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -793,7 +792,7 @@ function FLIGHTCONTROL:_CheckQueues() else -- TODO: Humans have to confirm via F10 menu. self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.LANDING) - flight:_UpdateMenu() + flight:_UpdateMenu(0.5) end -- Set time last flight got landing clearance. @@ -1299,7 +1298,7 @@ function FLIGHTCONTROL:_RemoveFlightFromQueue(queue, flight, queuename) table.remove(queue, i) if not flight.isAI then - flight:_UpdateMenu() + flight:_UpdateMenu(0.5) end return true, i @@ -1797,6 +1796,9 @@ function FLIGHTCONTROL:_CreatePlayerMenu(flight, mainmenu) MENU_GROUP_COMMAND:New(group, "Radio Check", helpmenu, self._PlayerRadioCheck, self, groupname) MENU_GROUP_COMMAND:New(group, "Confirm Status", helpmenu, self._PlayerConfirmStatus, self, groupname) MENU_GROUP_COMMAND:New(group, "Mark Holding", helpmenu, self._PlayerNotImplemented, self, groupname) + if gotcontrol and flight:IsInbound() and flight.stack then + MENU_GROUP_COMMAND:New(group, "Vector Holding", helpmenu, self._PlayerVectorInbound, self, groupname) + end --- -- Info Menu @@ -1832,22 +1834,24 @@ function FLIGHTCONTROL:_CreatePlayerMenu(flight, mainmenu) --- -- Taxiing --- - - if status==FLIGHTCONTROL.FlightStatus.READYTO then + + if status==FLIGHTCONTROL.FlightStatus.READYTX or status==FLIGHTCONTROL.FlightStatus.TAXIOUT then + -- Flight is "ready to taxi" (awaiting clearance) or "taxiing to runway". + MENU_GROUP_COMMAND:New(group, "Request Takeoff", rootmenu, self._PlayerRequestTakeoff, self, groupname) + MENU_GROUP_COMMAND:New(group, "Abort Taxi", rootmenu, self._PlayerAbortTaxi, self, groupname) + elseif status==FLIGHTCONTROL.FlightStatus.READYTO then + -- Flight is ready for take off. MENU_GROUP_COMMAND:New(group, "Abort Takeoff", rootmenu, self._PlayerAbortTakeoff, self, groupname) elseif status==FLIGHTCONTROL.FlightStatus.TAKEOFF then + -- Flight is taking off. MENU_GROUP_COMMAND:New(group, "Abort Takeoff", rootmenu, self._PlayerAbortTakeoff, self, groupname) - elseif status==FLIGHTCONTROL.FlightStatus.READYTX or status==FLIGHTCONTROL.FlightStatus.TAXIOUT then - MENU_GROUP_COMMAND:New(group, "Abort Taxi", rootmenu, self._PlayerAbortTaxi, self, groupname) - MENU_GROUP_COMMAND:New(group, "Request Takeoff", rootmenu, self._PlayerRequestTakeoff, self, groupname) elseif status==FLIGHTCONTROL.FlightStatus.TAXIINB then -- Could be after "abort taxi" call and we changed our mind (again) - MENU_GROUP_COMMAND:New(group, "Request Parking", rootmenu, self._PlayerRequestParking, self, groupname) - MENU_GROUP_COMMAND:New(group, "Request Taxi", rootmenu, self._PlayerRequestTaxi, self, groupname) + MENU_GROUP_COMMAND:New(group, "Request Taxi", rootmenu, self._PlayerRequestTaxi, self, groupname) + MENU_GROUP_COMMAND:New(group, "Request Parking", rootmenu, self._PlayerRequestParking, self, groupname) + MENU_GROUP_COMMAND:New(group, "Arrived at Parking", rootmenu, self._PlayerArrived, self, groupname) end - MENU_GROUP_COMMAND:New(group, "Arrived and Parking", rootmenu, self._PlayerArrived, self, groupname) - elseif flight:IsAirborne() then --- -- Airborne @@ -1858,16 +1862,19 @@ function FLIGHTCONTROL:_CreatePlayerMenu(flight, mainmenu) -- Inbound --- - MENU_GROUP_COMMAND:New(group, "Abort Inbound", rootmenu, self._PlayerAbortInbound, self, groupname) MENU_GROUP_COMMAND:New(group, "Holding", rootmenu, self._PlayerHolding, self, groupname) + MENU_GROUP_COMMAND:New(group, "Abort Inbound", rootmenu, self._PlayerAbortInbound, self, groupname) + MENU_GROUP_COMMAND:New(group, "Request Parking", rootmenu, self._PlayerRequestParking, self, groupname) + elseif flight:IsHolding() then --- -- Holding --- - MENU_GROUP_COMMAND:New(group, "Abort Holding", rootmenu, self._PlayerAbortHolding, self, groupname) MENU_GROUP_COMMAND:New(group, "Landing", rootmenu, self._PlayerConfirmLanding, self, groupname) + MENU_GROUP_COMMAND:New(group, "Abort Holding", rootmenu, self._PlayerAbortHolding, self, groupname) + MENU_GROUP_COMMAND:New(group, "Request Parking", rootmenu, self._PlayerRequestParking, self, groupname) elseif flight:IsLanding() then --- @@ -1875,11 +1882,16 @@ function FLIGHTCONTROL:_CreatePlayerMenu(flight, mainmenu) --- MENU_GROUP_COMMAND:New(group, "Abort Landing", rootmenu, self._PlayerAbortLanding, self, groupname) - - end - - if flight:IsInbound() or flight:IsHolding() or flight:IsLanding() or flight:IsLanded() then MENU_GROUP_COMMAND:New(group, "Request Parking", rootmenu, self._PlayerRequestParking, self, groupname) + + elseif flight:IsLanded() then + --- + -- Landed + --- + + MENU_GROUP_COMMAND:New(group, "Arrived at Parking", rootmenu, self._PlayerArrived, self, groupname) + MENU_GROUP_COMMAND:New(group, "Request Parking", rootmenu, self._PlayerRequestParking, self, groupname) + end else @@ -2158,6 +2170,9 @@ function FLIGHTCONTROL:_PlayerRequestInbound(groupname) -- Call sign. local callsign=flight:GetCallsignName() + + -- Get player element. + local player=flight:GetPlayerElement() -- Pilot calls inbound for landing. local text=string.format("%s, %s, inbound for landing", self.alias, callsign) @@ -2165,20 +2180,17 @@ function FLIGHTCONTROL:_PlayerRequestInbound(groupname) -- Radio message. self:TransmissionPilot(text, flight) + -- Current player coord. + local flightcoord=flight:GetCoordinate(nil, player.name) + -- Distance from player to airbase. - local dist=flight:GetCoordinate():Get2DDistance(self:GetCoordinate()) + local dist=flightcoord:Get2DDistance(self:GetCoordinate()) if dist0 then if Nlanding==1 then - text=string.format("Negative, we got %d flight inbound before it's your turn. Hold position until futher notice.", Nlanding) + text=text..string.format("negative, we got %d flight inbound before it's your turn. Hold position until futher notice.", Nlanding) else - text=string.format("Negative, we got %d flights inbound. Hold positon until futher notice.", Nlanding) + text=text..string.format("negative, we got %d flights inbound. Hold positon until futher notice.", Nlanding) end end @@ -2706,7 +2775,7 @@ function FLIGHTCONTROL:_PlayerRequestTakeoff(groupname) self:TransmissionTower(text, flight, 10) -- Update menu. - flight:_UpdateMenu() + flight:_UpdateMenu(0.5) else self:TextMessageToFlight(string.format("Negative, you must request TAXI before you can request TAKEOFF!"), flight) @@ -2755,7 +2824,7 @@ function FLIGHTCONTROL:_PlayerAbortTakeoff(groupname) self:TransmissionTower(text, flight, 10) -- Update menu. - flight:_UpdateMenu() + flight:_UpdateMenu(0.5) else self:TextMessageToFlight("Negative, You are NOT in the takeoff queue", flight) @@ -2857,7 +2926,7 @@ function FLIGHTCONTROL:_PlayerArrived(groupname) local player=flight:GetPlayerElement() -- Get current coordinate. - local coord=flight:GetCoordinate(player.name) + local coord=flight:GetCoordinate(nil, player.name) --Closest parking spot. local spot=self:GetClosestParkingSpot(coord) @@ -2882,7 +2951,7 @@ function FLIGHTCONTROL:_PlayerArrived(groupname) self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.PARKING) -- Create player menu. - flight:_UpdateMenu() + flight:_UpdateMenu(0.5) -- Create mark on F10 map. --[[ diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index 8854055a4..65b8ae44e 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -385,7 +385,7 @@ function FLIGHTGROUP:SetReadyForTakeoff(ReadyTO, Delay) return self end ---- Set the FLIGHTCONTROL controlling this flight group. +--- Set the FLIGHTCONTROL controlling this flight group. Also updates the player menu after 0.5 sec. -- @param #FLIGHTGROUP self -- @param Ops.FlightControl#FLIGHTCONTROL flightcontrol The FLIGHTCONTROL object. -- @return #FLIGHTGROUP self @@ -569,7 +569,7 @@ function FLIGHTGROUP:IsParking() return self:Is("Parking") end ---- Check if flight is parking. +--- Check if is taxiing to the runway. -- @param #FLIGHTGROUP self -- @return #boolean If true, flight is taxiing after engine start up. function FLIGHTGROUP:IsTaxiing() @@ -1638,10 +1638,10 @@ function FLIGHTGROUP:onafterSpawned(From, Event, To) self:SetFlightControl(flightcontrol) else -- F10 other menu. - self:_UpdateMenu() + self:_UpdateMenu(0.5) end else - self:_UpdateMenu() + self:_UpdateMenu(0.5) end end @@ -1679,7 +1679,7 @@ function FLIGHTGROUP:onafterParking(From, Event, To) if flightcontrol then - -- Set FC for this flight + -- Set FC for this flight. This also updates the menu. self:SetFlightControl(flightcontrol) if self.flightcontrol then @@ -1779,20 +1779,7 @@ function FLIGHTGROUP:onafterCruise(From, Event, To) -- AI --- - --[[ - if self:IsTransporting() then - if self.cargoTransport and self.cargoTZC and self.cargoTZC.DeployAirbase then - self:LandAtAirbase(self.cargoTZC.DeployAirbase) - end - elseif self:IsPickingup() then - if self.cargoTransport and self.cargoTZC and self.cargoTZC.PickupAirbase then - self:LandAtAirbase(self.cargoTZC.PickupAirbase) - end - else - self:_CheckGroupDone(nil, 120) - end - ]] - + -- Check group Done. self:_CheckGroupDone(nil, 120) else @@ -1801,7 +1788,7 @@ function FLIGHTGROUP:onafterCruise(From, Event, To) -- CLIENT --- - self:_UpdateMenu(0.1) + --self:_UpdateMenu(0.1) end @@ -3202,7 +3189,7 @@ function FLIGHTGROUP:_InitGroup(Template) -- Set callsign. Default is set on spawn if not modified by user. local callsign=template.units[1].callsign - self:I({callsign=callsign}) + --self:I({callsign=callsign}) if type(callsign)=="number" then -- Sometimes callsign is just "101". local cs=tostring(callsign) callsign={} @@ -4183,9 +4170,16 @@ function FLIGHTGROUP:_UpdateMenu(delay) -- Delayed call. self:ScheduleOnce(delay, FLIGHTGROUP._UpdateMenu, self) else + + -- Message to group. + MESSAGE:New("Updating MENU state="..self:GetState(), 5):ToGroup(self.group) + env.info(self.lid.."updating menu state=") + + -- Player element. + local player=self:GetPlayerElement() - -- Get current position of group. - local position=self:GetCoordinate() + -- Get current position of player. + local position=self:GetCoordinate(nil, player.name) -- Get all FLIGHTCONTROLS local fc={} @@ -4211,6 +4205,7 @@ function FLIGHTGROUP:_UpdateMenu(delay) -- Remove all submenus. self.menu.atc.root:RemoveSubMenus() + -- Create help menu. self:_CreateMenuAtcHelp(self.menu.atc.root) -- Max menu entries. @@ -4260,6 +4255,7 @@ function FLIGHTGROUP:_CreateMenuAtcHelp(rootmenu) --- MENU_GROUP_COMMAND:New(self.group, "Subtitles On/Off", helpmenu, self._MenuNotImplemented, self, groupname) MENU_GROUP_COMMAND:New(self.group, "My Voice On/Off", helpmenu, self._MenuNotImplemented, self, groupname) + MENU_GROUP_COMMAND:New(self.group, "Update Menu", helpmenu, self._UpdateMenu, self, 0) MENU_GROUP_COMMAND:New(self.group, "My Status", helpmenu, self._PlayerMyStatus, self, groupname) end diff --git a/Moose Development/Moose/Ops/Operation.lua b/Moose Development/Moose/Ops/Operation.lua new file mode 100644 index 000000000..d1ea5aa83 --- /dev/null +++ b/Moose Development/Moose/Ops/Operation.lua @@ -0,0 +1,434 @@ +--- **Ops** - Operation with multiple phases. +-- +-- ## Main Features: +-- +-- * Define operation phases +-- * Dedicate resources to operations +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20Operation). +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module Ops.Operation +-- @image OPS_Operation.png + + +--- OPERATION class. +-- @type OPERATION +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity level. +-- @field #string lid Class id string for output to DCS log file. +-- @field #string name Name of the operation. +-- @field #table cohorts Dedicated cohorts. +-- @field #table legions Dedicated legions. +-- @field #table phases Phases. +-- @field #number counterPhase Running number counting the phases. +-- @field #OPERATION.Phase phase Currently active phase (if any). +-- +-- @extends Core.Fsm#FSM + +--- *A warrior's mission is to foster the success of others.* -- Morihei Ueshiba +-- +-- === +-- +-- # The OPERATION Concept +-- +-- +-- +-- @field #OPERATION +OPERATION = { + ClassName = "OPERATION", + verbose = 0, + lid = nil, + cohorts = {}, + legions = {}, + phases = {}, + counterPhase = 0, +} + +--- Global mission counter. +_OPERATIONID=0 + +--- Operation phase. +-- @type OPERATION.Phase +-- @field #number uid Unique ID of the phase. +-- @field #string name Name of the phase. +-- @field Core.Condition#CONDITION conditionOver Conditions when the phase is over. +-- @field #boolean isOver If `true`, phase is over. + +--- OPERATION class version. +-- @field #string version +OPERATION.version="0.0.1" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: A lot + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new generic OPERATION object. +-- @param #OPERATION self +-- @param #string Name Name of the operation. Be creative! Default "Operation-01" where the last number is a running number. +-- @return #OPERATION self +function OPERATION:New(Name) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #OPERATION + + -- Increase global counter. + _OPERATIONID=_OPERATIONID+1 + + -- Set Name. + self.name=Name or string.format("Operation-%02d", _OPERATIONID) + + -- Set log ID. + self.lid=string.format("%s | ",self.name) + + + -- FMS start state is PLANNED. + self:SetStartState("Planned") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("*", "Start", "Running") + + self:AddTransition("*", "StatusUpdate", "*") + + self:AddTransition("Running", "Pause", "Paused") + self:AddTransition("Paused", "Unpause", "Running") + + self:AddTransition("*", "ChangePhase", "*") + self:AddTransition("*", "PhaseChange", "*") + + self:AddTransition("*", "Over", "Over") + + self:AddTransition("*", "Stop", "Stopped") + + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "StatusUpdate". + -- @function [parent=#OPERATION] StatusUpdate + -- @param #OPERATION self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#OPERATION] __StatusUpdate + -- @param #OPERATION self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Stop". + -- @function [parent=#OPERATION] Stop + -- @param #OPERATION self + + --- Triggers the FSM event "Stop" after a delay. + -- @function [parent=#OPERATION] __Stop + -- @param #OPERATION self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "PhaseChange". + -- @function [parent=#OPERATION] PhaseChange + -- @param #OPERATION self + -- @param #OPERATION.Phase Phase The new phase. + + --- Triggers the FSM event "PhaseChange" after a delay. + -- @function [parent=#OPERATION] __PhaseChange + -- @param #OPERATION self + -- @param #number delay Delay in seconds. + -- @param #OPERATION.Phase Phase The new phase. + + --- On after "PhaseChange" event. + -- @function [parent=#OPERATION] OnAfterPhaseChange + -- @param #OPERATION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #OPERATION.Phase Phase The new phase. + + -- Init status update. + self:__StatusUpdate(-1) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User API Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new generic OPERATION object. +-- @param #OPERATION self +-- @param #string Name Name of the phase. Default "Phase-01" where the last number is a running number. +-- @param Core.Condition#CONDITION ConditionOver Condition when the phase is over. +-- @return #OPERATION.Phase Phase table object. +function OPERATION:AddPhase(Name, ConditionOver) + + -- Increase phase counter. + self.counterPhase=self.counterPhase+1 + + local phase={} --#OPERATION.Phase + phase.uid=self.counterPhase + phase.name=Name or string.format("Phase-%02d", self.counterPhase) + phase.conditionOver=ConditionOver or CONDITION:New(Name) + phase.isOver=false + + -- Add phase. + table.insert(self.phases, phase) + + return phase +end + +--- Get a phase by its name. +-- @param #OPERATION self +-- @param #string Name Name of the phase. Default "Phase-01" where the last number is a running number. +-- @return #OPERATION.Phase Phase table object or nil if phase could not be found. +function OPERATION:GetPhaseByName(Name) + + for _,_phase in pairs(self.phases or {}) do + local phase=_phase --#OPERATION.Phase + if phase.name==Name then + return phase + end + end + + return nil +end + +--- Assign cohort to operation. +-- @param #OPERATION self +-- @param Ops.Cohort#COHORT Cohort The cohort +-- @return #OPERATION self +function OPERATION:AssignCohort(Cohort) + + self.cohorts[Cohort.name]=Cohort + +end + +--- Assign legion to operation. All cohorts of this legion will be assigned and are only available +-- @param #OPERATION self +-- @param Ops.Legion#LEGION Legion The legion to be assigned. +-- @return #OPERATION self +function OPERATION:AssignLegion(Legion) + + self.legions[Legion.alias]=Legion + +end + + + +--- Set start and stop time of the operation. +-- @param #OPERATION self +-- @param #string ClockStart Time the mission is started, e.g. "05:00" for 5 am. If specified as a #number, it will be relative (in seconds) to the current mission time. Default is 5 seconds after mission was added. +-- @param #string ClockStop (Optional) Time the mission is stopped, e.g. "13:00" for 1 pm. If mission could not be started at that time, it will be removed from the queue. If specified as a #number it will be relative (in seconds) to the current mission time. +-- @return #OPERATION self +function OPERATION:SetTime(ClockStart, ClockStop) + + -- Current mission time. + local Tnow=timer.getAbsTime() + + -- Set start time. Default in 5 sec. + local Tstart=Tnow+5 + if ClockStart and type(ClockStart)=="number" then + Tstart=Tnow+ClockStart + elseif ClockStart and type(ClockStart)=="string" then + Tstart=UTILS.ClockToSeconds(ClockStart) + end + + -- Set stop time. Default nil. + local Tstop=nil + if ClockStop and type(ClockStop)=="number" then + Tstop=Tnow+ClockStop + elseif ClockStop and type(ClockStop)=="string" then + Tstop=UTILS.ClockToSeconds(ClockStop) + end + + self.Tstart=Tstart + self.Tstop=Tstop + + if Tstop then + self.duration=self.Tstop-self.Tstart + end + + return self +end + +--- Get currrently active phase. +-- @param #OPERATION self +-- @return #OPERATION.Phase Current phase or `nil` if no current phase is active. +function OPERATION:GetPhaseActive() + return self.phase +end + +--- Get next phase. +-- @param #OPERATION self +-- @return #OPERATION.Phase Next phase or `nil` if no next phase exists. +function OPERATION:GetPhaseNext() + + for _,_phase in pairs(self.phases or {}) do + local phase=_phase --#OPERATION.Phase + + if not phase.isOver then + -- Return first phase that is not over. + return phase + end + + end + + return nil +end + +--- Count phases. +-- @param #OPERATION self +-- @return #number Number of phases +function OPERATION:CountPhases() + + local N=0 + for phasename, phase in pairs(self.phases) do + N=N+1 + end + + return N +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Status Update +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "Start" event. +-- @param #OPERATION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPERATION:onafterStart(From, Event, To) + + -- Get + local Phase=self:GetPhaseNext() + + if Phase then + self:PhaseChange(Phase) + end + +end + + +--- On after "StatusUpdate" event. +-- @param #OPERATION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPERATION:onafterStatusUpdate(From, Event, To) + + -- Current abs. mission time. + local Tnow=timer.getAbsTime() + + -- Current FSM state. + local fsmstate=self:GetState() + + -- Current phase. + local currphase=self:GetPhaseActive() + local phasename=currphase and currphase.name or "None" + local Nphase=self:CountPhases() + + -- General info. + local text=string.format("State=%s: Phase=%s, Phases=%d", fsmstate, phasename, Nphase) + self:I(self.lid..text) + + -- Info on phases. + local text="Phases:" + for i,_phase in pairs(self.phases) do + local phase=_phase --#OPERATION.Phase + text=text..string.format("\n[%d] %s", i, phase.name) + end + if text=="Phases:" then text=text.." None" end + + -- Next status update. + self:__StatusUpdate(-30) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "ChangePhase" event. +-- @param #OPERATION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPERATION.Phase Phase The new phase. +function OPERATION:onafterChangePhase(From, Event, To, Phase) + + -- Debug message. + self:T(self.lid..string.format("Changed to phase: %s", Phase.name)) + + -- Set currently active phase. + self.phase=Phase + +end + +--- On after "Over" event. +-- @param #OPERATION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPERATION.Phase Phase The new phase. +function OPERATION:onafterOver(From, Event, To) + + -- Debug message. + self:T(self.lid..string.format("Operation is over!")) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check phases. +-- @param #OPERATION self +function OPERATION:_CheckPhases() + + -- Currently active phase. + local phase=self:GetPhaseActive() + + -- Check if active phase is over. + if phase then + phase.isOver=phase.conditionOver:Evaluate() + end + + -- If no current phase or current phase is over, get next phase. + if phase==nil or (phase and phase.isOver) then + + -- Get next phase. + local Phase=self:GetPhaseNext() + + if Phase then + + -- Change phase to next one. + self:PhaseChange(Phase) + + else + + -- No further phases defined ==> Operation is over. + self:Over() + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index f76cf8f89..1fe3c579d 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -11607,7 +11607,7 @@ function OPSGROUP:SetDefaultCallsign(CallsignName, CallsignNumber) self.callsignDefault.NumberGroup=CallsignNumber or 1 self.callsignDefault.NameSquad=UTILS.GetCallsignName(self.callsign.NumberSquad) - self:I(self.lid..string.format("Default callsign=%s", self.callsignDefault.NameSquad)) + --self:I(self.lid..string.format("Default callsign=%s", self.callsignDefault.NameSquad)) return self end diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index 0523f12be..930f2cb06 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -30,6 +30,7 @@ Core/Timer.lua Core/Goal.lua Core/Spot.lua Core/TextAndSound.lua +Core/Condition.lua Wrapper/Object.lua Wrapper/Identifiable.lua @@ -96,6 +97,7 @@ Ops/Chief.lua Ops/CSAR.lua Ops/CTLD.lua Ops/Awacs.lua +Ops/Operation.lua Ops/FlightControl.lua AI/AI_Balancer.lua From 9b3f2ae3c73bd89d09022d0619169e6eb08651e6 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 31 May 2022 22:54:37 +0200 Subject: [PATCH 11/20] OPS --- Moose Development/Moose/Core/Condition.lua | 24 +- Moose Development/Moose/Ops/Auftrag.lua | 66 +++- Moose Development/Moose/Ops/Chief.lua | 42 +-- Moose Development/Moose/Ops/Cohort.lua | 17 +- Moose Development/Moose/Ops/Commander.lua | 210 +++++++---- Moose Development/Moose/Ops/FlightControl.lua | 94 +++-- Moose Development/Moose/Ops/FlightGroup.lua | 5 +- Moose Development/Moose/Ops/Legion.lua | 2 +- Moose Development/Moose/Ops/Operation.lua | 326 ++++++++++++++++-- Moose Development/Moose/Ops/OpsGroup.lua | 3 + Moose Development/Moose/Ops/Target.lua | 1 + 11 files changed, 580 insertions(+), 210 deletions(-) diff --git a/Moose Development/Moose/Core/Condition.lua b/Moose Development/Moose/Core/Condition.lua index d64b8c727..7c25f1948 100644 --- a/Moose Development/Moose/Core/Condition.lua +++ b/Moose Development/Moose/Core/Condition.lua @@ -7,10 +7,17 @@ -- -- === -- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20Operation). +-- +-- === +-- -- ### Author: **funkyfranky** +-- +-- === -- @module Core.Condition --- @image Core_Condition.png - +-- @image Core_Conditon.png --- CONDITON class. -- @type CONDITION @@ -48,7 +55,7 @@ CONDITION = { --- CONDITION class version. -- @field #string version -CONDITION.version="0.0.1" +CONDITION.version="0.1.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -157,6 +164,15 @@ end -- @return #boolean Result of condition functions. function CONDITION:Evaluate(AnyTrue) + -- Check if at least one function was given. + if #self.functionsAll + #self.functionsAny + #self.functionsAll == 0 then + if self.negateResult then + return true + else + return false + end + end + -- Any condition for gen. local evalAny=self.isAny if AnyTrue~=nil then @@ -185,7 +201,7 @@ function CONDITION:Evaluate(AnyTrue) end -- Debug message. - self:I(self.lid..string.format("Evaluate: isGen=%s, isAny=%s, isAll=%s (negate=%s) ==> result=%s", tostring(isGen), tostring(isAny), tostring(isAll), tostring(self.negateResult), tostring(result))) + self:T(self.lid..string.format("Evaluate: isGen=%s, isAny=%s, isAll=%s (negate=%s) ==> result=%s", tostring(isGen), tostring(isAny), tostring(isAll), tostring(self.negateResult), tostring(result))) return result end diff --git a/Moose Development/Moose/Ops/Auftrag.lua b/Moose Development/Moose/Ops/Auftrag.lua index f03539d27..e9ae00128 100644 --- a/Moose Development/Moose/Ops/Auftrag.lua +++ b/Moose Development/Moose/Ops/Auftrag.lua @@ -72,6 +72,8 @@ -- -- @field Ops.Target#TARGET engageTarget Target data to engage. -- +-- @field Ops.Operation#OPERATION operation Operation this mission is part of. +-- -- @field #boolean teleport Groups are teleported to the mission ingress waypoint. -- -- @field Core.Zone#ZONE_RADIUS engageZone *Circular* engagement zone. @@ -692,6 +694,9 @@ function AUFTRAG:New(Type) self.Ncasualties=0 self.Nkills=0 self.Nelements=0 + self.Ngroups=0 + self.Nassigned=nil + self.Ndead=0 -- FMS start state is PLANNED. self:SetStartState(self.status) @@ -3340,9 +3345,33 @@ end --- Check if mission is EXECUTING. The first OPSGROUP has reached the mission execution waypoint and is not executing the mission task. -- @param #AUFTRAG self +-- @param #boolean AllGroups (Optional) Check that all groups are currently executing the mission. -- @return #boolean If true, mission is currently executing. -function AUFTRAG:IsExecuting() - return self.status==AUFTRAG.Status.EXECUTING +function AUFTRAG:IsExecuting(AllGroups) + + local isExecuting=self.status==AUFTRAG.Status.EXECUTING + + if AllGroups and isExecuting then + + -- Number of groups executing. + local n=self:CountOpsGroupsInStatus(AUFTRAG.GroupStatus.EXECUTING) + + local N + if self.Nassigned then + N=self.Nassigned-self.Ndead + else + N=self:CountOpsGroups() + end + + if n==N then + return true + else + return false + end + + end + + return isExecuting end --- Check if mission was cancelled. @@ -4301,7 +4330,8 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param Ops.OpsGroup#OPSGROUP OpsGroup The ops group that is dead now. +-- @param Ops.OpsGroup#OPSGROUP OpsGroup The ops group to which the element belongs. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The element that got destroyed. function AUFTRAG:onafterElementDestroyed(From, Event, To, OpsGroup, Element) -- Increase number of own casualties. self.Ncasualties=self.Ncasualties+1 @@ -4319,6 +4349,9 @@ function AUFTRAG:onafterGroupDead(From, Event, To, OpsGroup) if asset then self:AssetDead(asset) end + + -- Number of dead groups. + self.Ndead=self.Ndead+1 end @@ -4677,6 +4710,9 @@ function AUFTRAG:onafterRepeat(From, Event, To) -- Reset casualties and units assigned. self.Ncasualties=0 self.Nelements=0 + self.Ngroups=0 + self.Nassigned=nil + self.Ndead=0 -- Update DCS mission task. Could be that the initial task (e.g. for bombing) was destroyed. Then we need to update the coordinate. self.DCStask=self:GetDCSMissionTask() @@ -4949,12 +4985,18 @@ function AUFTRAG:AddAsset(Asset) -- Add to table. self.assets=self.assets or {} + + -- Add to table. table.insert(self.assets, Asset) + + self.Nassigned=self.Nassigned or 0 + + self.Nassigned=self.Nassigned+1 return self end ---- Add asset to mission. +--- Add assets to mission. -- @param #AUFTRAG self -- @param #table Assets List of assets. -- @return #AUFTRAG self @@ -5019,6 +5061,22 @@ function AUFTRAG:CountOpsGroups() return N end +--- Count OPS groups in a certain status. +-- @param #AUFTRAG self +-- @param #string Status Status of group, e.g. `AUFTRAG.GroupStatus.EXECUTING`. +-- @return #number Number of alive OPS groups. +function AUFTRAG:CountOpsGroupsInStatus(Status) + local N=0 + for _,_groupdata in pairs(self.groupdata) do + local groupdata=_groupdata --#AUFTRAG.GroupData + if groupdata and groupdata.status==Status then + N=N+1 + end + end + return N +end + + --- Get coordinate of target. First unit/group of the set is used. -- @param #AUFTRAG self diff --git a/Moose Development/Moose/Ops/Chief.lua b/Moose Development/Moose/Ops/Chief.lua index 006fad90d..34a188fe4 100644 --- a/Moose Development/Moose/Ops/Chief.lua +++ b/Moose Development/Moose/Ops/Chief.lua @@ -2128,10 +2128,8 @@ function CHIEF:CheckTargetQueue() -- Add asset to mission. if mission then - for _,_asset in pairs(assets) do - local asset=_asset - mission:AddAsset(asset) - end + + mission:_AddAssets(assets) Legions=legions -- We got what we wanted ==> leave loop. @@ -2663,23 +2661,7 @@ end function CHIEF:RecruitAssetsForTarget(Target, MissionType, NassetsMin, NassetsMax) -- Cohorts. - local Cohorts={} - for _,_legion in pairs(self.commander.legions) do - local legion=_legion --Ops.Legion#LEGION - - -- Check that runway is operational.d - local Runway=legion:IsAirwing() and legion:IsRunwayOperational() or true - - if legion:IsRunning() and Runway then - - -- Loops over cohorts. - for _,_cohort in pairs(legion.cohorts) do - local cohort=_cohort --Ops.Cohort#COHORT - table.insert(Cohorts, cohort) - end - - end - end + local Cohorts=self.commander:_GetCohorts() -- Target position. local TargetVec2=Target:GetVec2() @@ -2699,23 +2681,7 @@ end function CHIEF:RecruitAssetsForZone(StratZone, Resource) -- Cohorts. - local Cohorts={} - for _,_legion in pairs(self.commander.legions) do - local legion=_legion --Ops.Legion#LEGION - - -- Check that runway is operational. - local Runway=legion:IsAirwing() and legion:IsRunwayOperational() or true - - if legion:IsRunning() and Runway then - - -- Loops over cohorts. - for _,_cohort in pairs(legion.cohorts) do - local cohort=_cohort --Ops.Cohort#COHORT - table.insert(Cohorts, cohort) - end - - end - end + local Cohorts=self.commander:_GetCohorts() -- Shortcuts. local MissionType=Resource.MissionType diff --git a/Moose Development/Moose/Ops/Cohort.lua b/Moose Development/Moose/Ops/Cohort.lua index 4ffcf6c01..be05e288a 100644 --- a/Moose Development/Moose/Ops/Cohort.lua +++ b/Moose Development/Moose/Ops/Cohort.lua @@ -242,8 +242,8 @@ function COHORT:New(TemplateGroupName, Ngroups, CohortName) -- @param #number delay Delay in seconds. --- On after "Pause" event. - -- @function [parent=#AUFTRAG] OnAfterPause - -- @param #AUFTRAG self + -- @function [parent=#COHORT] OnAfterPause + -- @param #COHORT self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. @@ -259,8 +259,8 @@ function COHORT:New(TemplateGroupName, Ngroups, CohortName) -- @param #number delay Delay in seconds. --- On after "Unpause" event. - -- @function [parent=#AUFTRAG] OnAfterUnpause - -- @param #AUFTRAG self + -- @function [parent=#COHORT] OnAfterUnpause + -- @param #COHORT self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. @@ -276,8 +276,8 @@ function COHORT:New(TemplateGroupName, Ngroups, CohortName) -- @param #number delay Delay in seconds. --- On after "Relocate" event. - -- @function [parent=#AUFTRAG] OnAfterRelocate - -- @param #AUFTRAG self + -- @function [parent=#COHORT] OnAfterRelocate + -- @param #COHORT self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. @@ -293,8 +293,8 @@ function COHORT:New(TemplateGroupName, Ngroups, CohortName) -- @param #number delay Delay in seconds. --- On after "Relocated" event. - -- @function [parent=#AUFTRAG] OnAfterRelocated - -- @param #AUFTRAG self + -- @function [parent=#COHORT] OnAfterRelocated + -- @param #COHORT self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. @@ -1004,7 +1004,6 @@ function COHORT:GetOpsGroups(MissionTypes, Attributes) if MissionTypes==nil or AUFTRAG.CheckMissionCapability(MissionTypes, self.missiontypes) then if Attributes==nil or self:CheckAttribute(Attributes) then if asset.flightgroup and asset.flightgroup:IsAlive() then - --set:AddObject(asset.flightgroup) set:AddGroup(asset.flightgroup) end end diff --git a/Moose Development/Moose/Ops/Commander.lua b/Moose Development/Moose/Ops/Commander.lua index 3c4c18503..3b0e10a60 100644 --- a/Moose Development/Moose/Ops/Commander.lua +++ b/Moose Development/Moose/Ops/Commander.lua @@ -531,6 +531,20 @@ function COMMANDER:AddTarget(Target) return self end +--- Add operation. +-- @param #COMMANDER self +-- @param Ops.Operation#OPERATION Operation The operation to be added. +-- @return #COMMANDER self +function COMMANDER:AddOperation(Operation) + + -- TODO: Check that is not already added. + + -- Add operation to table. + table.insert(self.opsqueue, Operation) + + return self +end + --- Check if a TARGET is already in the queue. -- @param #COMMANDER self -- @param Ops.Target#TARGET Target Target object to be added. @@ -1236,9 +1250,12 @@ function COMMANDER:CheckOpsQueue() -- Loop over operations. for _,_ops in pairs(self.opsqueue) do - local operation=_ops --Ops.Operation#OPRATION + local operation=_ops --Ops.Operation#OPERATION - --TODO: What? + if operation:IsRunning() then + + + end end @@ -1408,10 +1425,7 @@ function COMMANDER:CheckMissionQueue() if recruited then -- Add asset to mission. - for _,_asset in pairs(assets) do - local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem - mission:AddAsset(asset) - end + mission:_AddAssets(assets) -- Recruit asset for escorting recruited mission assets. local EscortAvail=self:RecruitAssetsForEscort(mission, assets) @@ -1467,6 +1481,121 @@ function COMMANDER:CheckMissionQueue() end +--- Get cohorts. +-- @param #COMMANDER self +-- @param #table Legions Special legions. +-- @param #table Cohorts Special cohorts. +-- @param Ops.Operation#OPERATION Operation Operation. +-- @return #table Cohorts. +function COMMANDER:_GetCohorts(Legions, Cohorts, Operation) + + --- Function that check if a legion or cohort is part of an operation. + local function CheckOperation(LegionOrCohort) + -- No operations ==> no problem! + if #self.opsqueue==0 then + return true + end + + -- Cohort is not dedicated to a running(!) operation. We assume so. + local isAvail=true + + -- Only available... + if Operation then + isAvail=false + end + + for _,_operation in pairs(self.opsqueue) do + local operation=_operation --Ops.Operation#OPERATION + + -- Legion is assigned to this operation. + local isOps=operation:IsAssignedCohortOrLegion(LegionOrCohort) + + if isOps and operation:IsRunning() then + + -- Is dedicated. + isAvail=false + + if Operation==nil then + -- No Operation given and this is dedicated to at least one operation. + return false + else + if Operation.uid==operation.uid then + -- Operation given and is part of it. + return true + end + end + end + end + + return isAvail + end + + -- Chosen cohorts. + local cohorts={} + + -- Check if there are any special legions and/or cohorts. + if (Legions and #Legions>0) or (Cohorts and #Cohorts>0) then + + -- Add cohorts of special legions. + for _,_legion in pairs(Legions or {}) do + local legion=_legion --Ops.Legion#LEGION + + -- Check that runway is operational. + local Runway=legion:IsAirwing() and legion:IsRunwayOperational() or true + + -- Legion has to be running. + if legion:IsRunning() and Runway then + + -- Add cohorts of legion. + for _,_cohort in pairs(legion.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + if CheckOperation(cohort.legion) or CheckOperation(cohort) then + table.insert(cohorts, cohort) + end + end + + end + end + + -- Add special cohorts. + for _,_cohort in pairs(Cohorts or {}) do + local cohort=_cohort --Ops.Cohort#COHORT + + if CheckOperation(cohort) then + table.insert(cohorts, cohort) + end + end + + else + + -- No special mission legions/cohorts found ==> take own legions. + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + + -- Check that runway is operational. + local Runway=legion:IsAirwing() and legion:IsRunwayOperational() or true + + -- Legion has to be running. + if legion:IsRunning() and Runway then + + -- Add cohorts of legion. + for _,_cohort in pairs(legion.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + if CheckOperation(cohort.legion) or CheckOperation(cohort) then + table.insert(cohorts, cohort) + end + end + + end + end + + end + + return cohorts +end + --- Recruit assets for a given mission. -- @param #COMMANDER self -- @param Ops.Auftrag#AUFTRAG Mission The mission. @@ -1479,29 +1608,10 @@ function COMMANDER:RecruitAssetsForMission(Mission) self:T2(self.lid..string.format("Recruiting assets for mission \"%s\" [%s]", Mission:GetName(), Mission:GetType())) -- Cohorts. - local Cohorts={} - for _,_legion in pairs(Mission.specialLegions or {}) do - local legion=_legion --Ops.Legion#LEGION - for _,_cohort in pairs(legion.cohorts) do - local cohort=_cohort --Ops.Cohort#COHORT - table.insert(Cohorts, cohort) - end - end - for _,_cohort in pairs(Mission.specialCohorts or {}) do - local cohort=_cohort --Ops.Cohort#COHORT - table.insert(Cohorts, cohort) - end + local Cohorts=self:_GetCohorts(Mission.specialLegions, Mission.specialCohorts, Mission.operation) - -- No special mission legions/cohorts found ==> take own legions. - if #Cohorts==0 then - for _,_legion in pairs(self.legions) do - local legion=_legion --Ops.Legion#LEGION - for _,_cohort in pairs(legion.cohorts) do - local cohort=_cohort --Ops.Cohort#COHORT - table.insert(Cohorts, cohort) - end - end - end + -- Debug info. + self:T(self.lid..string.format("Found %d cohort candidates for mission", #Cohorts)) -- Number of required assets. local NreqMin, NreqMax=Mission:GetRequiredAssets() @@ -1530,30 +1640,7 @@ function COMMANDER:RecruitAssetsForEscort(Mission, Assets) if Mission.NescortMin and Mission.NescortMax and (Mission.NescortMin>0 or Mission.NescortMax>0) then -- Cohorts. - local Cohorts={} - for _,_legion in pairs(Mission.escortLegions or {}) do - local legion=_legion --Ops.Legion#LEGION - for _,_cohort in pairs(legion.cohorts) do - local cohort=_cohort --Ops.Cohort#COHORT - table.insert(Cohorts, cohort) - end - end - for _,_cohort in pairs(Mission.escortCohorts or {}) do - local cohort=_cohort --Ops.Cohort#COHORT - table.insert(Cohorts, cohort) - end - - -- No special escort legions/cohorts found ==> take own legions. - if #Cohorts==0 then - for _,_legion in pairs(self.legions) do - local legion=_legion --Ops.Legion#LEGION - for _,_cohort in pairs(legion.cohorts) do - local cohort=_cohort --Ops.Cohort#COHORT - table.insert(Cohorts, cohort) - end - end - end - + local Cohorts=self:_GetCohorts(Mission.escortLegions, Mission.escortCohorts, Mission.operation) -- Call LEGION function but provide COMMANDER as self. local assigned=LEGION.AssignAssetsForEscort(self, Cohorts, Assets, Mission.NescortMin, Mission.NescortMax, Mission.escortTargetTypes, Mission.escortEngageRange) @@ -1683,24 +1770,7 @@ function COMMANDER:RecruitAssetsForTransport(Transport, CargoWeight, TotalWeight end -- Cohorts. - local Cohorts={} - for _,_legion in pairs(self.legions) do - local legion=_legion --Ops.Legion#LEGION - - -- Check that runway is operational. - local Runway=legion:IsAirwing() and legion:IsRunwayOperational() or true - - if legion:IsRunning() and Runway then - - -- Loops over cohorts. - for _,_cohort in pairs(legion.cohorts) do - local cohort=_cohort --Ops.Cohort#COHORT - table.insert(Cohorts, cohort) - end - - end - end - + local Cohorts=self:_GetCohorts() -- Target is the deploy zone. local TargetVec2=Transport:GetDeployZone():GetVec2() diff --git a/Moose Development/Moose/Ops/FlightControl.lua b/Moose Development/Moose/Ops/FlightControl.lua index 039541019..1ae08735e 100644 --- a/Moose Development/Moose/Ops/FlightControl.lua +++ b/Moose Development/Moose/Ops/FlightControl.lua @@ -137,6 +137,7 @@ FLIGHTCONTROL = { --- Flight status. -- @type FLIGHTCONTROL.FlightStatus +-- @field #string UNKNOWN Flight is unknown. -- @field #string INBOUND Flight is inbound. -- @field #string HOLDING Flight is holding. -- @field #string LANDING Flight is landing. @@ -147,6 +148,7 @@ FLIGHTCONTROL = { -- @field #string READYTO Flight is ready for takeoff. -- @field #string TAKEOFF Flight is taking off. FLIGHTCONTROL.FlightStatus={ + UNKNOWN="Unknown", PARKING="Parking", READYTX="Ready To Taxi", TAXIOUT="Taxi To Runway", @@ -1277,39 +1279,6 @@ function FLIGHTCONTROL:_PrintQueue(queue, name) 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:T(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(0.5) - end - - return true, i - end - end - - self:E(self.lid..string.format("WARNING: 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. @@ -2158,15 +2127,6 @@ function FLIGHTCONTROL:_PlayerRequestInbound(groupname) if flight then if flight:IsAirborne() then - - if self:IsControlling(flight) then - -- Nothing to do as this flight has already the right flightcontrol. - else - - -- Set FC controlling this flight. - flight:SetFlightControl(self) - - end -- Call sign. local callsign=flight:GetCallsignName() @@ -2216,10 +2176,7 @@ function FLIGHTCONTROL:_PlayerRequestInbound(groupname) -- Send message. self:TransmissionTower(text, flight, 15) - - -- Create player menu. - --flight:_UpdateMenu() - + else self:E(self.lid..string.format("WARNING: Could not get holding stack for flight %s", flight:GetName())) end @@ -2259,6 +2216,7 @@ function FLIGHTCONTROL:_PlayerVectorInbound(groupname) if flight then + -- Check if inbound, controlled and have a stack. if flight:IsInbound() and self:IsControlling(flight) and flight.stack then -- Call sign. @@ -2279,9 +2237,10 @@ function FLIGHTCONTROL:_PlayerVectorInbound(groupname) -- Heading to holding point. local heading=flightcoord:HeadingTo(flight.stack.pos0) - -- Distance to holding point. + -- Distance to holding point in meters. local distance=flightcoord:Get2DDistance(flight.stack.pos0) + -- Distance in NM. local dist=UTILS.MetersToNM(distance) -- Message text. @@ -2291,6 +2250,10 @@ function FLIGHTCONTROL:_PlayerVectorInbound(groupname) -- Send message. self:TextMessageToFlight(text, flight) + else + -- Send message. + local text="Negative, you must be INBOUND, CONTROLLED by us and have an assigned STACK!" + self:TextMessageToFlight(text, flight) end else self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) @@ -3034,13 +2997,38 @@ end --- Remove flight from all queues. -- @param #FLIGHTCONTROL self --- @param Ops.FlightGroup#FLIGHTGROUP flight The flight to be removed. -function FLIGHTCONTROL:_RemoveFlight(flight) - - flight.flightcontrol=nil - - self:_RemoveFlightFromQueue(self.flights, flight, "flights") +-- @param Ops.FlightGroup#FLIGHTGROUP Flight The flight to be removed. +function FLIGHTCONTROL:_RemoveFlight(Flight) + -- Loop over all flights in group. + for i,_flight in pairs(self.flights) do + local flight=_flight --Ops.FlightGroup#FLIGHTGROUP + + -- Check for name. + if flight.groupname==Flight.groupname then + + -- Debug message. + self:T(self.lid..string.format("Removing flight group %s", flight.groupname)) + + -- Remove table entry. + table.remove(self.flights, i) + + -- Remove myself. + Flight.flightcontrol=nil + + -- Set flight status to unknown. + self:SetFlightStatus(Flight, FLIGHTCONTROL.FlightStatus.UNKNOWN) + + -- Update menu. + if not flight.isAI then + flight:_UpdateMenu(0.5) + end + + end + end + + -- + self:E(self.lid..string.format("WARNING: Could NOT remove flight group %s from %s queue", flight.groupname, queuename)) end --- Get flight from group. diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index 65b8ae44e..8c485d905 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -412,7 +412,7 @@ function FLIGHTGROUP:SetFlightControl(flightcontrol) end -- Update flight's F10 menu. - if self.isAI==false then + if not self.isAI then self:_UpdateMenu(0.5) end @@ -2447,6 +2447,7 @@ function FLIGHTGROUP:onafterRTB(From, Event, To, airbase, SpeedTo, SpeedHold, Sp end + -- Land at airbase. self:_LandAtAirbase(airbase, SpeedTo, SpeedHold, SpeedLand) end @@ -4173,7 +4174,7 @@ function FLIGHTGROUP:_UpdateMenu(delay) -- Message to group. MESSAGE:New("Updating MENU state="..self:GetState(), 5):ToGroup(self.group) - env.info(self.lid.."updating menu state=") + env.info(self.lid.."updating menu state="..self:GetState()) -- Player element. local player=self:GetPlayerElement() diff --git a/Moose Development/Moose/Ops/Legion.lua b/Moose Development/Moose/Ops/Legion.lua index 027aba19e..150384c15 100644 --- a/Moose Development/Moose/Ops/Legion.lua +++ b/Moose Development/Moose/Ops/Legion.lua @@ -1609,7 +1609,7 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #WAREHOUSE.Pendingitem Request Information table of the request. +-- @param Functional.Warehouse#WAREHOUSE.Pendingitem Request Information table of the request. -- @param Core.Set#SET_GROUP CargoGroupSet Set of cargo groups. -- @param Core.Set#SET_GROUP TransportGroupSet Set of transport groups if any. function LEGION:onafterRequestSpawned(From, Event, To, Request, CargoGroupSet, TransportGroupSet) diff --git a/Moose Development/Moose/Ops/Operation.lua b/Moose Development/Moose/Ops/Operation.lua index d1ea5aa83..71bed062a 100644 --- a/Moose Development/Moose/Ops/Operation.lua +++ b/Moose Development/Moose/Ops/Operation.lua @@ -3,6 +3,7 @@ -- ## Main Features: -- -- * Define operation phases +-- * Define conditions when phases are over -- * Dedicate resources to operations -- -- === @@ -31,6 +32,7 @@ -- @field #table phases Phases. -- @field #number counterPhase Running number counting the phases. -- @field #OPERATION.Phase phase Currently active phase (if any). +-- @field #table targets Targets. -- -- @extends Core.Fsm#FSM @@ -51,6 +53,7 @@ OPERATION = { legions = {}, phases = {}, counterPhase = 0, + targets = {}, } --- Global mission counter. @@ -61,7 +64,18 @@ _OPERATIONID=0 -- @field #number uid Unique ID of the phase. -- @field #string name Name of the phase. -- @field Core.Condition#CONDITION conditionOver Conditions when the phase is over. --- @field #boolean isOver If `true`, phase is over. +-- @field #string status Phase status. + +--- Operation phase. +-- @type OPERATION.PhaseStatus +-- @field #string PLANNED Planned. +-- @field #string ACTIVE Active phase. +-- @field #string OVER Phase is over. +OPERATION.PhaseStatus={ + PLANNED="Planned", + ACTIVE="Active", + OVER="Over", +} --- OPERATION class version. -- @field #string version @@ -89,13 +103,15 @@ function OPERATION:New(Name) -- Increase global counter. _OPERATIONID=_OPERATIONID+1 + -- Unique ID of the operation. + self.uid=_OPERATIONID + -- Set Name. self.name=Name or string.format("Operation-%02d", _OPERATIONID) -- Set log ID. self.lid=string.format("%s | ",self.name) - -- FMS start state is PLANNED. self:SetStartState("Planned") @@ -108,7 +124,7 @@ function OPERATION:New(Name) self:AddTransition("Running", "Pause", "Paused") self:AddTransition("Paused", "Unpause", "Running") - self:AddTransition("*", "ChangePhase", "*") + self:AddTransition("*", "PhaseOver", "*") self:AddTransition("*", "PhaseChange", "*") self:AddTransition("*", "Over", "Over") @@ -159,6 +175,43 @@ function OPERATION:New(Name) -- @param #string To To state. -- @param #OPERATION.Phase Phase The new phase. + + --- Triggers the FSM event "PhaseOver". + -- @function [parent=#OPERATION] PhaseOver + -- @param #OPERATION self + -- @param #OPERATION.Phase Phase The phase that is over. + + --- Triggers the FSM event "PhaseOver" after a delay. + -- @function [parent=#OPERATION] __PhaseOver + -- @param #OPERATION self + -- @param #number delay Delay in seconds. + -- @param #OPERATION.Phase Phase The phase that is over. + + --- On after "PhaseOver" event. + -- @function [parent=#OPERATION] OnAfterPhaseOver + -- @param #OPERATION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #OPERATION.Phase Phase The phase that is over. + + + --- Triggers the FSM event "Over". + -- @function [parent=#OPERATION] Over + -- @param #OPERATION self + + --- Triggers the FSM event "Over" after a delay. + -- @function [parent=#OPERATION] __Over + -- @param #OPERATION self + -- @param #number delay Delay in seconds. + + --- On after "Over" event. + -- @function [parent=#OPERATION] OnAfterOver + -- @param #OPERATION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- Init status update. self:__StatusUpdate(-1) @@ -172,9 +225,8 @@ end --- Create a new generic OPERATION object. -- @param #OPERATION self -- @param #string Name Name of the phase. Default "Phase-01" where the last number is a running number. --- @param Core.Condition#CONDITION ConditionOver Condition when the phase is over. -- @return #OPERATION.Phase Phase table object. -function OPERATION:AddPhase(Name, ConditionOver) +function OPERATION:AddPhase(Name) -- Increase phase counter. self.counterPhase=self.counterPhase+1 @@ -182,8 +234,8 @@ function OPERATION:AddPhase(Name, ConditionOver) local phase={} --#OPERATION.Phase phase.uid=self.counterPhase phase.name=Name or string.format("Phase-%02d", self.counterPhase) - phase.conditionOver=ConditionOver or CONDITION:New(Name) - phase.isOver=false + phase.conditionOver=CONDITION:New(Name.." Over") + phase.status=OPERATION.PhaseStatus.PLANNED -- Add phase. table.insert(self.phases, phase) @@ -213,6 +265,7 @@ end -- @return #OPERATION self function OPERATION:AssignCohort(Cohort) + self:T(self.lid..string.format("Assiging Cohort %s to operation", Cohort.name)) self.cohorts[Cohort.name]=Cohort end @@ -227,7 +280,60 @@ function OPERATION:AssignLegion(Legion) end +--- Check if a given legion is assigned to this operation. All cohorts of this legion will be checked. +-- @param #OPERATION self +-- @param Ops.Legion#LEGION Legion The legion to be assigned. +-- @return #boolean If `true`, legion is assigned to this operation. +function OPERATION:IsAssignedLegion(Legion) + local legion=self.legions[Legion.alias] + + if legion then + self:T(self.lid..string.format("Legion %s is assigned to this operation", Legion.alias)) + return true + else + self:T(self.lid..string.format("Legion %s is NOT assigned to this operation", Legion.alias)) + return false + end + +end + +--- Check if a given cohort is assigned to this operation. +-- @param #OPERATION self +-- @param Ops.Cohort#COHORT Cohort The Cohort. +-- @return #boolean If `true`, cohort is assigned to this operation. +function OPERATION:IsAssignedCohort(Cohort) + + local cohort=self.cohorts[Cohort.name] + + if cohort then + self:T(self.lid..string.format("Cohort %s is assigned to this operation", Cohort.name)) + return true + else + self:T(self.lid..string.format("Cohort %s is NOT assigned to this operation", Cohort.name)) + return false + end + + return nil +end + +--- Check if a given cohort or legion is assigned to this operation. +-- @param #OPERATION self +-- @param Wrapper.Object#OBJECT Object The cohort or legion object. +-- @return #boolean If `true`, cohort is assigned to this operation. +function OPERATION:IsAssignedCohortOrLegion(Object) + + local isAssigned=nil + if Object:IsInstanceOf("COHORT") then + isAssigned=self:IsAssignedCohort(Object) + elseif Object:IsInstanceOf("LEGION") then + isAssigned=self:IsAssignedLegion(Object) + else + self:E(self.lid.."ERROR: Unknown Object!") + end + + return isAssigned +end --- Set start and stop time of the operation. -- @param #OPERATION self @@ -265,6 +371,88 @@ function OPERATION:SetTime(ClockStart, ClockStop) return self end +--- Set status of a phase. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase. +-- @param #string Status New status, *e.g.* `OPERATION.PhaseStatus.OVER`. +-- @return #OPERATION self +function OPERATION:SetPhaseStatus(Phase, Status) + if Phase then + self:T(self.lid..string.format("Phase %s status: %s-->%s"), Phase.status, Status) + Phase.status=Status + end + return self +end + +--- Get status of a phase. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase. +-- @return #string Phase status, *e.g.* `OPERATION.PhaseStatus.OVER`. +function OPERATION:GetPhaseStatus(Phase) + return Phase.status +end + +--- Set codition when the given phase is over. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase. +-- @param Core.Condition#CONDITION Condition Condition when the phase is over. +-- @return #OPERATION self +function OPERATION:SetPhaseConditonOver(Phase, Condition) + if Phase then + self:T(self.lid..string.format("Setting phase %s conditon over %s"), Phase.name, Condition and Condition.name or "None") + Phase.conditionOver=Condition + end + return self +end + +--- Add codition function when the given phase is over. Must return a `#boolean`. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase. +-- @param #function Function Function that needs to be `true`before the phase is over. +-- @param ... Condition function arguments if any. +-- @return #OPERATION self +function OPERATION:AddPhaseConditonOverAll(Phase, Function, ...) + if Phase then + Phase.conditionOver:AddFunctionAll(Function, ...) + end + return self +end + +--- Add codition function when the given phase is over. Must return a `#boolean`. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase. +-- @param #function Function Function that needs to be `true`before the phase is over. +-- @param ... Condition function arguments if any. +-- @return #OPERATION self +function OPERATION:AddPhaseConditonOverAny(Phase, Function, ...) + if Phase then + Phase.conditionOver:AddFunctionAny(Function, ...) + end + return self +end + + +--- Get codition when the given phase is over. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase. +-- @return Core.Condition#CONDITION Condition when the phase is over (if any). +function OPERATION:GetPhaseConditonOver(Phase, Condition) + return Phase.conditionOver +end + +--- Get currrently active phase. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase. +-- @param #string Status New status, e.g. `OPERATION.PhaseStatus.OVER`. +-- @return #OPERATION self +function OPERATION:SetPhaseStatus(Phase, Status) + if Phase then + self:T(self.lid..string.format("Phase \"%s\" status: %s-->%s", Phase.name, Phase.status, Status)) + Phase.status=Status + end + return self +end + --- Get currrently active phase. -- @param #OPERATION self -- @return #OPERATION.Phase Current phase or `nil` if no current phase is active. @@ -272,6 +460,20 @@ function OPERATION:GetPhaseActive() return self.phase end +--- Check if a phase is the currently active one. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase to check. +-- @return #boolean If `true`, this phase is currently active. +function OPERATION:IsPhaseActive(Phase) + local phase=self:GetPhaseActive() + if phase and phase.uid==Phase.uid then + return true + else + return false + end + return nil +end + --- Get next phase. -- @param #OPERATION self -- @return #OPERATION.Phase Next phase or `nil` if no next phase exists. @@ -280,7 +482,7 @@ function OPERATION:GetPhaseNext() for _,_phase in pairs(self.phases or {}) do local phase=_phase --#OPERATION.Phase - if not phase.isOver then + if phase.status==OPERATION.PhaseStatus.PLANNED then -- Return first phase that is not over. return phase end @@ -292,17 +494,52 @@ end --- Count phases. -- @param #OPERATION self +-- @param #string Status (Optional) Only count phases in a certain status, e.g. `OPERATION.PhaseStatus.PLANNED`. -- @return #number Number of phases -function OPERATION:CountPhases() +function OPERATION:CountPhases(Status) local N=0 - for phasename, phase in pairs(self.phases) do - N=N+1 + for _,_phase in pairs(self.phases) do + local phase=_phase --#OPERATION.Phase + if Status==nil or Status==phase.status then + N=N+1 + end end return N end +--- Check if operation is in FSM state "Planned". +-- @param #OPERATION self +-- @return #boolean If `true`, operation is "Planned". +function OPERATION:IsPlanned() + local is=self:is("Planned") + return is +end + +--- Check if operation is in FSM state "Running". +-- @param #OPERATION self +-- @return #boolean If `true`, operation is "Running". +function OPERATION:IsRunning() + local is=self:is("Running") + return is +end + +--- Check if operation is in FSM state "Paused". +-- @param #OPERATION self +-- @return #boolean If `true`, operation is "Paused". +function OPERATION:IsPaused() + local is=self:is("Paused") + return is +end + +--- Check if operation is in FSM state "Stopped". +-- @param #OPERATION self +-- @return #boolean If `true`, operation is "Stopped". +function OPERATION:IsStopped() + local is=self:is("Stopped") + return is +end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Status Update @@ -315,12 +552,8 @@ end -- @param #string To To state. function OPERATION:onafterStart(From, Event, To) - -- Get - local Phase=self:GetPhaseNext() - - if Phase then - self:PhaseChange(Phase) - end + -- Debug message. + self:T(self.lid..string.format("Starting Operation!")) end @@ -338,22 +571,34 @@ function OPERATION:onafterStatusUpdate(From, Event, To) -- Current FSM state. local fsmstate=self:GetState() + -- Check phases. + if self:IsRunning() then + self:_CheckPhases() + end + -- Current phase. local currphase=self:GetPhaseActive() - local phasename=currphase and currphase.name or "None" - local Nphase=self:CountPhases() + local phaseName="None" + if currphase then + phaseName=currphase.name + end + local NphaseTot=self:CountPhases() + local NphaseAct=self:CountPhases(OPERATION.PhaseStatus.ACTIVE) + local NphasePla=self:CountPhases(OPERATION.PhaseStatus.PLANNED) + local NphaseOvr=self:CountPhases(OPERATION.PhaseStatus.OVER) -- General info. - local text=string.format("State=%s: Phase=%s, Phases=%d", fsmstate, phasename, Nphase) + local text=string.format("State=%s: Phase=%s, Phases=%d [Active=%d, Planned=%d, Over=%d]", fsmstate, phaseName, NphaseTot, NphaseAct, NphasePla, NphaseOvr) self:I(self.lid..text) -- Info on phases. local text="Phases:" for i,_phase in pairs(self.phases) do local phase=_phase --#OPERATION.Phase - text=text..string.format("\n[%d] %s", i, phase.name) + text=text..string.format("\n[%d] %s: status=%s", i, phase.name, tostring(phase.status)) end if text=="Phases:" then text=text.." None" end + self:I(self.lid..text) -- Next status update. self:__StatusUpdate(-30) @@ -363,20 +608,30 @@ end -- FSM Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- On after "ChangePhase" event. +--- On after "PhaseChange" event. -- @param #OPERATION self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -- @param #OPERATION.Phase Phase The new phase. -function OPERATION:onafterChangePhase(From, Event, To, Phase) +function OPERATION:onafterPhaseChange(From, Event, To, Phase) + + -- Previous phase (if any). + local oldphase="None" + if self.phase then + self:SetPhaseStatus(self.phase, OPERATION.PhaseStatus.OVER) + oldphase=self.phase.name + end -- Debug message. - self:T(self.lid..string.format("Changed to phase: %s", Phase.name)) + self:T(self.lid..string.format("Phase change: %s --> %s", oldphase, Phase.name)) -- Set currently active phase. self.phase=Phase + -- Phase is active. + self:SetPhaseStatus(Phase, OPERATION.PhaseStatus.ACTIVE) + end --- On after "Over" event. @@ -389,7 +644,16 @@ function OPERATION:onafterOver(From, Event, To) -- Debug message. self:T(self.lid..string.format("Operation is over!")) - + + -- No active phase. + self.phase=nil + + -- Set all phases to OVER. + for _,_phase in pairs(self.phases) do + local phase=_phase --#OPERATION.Phase + self:SetPhaseStatus(phase, OPERATION.PhaseStatus.OVER) + end + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -403,13 +667,16 @@ function OPERATION:_CheckPhases() -- Currently active phase. local phase=self:GetPhaseActive() - -- Check if active phase is over. - if phase then - phase.isOver=phase.conditionOver:Evaluate() + -- Check if active phase is over if conditon over is defined. + if phase and phase.conditionOver then + local isOver=phase.conditionOver:Evaluate() + if isOver then + self:SetPhaseStatus(phase, OPERATION.PhaseStatus.OVER) + end end -- If no current phase or current phase is over, get next phase. - if phase==nil or (phase and phase.isOver) then + if phase==nil or phase.status==OPERATION.PhaseStatus.OVER then -- Get next phase. local Phase=self:GetPhaseNext() @@ -423,6 +690,7 @@ function OPERATION:_CheckPhases() -- No further phases defined ==> Operation is over. self:Over() + end end diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index 1fe3c579d..30ee7f641 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -4646,6 +4646,9 @@ function OPSGROUP:AddMission(Mission) -- Add elements. Mission.Nelements=Mission.Nelements+#self.elements + + -- Increase number of groups. + Mission.Ngroups=Mission.Ngroups+1 -- Add mission to queue. table.insert(self.missionqueue, Mission) diff --git a/Moose Development/Moose/Ops/Target.lua b/Moose Development/Moose/Ops/Target.lua index f74268b9f..8129059c4 100644 --- a/Moose Development/Moose/Ops/Target.lua +++ b/Moose Development/Moose/Ops/Target.lua @@ -38,6 +38,7 @@ -- @field #boolean isDestroyed If true, target objects were destroyed. -- @field #table resources Resource list. -- @field #table conditionStart Start condition functions. +-- @field Ops.Operation#OPERATION operation Operation this target is part of. -- @extends Core.Fsm#FSM --- **It is far more important to be able to hit the target than it is to haggle over who makes a weapon or who pulls a trigger** -- Dwight D Eisenhower From a53595a0557410697a03f85530451f2b29534368 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 6 Jun 2022 22:05:57 +0200 Subject: [PATCH 12/20] OPS Enhanced OPERATION and FLIGHTCONTROL features. --- Moose Development/Moose/Core/Set.lua | 2 +- Moose Development/Moose/Functional/Range.lua | 20 ++ Moose Development/Moose/Ops/ATIS.lua | 22 +- Moose Development/Moose/Ops/ArmyGroup.lua | 19 +- Moose Development/Moose/Ops/Auftrag.lua | 46 ++++ Moose Development/Moose/Ops/Chief.lua | 25 +- Moose Development/Moose/Ops/Cohort.lua | 4 +- Moose Development/Moose/Ops/Commander.lua | 44 ++++ Moose Development/Moose/Ops/FlightControl.lua | 226 ++++++++++++++---- Moose Development/Moose/Ops/FlightGroup.lua | 7 +- Moose Development/Moose/Ops/Legion.lua | 5 +- Moose Development/Moose/Ops/Operation.lua | 92 ++++++- Moose Development/Moose/Ops/OpsGroup.lua | 35 ++- Moose Development/Moose/Ops/Platoon.lua | 2 + 14 files changed, 443 insertions(+), 106 deletions(-) diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index 1d2fe6f30..de5b5b927 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -6311,7 +6311,7 @@ do -- SET_OPSGROUP --- Creates a new SET_OPSGROUP object, building a set of groups belonging to a coalitions, categories, countries, types or with defined prefix names. -- @param #SET_OPSGROUP self - -- @return #SET_OPSGROUP + -- @return #SET_OPSGROUP self function SET_OPSGROUP:New() -- Inherit SET_BASE. diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 6b4a3bc14..a7b61dc8f 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -2667,6 +2667,26 @@ function RANGE:_DisplayRangeInfo( _unitname ) text = text .. string.format( "Max strafing alt AGL: %s\n", tstrafemaxalt ) text = text .. string.format( "# of strafe targets: %d\n", self.nstrafetargets ) text = text .. string.format( "# of bomb targets: %d\n", self.nbombtargets ) + if self.instructor then + local alive = "N/A" + if self.instructorrelayname then + local relay = UNIT:FindByName( self.instructorrelayname ) + if relay then + alive = tostring( relay:IsAlive() ) + end + end + text = text .. string.format( "Instructor %.3f MHz (Relay=%s)\n", self.instructorfreq, alive ) + end + if self.rangecontrol then + local alive = "N/A" + if self.rangecontrolrelayname then + local relay = UNIT:FindByName( self.rangecontrolrelayname ) + if relay then + alive = tostring( relay:IsAlive() ) + end + end + text = text .. string.format( "Control %.3f MHz (Relay=%s)\n", self.rangecontrolfreq, alive ) + end text = text .. texthit text = text .. textbomb text = text .. textdelay diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index 113359cf0..848fe265b 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -1137,15 +1137,19 @@ end -- @param #number Port SRS port. Default 5002. -- @return #ATIS self function ATIS:SetSRS(PathToSRS, Gender, Culture, Voice, Port) - self.useSRS=true - self.msrs=MSRS:New(PathToSRS, self.frequency, self.modulation) - self.msrs:SetGender(Gender) - self.msrs:SetCulture(Culture) - self.msrs:SetVoice(Voice) - self.msrs:SetPort(Port) - self.msrs:SetCoalition(self:GetCoalition()) - if self.dTQueueCheck<=10 then - self:SetQueueUpdateTime(90) + if PathToSRS then + self.useSRS=true + self.msrs=MSRS:New(PathToSRS, self.frequency, self.modulation) + self.msrs:SetGender(Gender) + self.msrs:SetCulture(Culture) + self.msrs:SetVoice(Voice) + self.msrs:SetPort(Port) + self.msrs:SetCoalition(self:GetCoalition()) + if self.dTQueueCheck<=10 then + self:SetQueueUpdateTime(90) + end + else + self:E(self.lid..string.format("ERROR: No SRS path specified!")) end return self end diff --git a/Moose Development/Moose/Ops/ArmyGroup.lua b/Moose Development/Moose/Ops/ArmyGroup.lua index c40948931..e8a659885 100644 --- a/Moose Development/Moose/Ops/ArmyGroup.lua +++ b/Moose Development/Moose/Ops/ArmyGroup.lua @@ -137,7 +137,7 @@ function ARMYGROUP:New(group) self:AddTransition("*", "Rearm", "Rearm") -- Group is send to a coordinate and waits until ammo is refilled. self:AddTransition("Rearm", "Rearming", "Rearming") -- Group has arrived at the rearming coodinate and is waiting to be fully rearmed. - self:AddTransition("Rearming", "Rearmed", "Cruising") -- Group was rearmed. + self:AddTransition("*", "Rearmed", "Cruising") -- Group was rearmed. ------------------------ --- Pseudo Functions --- @@ -1381,9 +1381,22 @@ end -- @param #string To To state. function ARMYGROUP:onafterRearmed(From, Event, To) self:T(self.lid.."Group rearmed") + + -- Get Current mission. + local mission=self:GetMissionCurrent() + + -- Check if this is a rearming mission. + if mission and mission.type==AUFTRAG.Type.REARMING then + -- Rearmed ==> Mission Done! This also checks if the group is done. + self:MissionDone(mission) + + else + + -- Check group done. + self:_CheckGroupDone(1) + + end - -- Check group done. - self:_CheckGroupDone(1) end --- On before "RTZ" event. diff --git a/Moose Development/Moose/Ops/Auftrag.lua b/Moose Development/Moose/Ops/Auftrag.lua index e9ae00128..6a70170ba 100644 --- a/Moose Development/Moose/Ops/Auftrag.lua +++ b/Moose Development/Moose/Ops/Auftrag.lua @@ -418,6 +418,7 @@ _AUFTRAGSNR=0 -- @field #string AIRDEFENSE Air defense. -- @field #string EWR Early Warning Radar. -- @field #string RECOVERYTANKER Recovery tanker. +-- @filed #string REARMING Rearming mission. -- @field #string NOTHING Nothing. AUFTRAG.Type={ ANTISHIP="Anti Ship", @@ -459,6 +460,7 @@ AUFTRAG.Type={ AIRDEFENSE="Air Defence", EWR="Early Warning Radar", RECOVERYTANKER="Recovery Tanker", + REARMING="Rearming", NOTHING="Nothing", } @@ -480,6 +482,7 @@ AUFTRAG.Type={ -- @field #string AIRDEFENSE Air defense. -- @field #string EWR Early Warning Radar. -- @field #string RECOVERYTANKER Recovery tanker. +-- @field #string REARMING Rearming. -- @field #string NOTHING Nothing. AUFTRAG.SpecialTask={ FORMATION="Formation", @@ -499,6 +502,7 @@ AUFTRAG.SpecialTask={ AIRDEFENSE="Air Defense", EWR="Early Warning Radar", RECOVERYTANKER="Recovery Tanker", + REARMING="Rearming", NOTHING="Nothing", } @@ -2009,6 +2013,30 @@ function AUFTRAG:NewFUELSUPPLY(Zone) return mission end +--- **[GROUND]** Create a REARMING mission. +-- @param #AUFTRAG self +-- @param Core.Zone#ZONE Zone The zone, where units go and look for ammo supply. +-- @return #AUFTRAG self +function AUFTRAG:NewREARMING(Zone) + + local mission=AUFTRAG:New(AUFTRAG.Type.REARMING) + + mission:_TargetFromObject(Zone) + + mission.optionROE=ENUMS.ROE.WeaponHold + mission.optionAlarm=ENUMS.AlarmState.Auto + + mission.missionFraction=1.0 + + mission.missionWaypointRadius=0 + + mission.categories={AUFTRAG.Category.GROUND} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + --- **[AIR]** Create an ALERT 5 mission. Aircraft will be spawned uncontrolled and wait for an assignment. You must specify **one** mission type which is performed. -- This determines the payload and the DCS mission task which are used when the aircraft is spawned. @@ -5710,6 +5738,24 @@ function AUFTRAG:GetDCSMissionTask() table.insert(DCStasks, DCStask) + elseif self.type==AUFTRAG.Type.AMMOSUPPLY then + + ---------------------- + -- REARMING Mission -- + ---------------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.REARMING + + -- We create a "fake" DCS task and pass the parameters to the OPSGROUP. + local param={} + param.zone=self:GetObjective() + + DCStask.params=param + + table.insert(DCStasks, DCStask) + elseif self.type==AUFTRAG.Type.ALERT5 then --------------------- diff --git a/Moose Development/Moose/Ops/Chief.lua b/Moose Development/Moose/Ops/Chief.lua index 34a188fe4..9471147ec 100644 --- a/Moose Development/Moose/Ops/Chief.lua +++ b/Moose Development/Moose/Ops/Chief.lua @@ -2117,7 +2117,7 @@ function CHIEF:CheckTargetQueue() self:T2(self.lid..string.format("Recruiting assets for mission type %s [performance=%d] of target %s", mp.MissionType, mp.Performance, target:GetName())) -- Recruit assets. - local recruited, assets, legions=self:RecruitAssetsForTarget(target, mp.MissionType, NassetsMin, NassetsMax) + local recruited, assets, legions=self.commander:RecruitAssetsForTarget(target, mp.MissionType, NassetsMin, NassetsMax) if recruited then @@ -2649,29 +2649,6 @@ function CHIEF:_GetMissionTypeForGroupAttribute(Attribute) return missionperf end ---- Recruit assets for a given TARGET. --- @param #CHIEF self --- @param Ops.Target#TARGET Target The target. --- @param #string MissionType Mission Type. --- @param #number NassetsMin Min number of required assets. --- @param #number NassetsMax Max number of required assets. --- @return #boolean If `true` enough assets could be recruited. --- @return #table Assets that have been recruited from all legions. --- @return #table Legions that have recruited assets. -function CHIEF:RecruitAssetsForTarget(Target, MissionType, NassetsMin, NassetsMax) - - -- Cohorts. - local Cohorts=self.commander:_GetCohorts() - - -- Target position. - local TargetVec2=Target:GetVec2() - - -- Recruite assets. - local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, MissionType, nil, NassetsMin, NassetsMax, TargetVec2) - - - return recruited, assets, legions -end --- Recruit assets for a given OPS zone. -- @param #CHIEF self diff --git a/Moose Development/Moose/Ops/Cohort.lua b/Moose Development/Moose/Ops/Cohort.lua index be05e288a..8a130a982 100644 --- a/Moose Development/Moose/Ops/Cohort.lua +++ b/Moose Development/Moose/Ops/Cohort.lua @@ -787,7 +787,7 @@ end function COHORT:onafterStart(From, Event, To) -- Short info. - local text=string.format("Starting %s v%s %s", self.ClassName, self.version, self.name) + local text=string.format("Starting %s v%s %s [%s]", self.ClassName, self.version, self.name, self.attribute) self:I(self.lid..text) -- Start the status monitoring. @@ -993,7 +993,7 @@ end -- @param #COHORT self -- @param #table MissionTypes (Optional) Count only assest that can perform certain mission type(s). Default is all types. -- @param #table Attributes (Optional) Count only assest that have a certain attribute(s), e.g. `WAREHOUSE.Attribute.AIR_BOMBER`. --- @return Core.Set#SET_OPSGROUPS Ops groups set. +-- @return Core.Set#SET_OPSGROUP Ops groups set. function COHORT:GetOpsGroups(MissionTypes, Attributes) local set=SET_OPSGROUP:New() diff --git a/Moose Development/Moose/Ops/Commander.lua b/Moose Development/Moose/Ops/Commander.lua index 3b0e10a60..022fc2678 100644 --- a/Moose Development/Moose/Ops/Commander.lua +++ b/Moose Development/Moose/Ops/Commander.lua @@ -1254,6 +1254,23 @@ function COMMANDER:CheckOpsQueue() if operation:IsRunning() then + -- Loop over missions. + for _,_mission in pairs(operation.missions or {}) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission.phase==nil or (mission.phase and mission.phase==operation.phase) and mission:IsPlanned() then + self:AddMission(mission) + end + end + + -- Loop over targets. + for _,_target in pairs(operation.targets or {}) do + local target=_target --Ops.Target#TARGET + + if (target.phase==nil or (target.phase and target.phase==operation.phase)) and (not self:IsTarget(target)) then + self:AddTarget(target) + end + end end @@ -1352,6 +1369,9 @@ function COMMANDER:CheckTargetQueue() mission:SetRequiredAttribute(resource.Attributes) mission:SetRequiredProperty(resource.Properties) + -- Set operation (if any). + mission.operation=target.operation + -- Set resource mission. resource.mission=mission @@ -1651,6 +1671,30 @@ function COMMANDER:RecruitAssetsForEscort(Mission, Assets) return true end +--- Recruit assets for a given TARGET. +-- @param #COMMANDER self +-- @param Ops.Target#TARGET Target The target. +-- @param #string MissionType Mission Type. +-- @param #number NassetsMin Min number of required assets. +-- @param #number NassetsMax Max number of required assets. +-- @return #boolean If `true` enough assets could be recruited. +-- @return #table Assets that have been recruited from all legions. +-- @return #table Legions that have recruited assets. +function COMMANDER:RecruitAssetsForTarget(Target, MissionType, NassetsMin, NassetsMax) + + -- Cohorts. + local Cohorts=self:_GetCohorts() + + -- Target position. + local TargetVec2=Target:GetVec2() + + -- Recruite assets. + local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, MissionType, nil, NassetsMin, NassetsMax, TargetVec2) + + + return recruited, assets, legions +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Transport Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/FlightControl.lua b/Moose Development/Moose/Ops/FlightControl.lua index 1ae08735e..f76b000cc 100644 --- a/Moose Development/Moose/Ops/FlightControl.lua +++ b/Moose Development/Moose/Ops/FlightControl.lua @@ -12,7 +12,15 @@ -- -- === -- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20FlightControl). +-- +-- === +-- -- ### Author: **funkyfranky** +-- +-- === -- @module OPS.FlightControl -- @image OPS_FlightControl.png @@ -62,7 +70,10 @@ -- # 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). +-- Therefore, it solves (or reduces) a lot of common problems with the DCS implementation. +-- +-- You might be familiar with the `AIRBOSS` class. This class is the analogue for land based airfields. One major difference is that no pre-recorded sound files are +-- necessary. The radio transmissions use the SRS text-to-speech feature. -- -- ## Prerequisites -- @@ -77,7 +88,54 @@ -- * Only one player/client per group as we can create menus only for a group and not for a specific unit. -- * Only FLIGHTGROUPS are controlled. This means some older classes, *e.g.* RAT are not supported (yet). -- * So far only airdromes are handled, *i.e.* no FARPs or ships. --- * Only fixed wing aircraft are handled until now, *i.e.* no helos. +-- * Helicopters are not treated differently from fixed wing aircraft until now. +-- * The active runway can only be determined by the wind direction. So at least set a very light wind speed in your mission. +-- +-- # Basic Usage +-- +-- A flight control for a given airdrome can be created with the @{#FLIGHTCONTROL.New}(*AirbaseName, Frequency, Modulation, PathToSRS*) function. You need to specify the name of the airbase, the +-- tower radio frequency, its modulation and the path, where SRS is located on the machine that is running this mission. +-- +-- For the FC to be operating, it needs to be started with the @{#FLIGHTCONTROL.Start}() function. +-- +-- ## Simple Script +-- +-- The simplest script looks like +-- +-- local FC_BATUMI=FLIGHTCONTROL:New(AIRBASE.Caucasus.Batumi, 251, nil, "D:\\SomeDirectory\\_SRS") +-- FC_BATUMI:Start() +-- +-- This will start the FC for at the Batumi airbase with tower frequency 251 MHz AM. SRS needs to be in the given directory. +-- +-- Like this, a default holding pattern (see below) is parallel to the direction of the active runway. +-- +-- # Holding Patterns +-- +-- Holding pattern are air spaces where incoming aircraft are guided to and have to hold until they get landing clearance. +-- +-- You can add a holding pattern with the @{#FLIGHTCONTROL.AddHoldingPattern}(*ArrivalZone, Heading, Length, FlightlevelMin, FlightlevelMax, Prio*) function, where +-- +-- * `ArrivalZone` is the zone where the aircraft enter the pattern. +-- * `Heading` is the direction into which the aircraft have to fly from the arrival zone. +-- * `Length` is the length of the pattern. +-- * `FlightLevelMin` is the lowest altitude at which aircraft can hold. +-- * `FlightLevelMax` is the highest altitude at which aircraft can hold. +-- * `Prio` is the priority of this holdig stacks. If multiple patterns are defined, patterns with higher prio will be filled first. +-- +-- # Parking Guard +-- +-- # Taxi Limits +-- +-- You can define limits on how many aircraft are simultaniously landing and taking off. This avoids (DCS) problems where taxiing aircraft cause a "traffic jam" on the taxi way(s) +-- and bring the whole airbase effectively to a stand still. +-- +-- ## Landing Limits +-- +-- +-- ## Takeoff Limits +-- +-- Note that the limits here are only affecting AI aircraft groups. Human players are assumed to be a lot more well behaved and capable as they are able to taxi around obstacles, *e.g.* +-- other aircraft etc. -- -- -- @field #FLIGHTCONTROL @@ -102,7 +160,7 @@ FLIGHTCONTROL = { Nlanding = nil, dTlanding = nil, Nparkingspots = nil, - holdingpatterns = {}, + holdingpatterns = {}, hpcounter = 0, } @@ -170,20 +228,20 @@ FLIGHTCONTROL.FlightStatus={ --- FlightControl class version. -- @field #string version -FLIGHTCONTROL.version="0.5.2" +FLIGHTCONTROL.version="0.5.3" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list -- TODO: Runway destroyed. --- TODO: Support airwings. Dont give clearance for Alert5 or if mission has not started. -- TODO: Switch to enable/disable AI messages. -- TODO: Improve ATC TTS messages. -- TODO: Talk me down option. -- TODO: ATIS option. -- TODO: Check runways and clean up. --- TODO: Accept and forbit parking spots. -- TODO: Add FARPS? +-- DONE: Accept and forbit parking spots. DONE via AIRBASE black/white lists and airwing features. +-- DONE: Support airwings. Dont give clearance for Alert5 or if mission has not started. -- DONE: Define holding zone. -- DONE: Basic ATC voice overs. -- DONE: Add SRS TTS. @@ -234,6 +292,9 @@ function FLIGHTCONTROL:New(AirbaseName, Frequency, Modulation, PathToSRS) -- 5 NM zone around the airbase. self.zoneAirbase=ZONE_RADIUS:New("FC", self:GetCoordinate():GetVec2(), UTILS.NMToMeters(5)) + + -- Add backup holding pattern. + self:_AddHoldingPatternBackup() -- Set alias. self.alias=self.airbasename.." Tower" @@ -331,7 +392,17 @@ function FLIGHTCONTROL:SetFrequency(Frequency, Modulation) self.frequency=Frequency or 305 self.modulation=Modulation or radio.modulation.AM + + if self.msrsPilot then + self.msrsPilot:SetFrequencies(Frequency) + self.msrsPilot:SetModulations(Modulation) + end + if self.msrsTower then + self.msrsTower:SetFrequencies(Frequency) + self.msrsTower:SetModulations(Modulation) + end + return self end @@ -398,8 +469,9 @@ end -- @param #number Length Length in nautical miles. Default 15 NM. -- @param #number FlightlevelMin Min flight level. Default 5. -- @param #number FlightlevelMax Max flight level. Default 15. +-- @param #number Prio Priority. Lower is higher. Default 50. -- @return #FLIGHTCONTROL.HoldingPattern Holding pattern table. -function FLIGHTCONTROL:AddHoldingPattern(ArrivalZone, Heading, Length, FlightlevelMin, FlightlevelMax) +function FLIGHTCONTROL:AddHoldingPattern(ArrivalZone, Heading, Length, FlightlevelMin, FlightlevelMax, Prio) -- Get ZONE if passed as string. if type(ArrivalZone)=="string" then @@ -410,13 +482,14 @@ function FLIGHTCONTROL:AddHoldingPattern(ArrivalZone, Heading, Length, Flightlev self.hpcounter=self.hpcounter+1 local hp={} --#FLIGHTCONTROL.HoldingPattern - hp.arrivalzone=ArrivalZone hp.uid=self.hpcounter + hp.arrivalzone=ArrivalZone hp.name=string.format("%s-%d", ArrivalZone:GetName(), hp.uid) hp.pos0=ArrivalZone:GetCoordinate() hp.pos1=hp.pos0:Translate(UTILS.NMToMeters(Length or 15), Heading) hp.angelsmin=FlightlevelMin or 5 hp.angelsmax=FlightlevelMax or 15 + hp.prio=Prio or 50 hp.stacks={} for i=hp.angelsmin, hp.angelsmax do @@ -438,6 +511,50 @@ function FLIGHTCONTROL:AddHoldingPattern(ArrivalZone, Heading, Length, Flightlev hp.pos0:ArrowToAll(hp.pos1, nil, {1,0,0}, 1, {1,1,0}, 0.5, 2, true) ArrivalZone:DrawZone() + local function _sort(a,b) + return a.prio %s to %s", Event, From, To, airbase and airbase:GetName() or "None")) + if self:IsAlive() then local allowed=true @@ -4296,7 +4299,7 @@ function FLIGHTGROUP:_PlayerMyStatus(groupname) local text=string.format("My Status:") text=text..string.format("\nCallsign: %s", tostring(flight:GetCallsignName())) text=text..string.format("\nFlight status: %s", tostring(flight:GetState())) - text=text..string.format("\nFlight control: %s status=%s", tostring(fc and fc.airbasename or "N/A"), tostring(fc and fc:GetFlightStatus(flight) or "N/A")) + text=text..string.format("\nFlight control: %s [%s]", tostring(fc and fc.airbasename or "N/A"), tostring(fc and fc:GetFlightStatus(flight) or "N/A")) -- Send message. --self:TextMessageToFlight(text, flight, 10, true) diff --git a/Moose Development/Moose/Ops/Legion.lua b/Moose Development/Moose/Ops/Legion.lua index 150384c15..ad7534e26 100644 --- a/Moose Development/Moose/Ops/Legion.lua +++ b/Moose Development/Moose/Ops/Legion.lua @@ -1869,8 +1869,9 @@ function LEGION:GetOpsGroups(MissionTypes, Attributes) for _,_cohort in pairs(self.cohorts) do local cohort=_cohort --Ops.Cohort#COHORT - local setcohort=cohort:GetOpsGroups(MissionTypes, Attributes) - setLegion:AddSet(setcohort) + local setCohort=cohort:GetOpsGroups(MissionTypes, Attributes) + self:I(self.lid..string.format("Found %d opsgroups of cohort %s", setCohort:Count(), cohort.name)) + setLegion:AddSet(setCohort) end return setLegion diff --git a/Moose Development/Moose/Ops/Operation.lua b/Moose Development/Moose/Ops/Operation.lua index 71bed062a..c038b53e9 100644 --- a/Moose Development/Moose/Ops/Operation.lua +++ b/Moose Development/Moose/Ops/Operation.lua @@ -33,6 +33,7 @@ -- @field #number counterPhase Running number counting the phases. -- @field #OPERATION.Phase phase Currently active phase (if any). -- @field #table targets Targets. +-- @field #table missions Missions. -- -- @extends Core.Fsm#FSM @@ -54,6 +55,7 @@ OPERATION = { phases = {}, counterPhase = 0, targets = {}, + missions = {}, } --- Global mission counter. @@ -136,16 +138,15 @@ function OPERATION:New(Name) --- Pseudo Functions --- ------------------------ - --- Triggers the FSM event "StatusUpdate". - -- @function [parent=#OPERATION] StatusUpdate + --- Triggers the FSM event "Start". + -- @function [parent=#OPERATION] Start -- @param #OPERATION self - --- Triggers the FSM event "Status" after a delay. - -- @function [parent=#OPERATION] __StatusUpdate + --- Triggers the FSM event "Start" after a delay. + -- @function [parent=#OPERATION] __Start -- @param #OPERATION self -- @param #number delay Delay in seconds. - --- Triggers the FSM event "Stop". -- @function [parent=#OPERATION] Stop -- @param #OPERATION self @@ -155,6 +156,15 @@ function OPERATION:New(Name) -- @param #OPERATION self -- @param #number delay Delay in seconds. + --- Triggers the FSM event "StatusUpdate". + -- @function [parent=#OPERATION] StatusUpdate + -- @param #OPERATION self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#OPERATION] __StatusUpdate + -- @param #OPERATION self + -- @param #number delay Delay in seconds. + --- Triggers the FSM event "PhaseChange". -- @function [parent=#OPERATION] PhaseChange @@ -243,6 +253,36 @@ function OPERATION:AddPhase(Name) return phase end +--- Add mission to operation. +-- @param #OPERATION self +-- @param Ops.Auftrag#AUFTRAG Mission The mission to add. +-- @param #OPERATION.Phase Phase (Optional) The phase in which the mission should be executed. If no phase is given, it will be exectuted ASAP. +function OPERATION:AddMission(Mission, Phase) + + Mission.phase=Phase + Mission.operation=self + + table.insert(self.missions, Mission) + + return self +end + +--- Add Target to operation. +-- @param #OPERATION self +-- @param Ops.Target#TARGET Target The target to add. +-- @param #OPERATION.Phase Phase (Optional) The phase in which the target should be attacked. If no phase is given, it will be attacked ASAP. +function OPERATION:AddTarget(Target, Phase) + + Target.phase=Phase + Target.operation=self + + table.insert(self.targets, Target) + + return self +end + + + --- Get a phase by its name. -- @param #OPERATION self -- @param #string Name Name of the phase. Default "Phase-01" where the last number is a running number. @@ -310,6 +350,14 @@ function OPERATION:IsAssignedCohort(Cohort) self:T(self.lid..string.format("Cohort %s is assigned to this operation", Cohort.name)) return true else + + -- Check if legion of this cohort was assigned. + local Legion=Cohort.legion + if Legion and self:IsAssignedLegion(Legion) then + self:T(self.lid..string.format("Legion %s of Cohort %s is assigned to this operation", Legion.alias, Cohort.name)) + return true + end + self:T(self.lid..string.format("Cohort %s is NOT assigned to this operation", Cohort.name)) return false end @@ -460,6 +508,22 @@ function OPERATION:GetPhaseActive() return self.phase end +--- Get name of a phase. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase of which the name is returned. +-- @return #string The name of the phase. +function OPERATION:GetPhaseName(Phase) + + Phase=Phase or self.phase + + if Phase then + return Phase.name + else + return "None" + end + +end + --- Check if a phase is the currently active one. -- @param #OPERATION self -- @param #OPERATION.Phase Phase The phase to check. @@ -509,6 +573,24 @@ function OPERATION:CountPhases(Status) return N end +--- Count targets alive. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase (Optional) Only count targets set for this phase. +-- @return #number Number of phases +function OPERATION:CountTargets(Phase) + + local N=0 + for _,_target in pairs(self.targets) do + local target=_target --Ops.Target#TARGET + + if target:IsAlive() and (Phase==nil or target.phase==Phase) then + N=N+1 + end + end + + return N +end + --- Check if operation is in FSM state "Planned". -- @param #OPERATION self -- @return #boolean If `true`, operation is "Planned". diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index 30ee7f641..4a74e6dda 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -4162,6 +4162,17 @@ function OPSGROUP:onafterTaskExecute(From, Event, To, Task) --- -- Just stay put and wait until something happens. + + elseif Task.dcstask.id==AUFTRAG.SpecialTask.REARMING then + + --- + -- Task "Rearming" + --- + + -- Check if ammo is full. + + local rearmed=self:_CheckAmmoFull() + elseif Task.dcstask.id==AUFTRAG.SpecialTask.ALERT5 then @@ -4483,8 +4494,10 @@ function OPSGROUP:onafterTaskCancel(From, Event, To, Task) done=true elseif Task.dcstask.id==AUFTRAG.SpecialTask.AMMOSUPPLY then done=true - elseif Task.dcstask.id==AUFTRAG.SpecialTask.FUELSUPPLY then + elseif Task.dcstask.id==AUFTRAG.SpecialTask.FUELSUPPLY then done=true + elseif Task.dcstask.id==AUFTRAG.SpecialTask.REARMING then + done=true elseif Task.dcstask.id==AUFTRAG.SpecialTask.ALERT5 then done=true elseif Task.dcstask.id==AUFTRAG.SpecialTask.ONGUARD or Task.dcstask.id==AUFTRAG.SpecialTask.ARMOREDGUARD then @@ -4944,7 +4957,12 @@ function OPSGROUP:onbeforeMissionStart(From, Event, To, Mission) -- Startup group if it is uncontrolled. Alert 5 aircraft will not be started though! if self:IsFlightgroup() and self:IsUncontrolled() and Mission.type~=AUFTRAG.Type.ALERT5 then - self:StartUncontrolled(delay) + local fc=FLIGHTGROUP.GetFlightControl(self) + if fc and fc:IsControlling(self) then + FLIGHTGROUP.SetReadyForTakeoff(self, true) + else + self:StartUncontrolled(delay) + end end return true @@ -4972,9 +4990,9 @@ function OPSGROUP:onafterMissionStart(From, Event, To, Mission) Mission:__Started(3) -- Set ready for takeoff in case of FLIGHTCONTROL. - if self.isFlightgroup and Mission.type~=AUFTRAG.Type.ALERT5 then - FLIGHTGROUP.SetReadyForTakeoff(self, true) - end + --if self.isFlightgroup and Mission.type~=AUFTRAG.Type.ALERT5 then + -- FLIGHTGROUP.SetReadyForTakeoff(self, true) + --end -- Route group to mission zone. if self.speedMax>3.6 or true then @@ -5399,8 +5417,9 @@ function OPSGROUP:RouteToMission(mission, delay) elseif mission.type==AUFTRAG.Type.PATROLZONE or mission.type==AUFTRAG.Type.BARRAGE or - mission.type==AUFTRAG.Type.AMMOSUPPLY or - mission.type==AUFTRAG.Type.FUELSUPPLY or + mission.type==AUFTRAG.Type.AMMOSUPPLY or + mission.type==AUFTRAG.Type.FUELSUPPLY or + mission.type==AUFTRAG.Type.REARMING or mission.type==AUFTRAG.Type.AIRDEFENSE or mission.type==AUFTRAG.Type.EWR then --- @@ -5540,8 +5559,6 @@ function OPSGROUP:RouteToMission(mission, delay) if inRange then - env.info("FF in range!") - waypointcoord=self:GetCoordinate(true) else diff --git a/Moose Development/Moose/Ops/Platoon.lua b/Moose Development/Moose/Ops/Platoon.lua index c262ff4a5..6d6a3e87a 100644 --- a/Moose Development/Moose/Ops/Platoon.lua +++ b/Moose Development/Moose/Ops/Platoon.lua @@ -100,6 +100,7 @@ end -- Start & Status ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--[[ --- On after Start event. Starts the FLIGHTGROUP FSM and event handlers. -- @param #PLATOON self -- @param #string From From state. @@ -114,6 +115,7 @@ function PLATOON:onafterStart(From, Event, To) -- Start the status monitoring. self:__Status(-1) end +]] --- On after "Status" event. -- @param #PLATOON self From 8926e06e44007264361cba92deb431ad568fbcc4 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 8 Jun 2022 23:42:17 +0200 Subject: [PATCH 13/20] Runways **AIRBASE** - Runways are now retrieved from DCS API function --- Moose Development/Moose/Core/Database.lua | 2 +- .../Moose/Functional/Warehouse.lua | 25 +- Moose Development/Moose/Ops/FlightControl.lua | 99 ++++-- Moose Development/Moose/Ops/FlightGroup.lua | 9 +- Moose Development/Moose/Ops/OpsGroup.lua | 11 + Moose Development/Moose/Utilities/Utils.lua | 2 +- Moose Development/Moose/Wrapper/Airbase.lua | 292 +++++++++++++++++- 7 files changed, 393 insertions(+), 47 deletions(-) diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index 5e2063a12..906cd5edb 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -1037,7 +1037,7 @@ function DATABASE:_RegisterAirbases() local airbaseUID=airbase:GetID(true) -- Debug output. - local text=string.format("Register %s: %s (ID=%d UID=%d), parking=%d [", AIRBASE.CategoryName[airbase.category], tostring(DCSAirbaseName), airbaseID, airbaseUID, airbase.NparkingTotal) + local text=string.format("Register %s: %s (UID=%d), Runways=%d, Parking=%d [", AIRBASE.CategoryName[airbase.category], tostring(DCSAirbaseName), airbaseUID, #airbase.runways, airbase.NparkingTotal) for _,terminalType in pairs(AIRBASE.TerminalType) do if airbase.NparkingTerminal and airbase.NparkingTerminal[terminalType] then text=text..string.format("%d=%d ", terminalType, airbase.NparkingTerminal[terminalType]) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 04e5728ba..10eb19150 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -1939,6 +1939,7 @@ function WAREHOUSE:New(warehouse, alias) self:SetMarker(true) self:SetReportOff() self:SetRunwayRepairtime() + self.allowSpawnOnClientSpots=false -- Add warehouse to database. _WAREHOUSEDB.Warehouses[self.uid]=self @@ -2584,6 +2585,14 @@ function WAREHOUSE:SetSafeParkingOff() return self end +--- Set wether client parking spots can be used for spawning. +-- @param #WAREHOUSE self +-- @return #WAREHOUSE self +function WAREHOUSE:SetAllowSpawnOnClientParking() + self.allowSpawnOnClientSpots=true + return self +end + --- Set low fuel threshold. If one unit of an asset has less fuel than this number, the event AssetLowFuel will be fired. -- @param #WAREHOUSE self -- @param #number threshold Relative low fuel threshold, i.e. a number in [0,1]. Default 0.15 (15%). @@ -7878,14 +7887,16 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) -- Get client coordinates. local function _clients() - local clients=_DATABASE.CLIENTS local coords={} - for clientname, client in pairs(clients) do - local template=_DATABASE:GetGroupTemplateFromUnitName(clientname) - local units=template.units - for i,unit in pairs(units) do - local coord=COORDINATE:New(unit.x, unit.alt, unit.y) - coords[unit.name]=coord + if not self.allowSpawnOnClientSpots then + local clients=_DATABASE.CLIENTS + for clientname, client in pairs(clients) do + local template=_DATABASE:GetGroupTemplateFromUnitName(clientname) + local units=template.units + for i,unit in pairs(units) do + local coord=COORDINATE:New(unit.x, unit.alt, unit.y) + coords[unit.name]=coord + end end end return coords diff --git a/Moose Development/Moose/Ops/FlightControl.lua b/Moose Development/Moose/Ops/FlightControl.lua index f76b000cc..9911435c6 100644 --- a/Moose Development/Moose/Ops/FlightControl.lua +++ b/Moose Development/Moose/Ops/FlightControl.lua @@ -317,9 +317,6 @@ function FLIGHTCONTROL:New(AirbaseName, Frequency, Modulation, PathToSRS) -- Wait at least 10 seconds after last radio message before calling the next status update. self.dTmessage=10 - - -- Init runways. - self:_InitRunwayData() -- Start State. self:SetStartState("Stopped") @@ -801,7 +798,7 @@ function FLIGHTCONTROL:OnEventBirth(EventData) -- Create player menu. self:ScheduleOnce(0.5, self._CreateFlightGroup, self, EventData.IniGroup) - end + end -- Spawn parking guard. if bornhere then @@ -1540,35 +1537,19 @@ 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() + local rwy=self.airbase:GetActiveRunway() + return rwy end ---- Get the active runway based on current wind direction. +--- Get the name of the active runway. -- @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 + local rwy=self.airbase:GetRunwayName() return rwy end @@ -2408,8 +2389,7 @@ function FLIGHTCONTROL:_PlayerVectorInbound(groupname) local dist=UTILS.MetersToNM(distance) -- Message text. - local text=string.format("%s, fly heading %03d for %d nautical miles, hold at angels %d.", - callsign, self.alias, heading, dist, flight.stack.angels) + local text=string.format("%s, fly heading %03d for %d nautical miles, hold at angels %d.", callsign, heading, dist, flight.stack.angels) -- Send message. self:TextMessageToFlight(text, flight) @@ -2513,16 +2493,24 @@ function FLIGHTCONTROL:_PlayerHolding(groupname) if stack then + -- Pilot calls inbound for landing. + local text=string.format("%s, %s, arrived at holding pattern", self.alias, callsign) + + -- Radio message. + self:TransmissionPilot(text, flight) + -- Current coordinate. local Coordinate=flight:GetCoordinate(nil, player.name) -- Distance. local dist=stack.pos0:Get2DDistance(Coordinate) - if dist<5000 then + local dmax=UTILS.NMToMeters(5) + + if dist idx="07". +-- @field #number heading True heading of the runway in degrees. +-- @field #number magheading Magnetic heading of the runway in degrees. This is what is marked on the runway. -- @field #number length Length of runway in meters. +-- @field #number width Width of runway in meters. +-- @field Core.Zone#ZONE_POLYGON zone Runway zone. +-- @field Core.Point#COORDINATE center Center of the runway. -- @field Core.Point#COORDINATE position Position of runway start. -- @field Core.Point#COORDINATE endpoint End point of runway. +-- @field #boolean isLeft If `true`, this is the left of two parallel runways. If `false`, this is the right of two runways. If `nil`, no parallel runway exists. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Registration @@ -602,6 +609,9 @@ function AIRBASE:Register(AirbaseName) else self:E("ERROR: Unknown airbase category!") end + + -- Init Runways. + self:_InitRunways() -- Init parking spots. self:_InitParkingSpots() @@ -819,6 +829,42 @@ function AIRBASE:SetParkingSpotBlacklist(TerminalIdBlacklist) return self end +--- Sets the ATC belonging to an airbase object to be silent and unresponsive. This is useful for disabling the award winning ATC behavior in DCS. +-- Note that this DOES NOT remove the airbase from the list. It just makes it unresponsive and silent to any radio calls to it. +-- @param #AIRBASE self +-- @param #boolean Silent If `true`, enable silent mode. If `false` or `nil`, disable silent mode. +-- @return #AIRBASE self +function AIRBASE:SetRadioSilentMode(Silent) + + -- Get DCS airbase object. + local airbase=self:GetDCSObject() + + -- Set mode. + if airbase then + airbase:setRadioSilentMode(Silent) + end + + return self +end + +--- Check whether or not the airbase has been silenced. +-- @param #AIRBASE self +-- @return #boolean If `true`, silent mode is enabled. +function AIRBASE:GetRadioSilentMode() + + -- Is silent? + local silent=nil + + -- Get DCS airbase object. + local airbase=self:GetDCSObject() + + -- Set mode. + if airbase then + silent=airbase:getRadioSilentMode() + end + + return silent +end --- Get category of airbase. -- @param #AIRBASE self @@ -1478,6 +1524,221 @@ end -- Runway ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Get runways. +-- @param #AIRBASE self +-- @return #table Runway data. +function AIRBASE:GetRunways() + return self.runways or {} +end + +--- Init runways. +-- @param #AIRBASE self +-- @param #boolean IncludeInverse If `true` or `nil`, include inverse runways. +-- @return #table Runway data. +function AIRBASE:_InitRunways(IncludeInverse) + + -- Default is true. + if IncludeInverse==nil then + IncludeInverse=true + end + + -- Runway table. + local Runways={} + + if self:GetAirbaseCategory()~=Airbase.Category.AIRDROME then + self.runways={} + return {} + end + + --- Function to create a runway data table. + local function _createRunway(name, course, width, length, center) + + -- Bearing in rad. + local bearing=-1*course + + -- Heading in degrees. + local heading=math.deg(bearing) + + -- Data table. + local runway={} --#AIRBASE.Runway + runway.name=string.format("%02d", tonumber(name)) + runway.magheading=tonumber(runway.name)*10 + runway.heading=heading + runway.width=width or 0 + runway.length=length or 0 + runway.center=COORDINATE:NewFromVec3(center) + + -- Ensure heading is [0,360] + if runway.heading>360 then + runway.heading=runway.heading-360 + elseif runway.heading<0 then + runway.heading=runway.heading+360 + end + + -- For example at Nellis, DCS reports two runways, i.e. 03 and 21, BUT the "course" of both is -0.700 rad = 40 deg! + -- As a workaround, I check the difference between the "magnetic" heading derived from the name and the true heading. + -- If this is too large then very likely the "inverse" heading is the one we are looking for. + if math.abs(runway.heading-runway.magheading)>60 then + self:T(string.format("WARNING: Runway %s: heading=%.1f magheading=%.1f", runway.name, runway.heading, runway.magheading)) + runway.heading=runway.heading-180 + end + + -- Ensure heading is [0,360] + if runway.heading>360 then + runway.heading=runway.heading-360 + elseif runway.heading<0 then + runway.heading=runway.heading+360 + end + + -- Start and endpoint of runway. + runway.position=runway.center:Translate(-runway.length/2, runway.heading) + runway.endpoint=runway.center:Translate( runway.length/2, runway.heading) + + local init=runway.center:GetVec3() + local width = runway.width/2 + local L2=runway.length/2 + + local offset1 = {x = init.x + (math.cos(bearing + math.pi) * L2), y = init.z + (math.sin(bearing + math.pi) * L2)} + local offset2 = {x = init.x - (math.cos(bearing + math.pi) * L2), y = init.z - (math.sin(bearing + math.pi) * L2)} + + local points={} + points[1] = {x = offset1.x + (math.cos(bearing + (math.pi/2)) * width), y = offset1.y + (math.sin(bearing + (math.pi/2)) * width)} + points[2] = {x = offset1.x + (math.cos(bearing - (math.pi/2)) * width), y = offset1.y + (math.sin(bearing - (math.pi/2)) * width)} + points[3] = {x = offset2.x + (math.cos(bearing - (math.pi/2)) * width), y = offset2.y + (math.sin(bearing - (math.pi/2)) * width)} + points[4] = {x = offset2.x + (math.cos(bearing + (math.pi/2)) * width), y = offset2.y + (math.sin(bearing + (math.pi/2)) * width)} + + -- Runway zone. + runway.zone=ZONE_POLYGON_BASE:New(string.format("%s Runway %s", self.AirbaseName, runway.name), points) + + return runway + end + + + -- Get DCS object. + local airbase=self:GetDCSObject() + + if airbase then + + + -- Get DCS runways. + local runways=airbase:getRunways() + + -- Debug info. + self:T2(runways) + + if runways then + + -- Loop over runways. + for _,rwy in pairs(runways) do + + -- Debug info. + self:T(rwy) + + -- Get runway data. + local runway=_createRunway(rwy.Name, rwy.course, rwy.width, rwy.length, rwy.position) --#AIRBASE.Runway + + -- Add to table. + table.insert(Runways, runway) + + -- Include "inverse" runway. + if IncludeInverse then + + -- Create "inverse". + local idx=tonumber(runway.name) + local name2=tostring(idx-18) + if idx<18 then + name2=tostring(idx+18) + end + + -- Create "inverse" runway. + local runway=_createRunway(name2, rwy.course-math.pi, rwy.width, rwy.length, rwy.position) --#AIRBASE.Runway + + -- Add inverse to table. + table.insert(Runways, runway) + + end + + end + + end + + end + + -- Look for identical (parallel) runways, e.g. 03L and 03R at Nellis. + local rpairs={} + for i,_ri in pairs(Runways) do + local ri=_ri --#AIRBASE.Runway + for j,_rj in pairs(Runways) do + local rj=_rj --#AIRBASE.Runway + if i 0 + return ((b.z - a.z)*(c.x - a.x) - (b.x - a.x)*(c.z - a.z)) > 0 + end + + --[[ + local a={x=1, y=0, z=0} + local A={x=0, y=0, z=0} + local b={x=0, y=0, z=1} + local c={x=0, y=0, z=-1} + local bl=isLeft(A, a, b) + local cl=isLeft(A, a, c) + env.info(string.format("b left=%s, c left=%s", tostring(bl), tostring(cl))) + ]] + + for i,j in pairs(rpairs) do + local ri=Runways[i] --#AIRBASE.Runway + local rj=Runways[j] --#AIRBASE.Runway + + -- Draw arrow. + --ri.center:ArrowToAll(rj.center) + + local c0=ri.center + + -- Vector in the direction of the runway. + local a=UTILS.VecTranslate(c0, 1000, ri.heading) + + -- Vector from runway i to runway j. + local b=UTILS.VecSubstract(rj.center, ri.center) + b=UTILS.VecAdd(ri.center, b) + + --[[ + local ca=COORDINATE:NewFromVec3(a) + local cb=COORDINATE:NewFromVec3(b) + c0:ArrowToAll(ca, nil , {0,1,0}) + c0:ArrowToAll(cb, nil , {0,0,1}) + ]] + + -- Check if rj is left of ri. + local left=isLeft(c0, a, b) + + --env.info(string.format("Found pair %s: i=%d, j=%d, left==%s", ri.name, i, j, tostring(left))) + + if left then + ri.isLeft=false + rj.isLeft=true + else + ri.isLeft=true + rj.isLeft=false + end + + --break + end + + -- Set runways. + self.runways=Runways + + return Runways +end + + --- Get runways data. Only for airdromes! -- @param #AIRBASE self -- @param #number magvar (Optional) Magnetic variation in degrees. @@ -1659,7 +1920,10 @@ end function AIRBASE:GetActiveRunway(magvar) -- Get runways data (initialize if necessary). - local runways=self:GetRunwayData(magvar) + --local runways=self:GetRunwayData(magvar) + + -- Get runway data. + local runways=self:GetRunways() -- Return user forced active runway if it was set. if self.activerwyno then @@ -1695,9 +1959,6 @@ function AIRBASE:GetActiveRunway(magvar) -- Dot product: parallel component of the two vectors. local dot=UTILS.VecDot(Vwind, Vrunway) - -- Debug. - --env.info(string.format("runway=%03d° dot=%.3f", runway.heading, dot)) - -- New min? if dotmin==nil or dot Date: Tue, 21 Jun 2022 16:08:46 +0200 Subject: [PATCH 14/20] FLIGHTCONTROL v0.6.0 --- Moose Development/Moose/Ops/ATIS.lua | 101 +-- Moose Development/Moose/Ops/FlightControl.lua | 701 +++++++++++++----- Moose Development/Moose/Ops/FlightGroup.lua | 433 +++++++---- Moose Development/Moose/Ops/OpsGroup.lua | 67 +- Moose Development/Moose/Wrapper/Airbase.lua | 173 ++++- 5 files changed, 1034 insertions(+), 441 deletions(-) diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index 848fe265b..afc9845e9 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -52,7 +52,6 @@ --- ATIS class. -- @type ATIS -- @field #string ClassName Name of the class. --- @field #boolean Debug Debug mode. Messages to all about status. -- @field #string lid Class id string for output to DCS log file. -- @field #string theatre DCS map name. -- @field #string airbasename The name of the airbase. @@ -309,7 +308,6 @@ -- @field #ATIS ATIS = { ClassName = "ATIS", - Debug = false, lid = nil, theatre = nil, airbasename = nil, @@ -614,26 +612,26 @@ ATIS.version="0.9.6" --- Create a new ATIS class object for a specific aircraft carrier unit. -- @param #ATIS self --- @param #string airbasename Name of the airbase. --- @param #number frequency Radio frequency in MHz. Default 143.00 MHz. --- @param #number modulation Radio modulation: 0=AM, 1=FM. Default 0=AM. See `radio.modulation.AM` and `radio.modulation.FM` enumerators +-- @param #string AirbaseName Name of the airbase. +-- @param #number Frequency Radio frequency in MHz. Default 143.00 MHz. +-- @param #number Modulation Radio modulation: 0=AM, 1=FM. Default 0=AM. See `radio.modulation.AM` and `radio.modulation.FM` enumerators. -- @return #ATIS self -function ATIS:New(airbasename, frequency, modulation) +function ATIS:New(AirbaseName, Frequency, Modulation) -- Inherit everything from FSM class. local self=BASE:Inherit(self, FSM:New()) -- #ATIS - self.airbasename=airbasename - self.airbase=AIRBASE:FindByName(airbasename) + self.airbasename=AirbaseName + self.airbase=AIRBASE:FindByName(AirbaseName) if self.airbase==nil then - self:E("ERROR: Airbase %s for ATIS could not be found!", tostring(airbasename)) + self:E("ERROR: Airbase %s for ATIS could not be found!", tostring(AirbaseName)) return nil end -- Default freq and modulation. - self.frequency=frequency or 143.00 - self.modulation=modulation or 0 + self.frequency=Frequency or 143.00 + self.modulation=Modulation or 0 -- Get map. self.theatre=env.mission.theatre @@ -740,15 +738,6 @@ function ATIS:New(airbasename, frequency, modulation) -- @param #string To To state. -- @param #string Text Report text. - - -- Debug trace. - if false then - self.Debug=true - BASE:TraceOnOff(true) - BASE:TraceClass(self.ClassName) - BASE:TraceLevel(1) - end - return self end @@ -809,6 +798,15 @@ function ATIS:SetRunwayLength() return self end +--- Give information on runway length. +-- @param #ATIS self +-- @return #ATIS self +function ATIS:SetRunwayLength() + self.rwylength=true + return self +end + + --- Give information on airfield elevation -- @param #ATIS self -- @return #ATIS self @@ -1395,7 +1393,8 @@ function ATIS:onafterBroadcast(From, Event, To) --- Runway --- -------------- - local runway, rwyLeft=self:GetActiveRunway() + local runwayLanding, rwyLandingLeft=self:GetActiveRunway() + local runwayTakeoff, rwyTakeoffLeft=self:GetActiveRunway(true) ------------ --- Time --- @@ -2017,19 +2016,19 @@ function ATIS:onafterBroadcast(From, Event, To) alltext=alltext..";\n"..subtitle -- Active runway. - local subtitle=string.format("Active runway %s", runway) - if rwyLeft==true then + local subtitle=string.format("Active runway %s", runwayLanding) + if rwyLandingLeft==true then subtitle=subtitle.." Left" - elseif rwyLeft==false then + elseif rwyLandingLeft==false then subtitle=subtitle.." Right" end local _RUNACT=subtitle if not self.useSRS then self:Transmission(ATIS.Sound.ActiveRunway, 1.0, subtitle) - self.radioqueue:Number2Transmission(runway) - if rwyLeft==true then + self.radioqueue:Number2Transmission(runwayLanding) + if rwyLandingLeft==true then self:Transmission(ATIS.Sound.Left, 0.2) - elseif rwyLeft==false then + elseif rwyLandingLeft==false then self:Transmission(ATIS.Sound.Right, 0.2) end end @@ -2141,7 +2140,7 @@ function ATIS:onafterBroadcast(From, Event, To) end -- ILS - local ils=self:GetNavPoint(self.ils, runway, rwyLeft) + local ils=self:GetNavPoint(self.ils, runwayLanding, rwyLandingLeft) if ils then subtitle=string.format("ILS frequency %.2f MHz", ils.frequency) if not self.useSRS then @@ -2159,7 +2158,7 @@ function ATIS:onafterBroadcast(From, Event, To) end -- Outer NDB - local ndb=self:GetNavPoint(self.ndbouter, runway, rwyLeft) + local ndb=self:GetNavPoint(self.ndbouter, runwayLanding, rwyLandingLeft) if ndb then subtitle=string.format("Outer NDB frequency %.2f MHz", ndb.frequency) if not self.useSRS then @@ -2177,7 +2176,7 @@ function ATIS:onafterBroadcast(From, Event, To) end -- Inner NDB - local ndb=self:GetNavPoint(self.ndbinner, runway, rwyLeft) + local ndb=self:GetNavPoint(self.ndbinner, runwayLanding, rwyLandingLeft) if ndb then subtitle=string.format("Inner NDB frequency %.2f MHz", ndb.frequency) if not self.useSRS then @@ -2236,7 +2235,7 @@ function ATIS:onafterBroadcast(From, Event, To) end -- PRMG - local ndb=self:GetNavPoint(self.prmg, runway, rwyLeft) + local ndb=self:GetNavPoint(self.prmg, runwayLanding, rwyLandingLeft) if ndb then subtitle=string.format("PRMG channel %d", ndb.frequency) if not self.useSRS then @@ -2363,39 +2362,19 @@ end --- Get active runway runway. -- @param #ATIS self +-- @param #boolean Takeoff If `true`, get runway for takeoff. Default is for landing. -- @return #string Active runway, e.g. "31" for 310 deg. -- @return #boolean Use Left=true, Right=false, or nil. -function ATIS:GetActiveRunway() - - local coord=self.airbase:GetCoordinate() - local height=coord:GetLandHeight() - - -- Get wind direction and speed in m/s. - local windFrom, windSpeed=coord:GetWind(height+10) - - -- Get active runway data based on wind direction. - local runact=self.airbase:GetActiveRunway(self.runwaym2t) - - -- Active runway "31". - local runway=self:GetMagneticRunway(windFrom) or runact.idx - - -- Left or right in case there are two runways with the same heading. - local rwyLeft=nil - - -- Check if user explicitly specified a runway. - if self.activerunway then - - -- Get explicit runway heading if specified. - local runwayno=self:GetRunwayWithoutLR(self.activerunway) - if runwayno~="" then - runway=runwayno - end - - -- Was "L"eft or "R"ight given? - rwyLeft=self:GetRunwayLR(self.activerunway) +function ATIS:GetActiveRunway(Takeoff) + + local runway=nil --Wrapper.Airbase#AIRBASE.Runway + if Takeoff then + runway=self.airbase:GetActiveRunwayTakeoff() + else + runway=self.airbase:GetActiveRunwayLanding() end - - return runway, rwyLeft + + return runway.name, runway.isLeft end --- Get runway from user supplied magnetic heading. diff --git a/Moose Development/Moose/Ops/FlightControl.lua b/Moose Development/Moose/Ops/FlightControl.lua index 9911435c6..845716cff 100644 --- a/Moose Development/Moose/Ops/FlightControl.lua +++ b/Moose Development/Moose/Ops/FlightControl.lua @@ -1,7 +1,5 @@ --- **OPS** - Air Traffic Control for AI and human players. -- --- --- -- **Main Features:** -- -- * Manage aircraft departure and arrival @@ -37,11 +35,9 @@ -- @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 frequency ATC radio frequency in MHz. -- @field #number modulation ATC radio modulation, *e.g.* `radio.modulation.AM`. -- @field #number NlandingTot Max number of aircraft groups in the landing pattern. @@ -59,6 +55,8 @@ -- @field Sound.SRS#MSRS msrsPilot Moose SRS wrapper. -- @field #number Tlastmessage Time stamp (abs.) of last radio transmission. -- @field #number dTmessage Time interval between messages. +-- @field #boolean markPatterns If `true`, park holding pattern. +-- @field #number speedLimitTaxi Taxi speed limit in m/s. -- @extends Core.Fsm#FSM --- **Ground Control**: Airliner X, Good news, you are clear to taxi to the active. @@ -153,10 +151,6 @@ FLIGHTCONTROL = { flights = {}, clients = {}, atis = nil, - activerwyno = 1, - atcfreq = nil, - atcradio = nil, - atcradiounitname = nil, Nlanding = nil, dTlanding = nil, Nparkingspots = nil, @@ -174,6 +168,8 @@ FLIGHTCONTROL = { -- @field #number angelsmin Smallest holding altitude in angels. -- @field #number angelsmax Largest holding alitude in angels. -- @field #table stacks Holding stacks. +-- @field #number markArrival Marker ID of the arrival zone. +-- @field #number markArrow Marker ID of the direction. --- Holding stack. -- @type FLIGHTCONTROL.HoldingStack @@ -183,10 +179,6 @@ FLIGHTCONTROL = { -- @field Core.Point#COORDINATE pos1 Second position of racetrack holding pattern. -- @field #number heading Heading. ---- 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 @@ -219,16 +211,9 @@ FLIGHTCONTROL.FlightStatus={ ARRIVED="Arrived", } ---- Runway data. --- @type FLIGHTCONTROL.Runway --- @field #number direction Direction of the runway. --- @field #number length Length of runway in meters. --- @field #number width Width of runway in meters. --- @field Core.Point#COORDINATE position Position of runway start. - --- FlightControl class version. -- @field #string version -FLIGHTCONTROL.version="0.5.3" +FLIGHTCONTROL.version="0.6.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -304,6 +289,7 @@ function FLIGHTCONTROL:New(AirbaseName, Frequency, Modulation, PathToSRS) self:SetLimitTaxi(1, false, 0) self:SetLandingInterval() self:SetFrequency(Frequency, Modulation) + self:SetMarkHoldingPattern(true) -- SRS for Tower. self.msrsTower=MSRS:New(PathToSRS, Frequency, Modulation) @@ -322,10 +308,14 @@ function FLIGHTCONTROL:New(AirbaseName, Frequency, Modulation, PathToSRS) self:SetStartState("Stopped") -- Add FSM transitions. - -- From State --> Event --> To State - self:AddTransition("Stopped", "Start", "Running") -- Start FSM. - self:AddTransition("*", "Status", "*") -- Update status. - self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FSM. + self:AddTransition("*", "StatusUpdate", "*") -- Update status. + + self:AddTransition("*", "PlayerKilledGuard", "*") -- Player killed parking guard + self:AddTransition("*", "PlayerSpeeding", "*") -- Player speeding on taxi way. + + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. -- Add to data base. _DATABASE:AddFlightControl(self) @@ -355,12 +345,12 @@ function FLIGHTCONTROL:New(AirbaseName, Frequency, Modulation, PathToSRS) -- @param #number delay Delay in seconds. - --- Triggers the FSM event "Status". - -- @function [parent=#FLIGHTCONTROL] Status + --- Triggers the FSM event "StatusUpdate". + -- @function [parent=#FLIGHTCONTROL] StatusUpdate -- @param #FLIGHTCONTROL self - --- Triggers the FSM event "Status" after a delay. - -- @function [parent=#FLIGHTCONTROL] __Status + --- Triggers the FSM event "StatusUpdate" after a delay. + -- @function [parent=#FLIGHTCONTROL] __StatusUpdate -- @param #FLIGHTCONTROL self -- @param #number delay Delay in seconds. @@ -405,6 +395,13 @@ end --- Set the number of aircraft groups, that are allowed to land simultaniously. -- Note that this restricts AI and human players. +-- +-- By default, up to two groups get landing clearance. They are spaced out in time, i.e. after the first one got cleared, the second has to wait a bit. +-- This +-- +-- By default, landing clearance is only given when **no** other flight is taking off. You can adjust this for airports with more than one runway or +-- in cases where simulatious takeoffs and landings are unproblematic. Note that only because there are multiple runways, it does not mean the AI uses them. +-- -- @param #FLIGHTCONTROL self -- @param #number Nlanding Max number of aircraft landing simultaniously. Default 2. -- @param #number Ntakeoff Allowed number of aircraft taking off for groups to get landing clearance. Default 0. @@ -503,11 +500,8 @@ function FLIGHTCONTROL:AddHoldingPattern(ArrivalZone, Heading, Length, Flightlev -- Add to table. table.insert(self.holdingpatterns, hp) - - -- Mark holding pattern. - hp.pos0:ArrowToAll(hp.pos1, nil, {1,0,0}, 1, {1,1,0}, 0.5, 2, true) - ArrivalZone:DrawZone() - + + -- Sort holding patterns wrt to prio. local function _sort(a,b) return a.prio=1 then - 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) + local text=string.format("State %s - Runway Landing=%s, Takeoff=%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(), rwyLanding, rwyTakeoff, Nfree, Noccu, Nresv, self.Nparkingspots, Nflights, NQparking, NQtaxiout, NQreadyto, NQtakeoff, NQinbound, NQholding, NQlanding, NQtaxiinb, NQarrived) self:I(self.lid..text) end @@ -747,9 +756,28 @@ function FLIGHTCONTROL:onafterStatus() else self:E(string.format("WARNING: Number of total flights %d!=%d number of flights in all queues!", Nflights, Nqueues)) end + + if self.verbose>=2 then + local text="Holding Patterns:" + for i,_pattern in pairs(self.holdingpatterns) do + local pattern=_pattern --#FLIGHTCONTROL.HoldingPattern + + -- Pattern info. + text=text..string.format("\n[%d] Pattern %s [Prio=%d, UID=%d]: Stacks=%d, Angels %d - %d", i, pattern.name, pattern.prio, pattern.uid, #pattern.stacks, pattern.angelsmin, pattern.angelsmax) + + if self.verbose>=4 then + -- Explicit stack info. + for _,_stack in pairs(pattern.stacks) do + local stack=_stack --#FLIGHTCONTROL.HoldingStack + local text=string.format("", stack.angels, stack) + end + end + end + self:I(self.lid..text) + end -- Next status update in ~30 seconds. - self:__Status(-30) + self:__StatusUpdate(-30) end --- Stop FLIGHTCONTROL FSM. @@ -763,7 +791,7 @@ function FLIGHTCONTROL:onafterStop() self:UnHandleEvent(EVENTS.Land) self:UnHandleEvent(EVENTS.EngineShutdown) self:UnHandleEvent(EVENTS.Crash) - + self:UnHandleEvent(EVENTS.Kill) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -878,6 +906,41 @@ function FLIGHTCONTROL:OnEventCrash(EventData) end +--- Event handler for event crash. +-- @param #FLIGHTCONTROL self +-- @param Core.Event#EVENTDATA EventData +function FLIGHTCONTROL:OnEventKill(EventData) + self:F3({EvendData=EventData}) + + -- Debug info. + self:T2(self.lid..string.format("KILL: ini unit = %s", tostring(EventData.IniUnitName))) + self:T3(self.lid..string.format("KILL: ini group = %s", tostring(EventData.IniGroupName))) + self:T2(self.lid..string.format("KILL: tgt unit = %s", tostring(EventData.TgtUnitName))) + self:T3(self.lid..string.format("KILL: tgt group = %s", tostring(EventData.TgtGroupName))) + + -- Parking guard name prefix. + local guardPrefix=string.format("Parking Guard %s", self.airbasename) + + local victimName=EventData.IniUnitName + local killerName=EventData.TgtUnitName + + if victimName and victimName:find(guardPrefix) then + + env.info(string.format("Parking guard %s killed!", victimName)) + + for _,_flight in pairs(self.flights) do + local flight=_flight --Ops.FlightGroup#FLIGHTGROUP + local element=flight:GetElementByName(killerName) + if element then + env.info(string.format("Parking guard %s killed by %s!", victimName, killerName)) + return + end + end + + end + +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Queue Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -916,7 +979,7 @@ function FLIGHTCONTROL:_CheckQueues() if parking and dTlanding>=self.dTlanding then -- Get callsign. - local callsign=flight:GetCallsignName() + local callsign=self:_GetCallsignName(flight) -- Runway. local runway=self:GetActiveRunwayText() @@ -926,20 +989,23 @@ function FLIGHTCONTROL:_CheckQueues() -- Transmit message. self:TransmissionTower(text, flight) - - -- Message. - local text=string.format("Runway %s, %s", runway, callsign) - - -- Transmit message. - self:TransmissionPilot(text, flight, 10) - + -- Give AI the landing signal. if flight.isAI then + + -- Message. + local text=string.format("Runway %s, cleared to land, %s", runway, callsign) + + -- Transmit message. + self:TransmissionPilot(text, flight, 10) + + -- Land AI. self:_LandAI(flight, parking) else - -- TODO: Humans have to confirm via F10 menu. + + -- We set this flight to landing. With this he is allowed to leave the pattern. self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.LANDING) - flight:_UpdateMenu(0.5) + end -- Set time last flight got landing clearance. @@ -960,10 +1026,10 @@ function FLIGHTCONTROL:_CheckQueues() if self:_CheckFlightTakeoff(flight) then -- Get callsign. - local callsign=flight:GetCallsignName() + local callsign=self:_GetCallsignName(flight) -- Runway. - local runway=self:GetActiveRunwayText() + local runway=self:GetActiveRunwayText(true) -- Message. local text=string.format("%s, %s, taxi to runway %s, hold short", callsign, self.alias, runway) @@ -1230,14 +1296,18 @@ end -- @return Ops.FlightGroup#FLIGHTGROUP Marshal flight next in line and ready to enter the pattern. Or nil if no flight is ready. function FLIGHTCONTROL:_GetNextFightHolding() + -- Get all flights holding. local Qholding=self:GetFlights(FLIGHTCONTROL.FlightStatus.HOLDING) + + -- Min holding time in seconds. + local TholdingMin=30 if #Qholding==0 then return nil elseif #Qholding==1 then local fg=Qholding[1] --Ops.FlightGroup#FLIGHTGROUP local T=fg:GetHoldingTime() - if T>60 then + if T>TholdingMin then return fg end end @@ -1282,10 +1352,10 @@ function FLIGHTCONTROL:_GetNextFightHolding() -- Check holding time. local T=fg:GetHoldingTime() - if T>60 then + if T>TholdingMin then return fg end - + return nil end @@ -1433,6 +1503,12 @@ 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())) + -- Update menu when flight status changed. + if flight.controlstatus~=status and not flight.isAI then + self:T(self.lid.."Updating menu in 0.2 sec after flight status change") + flight:_UpdateMenu(0.2) + end + -- Set new status flight.controlstatus=status @@ -1457,8 +1533,10 @@ end -- @return #boolean function FLIGHTCONTROL:IsControlling(flight) - return flight.flightcontrol and flight.flightcontrol.airbasename==self.airbasename or false - + -- Check that we are controlling this flight. + local is=flight.flightcontrol and flight.flightcontrol.airbasename==self.airbasename or false + + return is end --- Check if a group is in a queue. @@ -1545,12 +1623,39 @@ function FLIGHTCONTROL:GetActiveRunway() return rwy end +--- Get the active runway for landing. +-- @param #FLIGHTCONTROL self +-- @return Wrapper.Airbase#AIRBASE.Runway Active runway. +function FLIGHTCONTROL:GetActiveRunwayLanding() + local rwy=self.airbase:GetActiveRunwayLanding() + return rwy +end + +--- Get the active runway for takeoff. +-- @param #FLIGHTCONTROL self +-- @return Wrapper.Airbase#AIRBASE.Runway Active runway. +function FLIGHTCONTROL:GetActiveRunwayTakeoff() + local rwy=self.airbase:GetActiveRunwayTakeoff() + return rwy +end + + --- Get the name of the active runway. -- @param #FLIGHTCONTROL self +-- @param #boolean Takeoff If true, return takeoff runway name. Default is landing. -- @return #string Runway text, e.g. "31L" or "09". -function FLIGHTCONTROL:GetActiveRunwayText() - local rwy=self.airbase:GetRunwayName() - return rwy +function FLIGHTCONTROL:GetActiveRunwayText(Takeoff) + + local runway + if Takeoff then + runway=self:GetActiveRunwayTakeoff() + else + runway=self:GetActiveRunwayLanding() + end + + local name=self.airbase:GetRunwayName(runway) + + return name or "XX" end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1649,6 +1754,10 @@ function FLIGHTCONTROL:SetParkingFree(spot) spot.OccupiedBy=nil spot.ReservedBy=nil + -- Remove parking guard. + self:RemoveParkingGuard(spot) + + -- Update marker. self:UpdateParkingMarker(spot) end @@ -1880,8 +1989,13 @@ function FLIGHTCONTROL:_CreatePlayerMenu(flight, mainmenu) -- Are we controlling this flight. local gotcontrol=self:IsControlling(flight) + -- Get player element. + local player=flight:GetPlayerElement() + -- Debug info. - self:T(self.lid..string.format("Creating ATC player menu for flight %s: in state=%s status=%s, gotcontrol=%s", tostring(flight.groupname), flight:GetState(), flightstatus, tostring(gotcontrol))) + local text=string.format("Creating ATC player menu for flight %s: in state=%s status=%s, gotcontrol=%s, player=%s", + tostring(flight.groupname), flight:GetState(), flightstatus, tostring(gotcontrol), player.status) + self:T(self.lid..text) -- Airbase root menu. @@ -1893,7 +2007,6 @@ function FLIGHTCONTROL:_CreatePlayerMenu(flight, mainmenu) local helpmenu=MENU_GROUP:New(group, "Help", rootmenu) MENU_GROUP_COMMAND:New(group, "Radio Check", helpmenu, self._PlayerRadioCheck, self, groupname) MENU_GROUP_COMMAND:New(group, "Confirm Status", helpmenu, self._PlayerConfirmStatus, self, groupname) - MENU_GROUP_COMMAND:New(group, "Mark Holding", helpmenu, self._PlayerNotImplemented, self, groupname) if gotcontrol and flight:IsInbound() and flight.stack then MENU_GROUP_COMMAND:New(group, "Vector Holding", helpmenu, self._PlayerVectorInbound, self, groupname) end @@ -1917,7 +2030,7 @@ function FLIGHTCONTROL:_CreatePlayerMenu(flight, mainmenu) -- FC is controlling this flight --- - if flight:IsParking() then + if flight:IsParking(player) or player.status==OPSGROUP.ElementStatus.ENGINEON then --- -- Parking --- @@ -1928,7 +2041,7 @@ function FLIGHTCONTROL:_CreatePlayerMenu(flight, mainmenu) MENU_GROUP_COMMAND:New(group, "Request Taxi", rootmenu, self._PlayerRequestTaxi, self, groupname) end - elseif flight:IsTaxiing() then + elseif flight:IsTaxiing(player) then --- -- Taxiing --- @@ -1950,18 +2063,13 @@ function FLIGHTCONTROL:_CreatePlayerMenu(flight, mainmenu) MENU_GROUP_COMMAND:New(group, "Arrived at Parking", rootmenu, self._PlayerArrived, self, groupname) end - elseif flight:IsAirborne() then - --- - -- Airborne - --- - elseif flight:IsInbound() then --- -- Inbound --- - MENU_GROUP_COMMAND:New(group, "Holding", rootmenu, self._PlayerHolding, self, groupname) - MENU_GROUP_COMMAND:New(group, "Abort Inbound", rootmenu, self._PlayerAbortInbound, self, groupname) + MENU_GROUP_COMMAND:New(group, "Holding!", rootmenu, self._PlayerHolding, self, groupname) + MENU_GROUP_COMMAND:New(group, "Abort Inbound", rootmenu, self._PlayerAbortInbound, self, groupname) MENU_GROUP_COMMAND:New(group, "Request Parking", rootmenu, self._PlayerRequestParking, self, groupname) @@ -1970,19 +2078,19 @@ function FLIGHTCONTROL:_CreatePlayerMenu(flight, mainmenu) -- Holding --- - MENU_GROUP_COMMAND:New(group, "Landing", rootmenu, self._PlayerConfirmLanding, self, groupname) - MENU_GROUP_COMMAND:New(group, "Abort Holding", rootmenu, self._PlayerAbortHolding, self, groupname) - MENU_GROUP_COMMAND:New(group, "Request Parking", rootmenu, self._PlayerRequestParking, self, groupname) + MENU_GROUP_COMMAND:New(group, "Confirm Landing!", rootmenu, self._PlayerConfirmLanding, self, groupname) + MENU_GROUP_COMMAND:New(group, "Abort Holding", rootmenu, self._PlayerAbortHolding, self, groupname) + MENU_GROUP_COMMAND:New(group, "Request Parking", rootmenu, self._PlayerRequestParking, self, groupname) - elseif flight:IsLanding() then + elseif flight:IsLanding(player) then --- -- Landing --- - MENU_GROUP_COMMAND:New(group, "Abort Landing", rootmenu, self._PlayerAbortLanding, self, groupname) + MENU_GROUP_COMMAND:New(group, "Abort Landing", rootmenu, self._PlayerAbortLanding, self, groupname) MENU_GROUP_COMMAND:New(group, "Request Parking", rootmenu, self._PlayerRequestParking, self, groupname) - elseif flight:IsLanded() then + elseif flight:IsLanded(player) then --- -- Landed --- @@ -1990,6 +2098,24 @@ function FLIGHTCONTROL:_CreatePlayerMenu(flight, mainmenu) MENU_GROUP_COMMAND:New(group, "Arrived at Parking", rootmenu, self._PlayerArrived, self, groupname) MENU_GROUP_COMMAND:New(group, "Request Parking", rootmenu, self._PlayerRequestParking, self, groupname) + elseif flight:IsArrived(player) then + --- + -- Arrived (at Parking) + --- + + if status==FLIGHTCONTROL.FlightStatus.READYTX then + MENU_GROUP_COMMAND:New(group, "Abort Taxi", rootmenu, self._PlayerAbortTaxi, self, groupname) + else + MENU_GROUP_COMMAND:New(group, "Request Taxi", rootmenu, self._PlayerRequestTaxi, self, groupname) + end + + elseif flight:IsAirborne(player) then + --- + -- Airborne + --- + + -- Nothing to do. + end else @@ -2038,9 +2164,9 @@ function FLIGHTCONTROL:_PlayerRadioCheck(groupname) if flight then -- Call sign. - local callsign=flight:GetCallsignName() + local callsign=self:_GetCallsignName(flight) - -- Pilot calls inbound for landing. + -- Pilot radio check. local text=string.format("%s, %s, radio check %.3f", self.alias, callsign, self.frequency) -- Radio message. @@ -2069,9 +2195,9 @@ function FLIGHTCONTROL:_PlayerConfirmStatus(groupname) if flight then -- Call sign. - local callsign=flight:GetCallsignName() + local callsign=self:_GetCallsignName(flight) - -- Pilot calls inbound for landing. + -- Pilot requests status. local text=string.format("%s, %s, confirm my status", self.alias, callsign) -- Radio message. @@ -2122,7 +2248,8 @@ function FLIGHTCONTROL:_PlayerInfoAirbase(groupname) local text=string.format("Airbase %s Info:", self.airbasename) text=text..string.format("\nATC Status: %s", self:GetState()) text=text..string.format("\nFrequency: %.3f %s", self.frequency, UTILS.GetModulationName(self.modulation)) - text=text..string.format("\nActive Runway: %s", self:GetActiveRunwayText()) + text=text..string.format("\nRunway Landing: %s", self:GetActiveRunwayText()) + text=text..string.format("\nRunway Takeoff: %s", self:GetActiveRunwayText(true)) -- Message to flight self:TextMessageToFlight(text, flight, 10, true) @@ -2260,7 +2387,7 @@ function FLIGHTCONTROL:_PlayerRequestInbound(groupname) if flight:IsAirborne() then -- Call sign. - local callsign=flight:GetCallsignName() + local callsign=self:_GetCallsignName(flight) -- Get player element. local player=flight:GetPlayerElement() @@ -2302,11 +2429,11 @@ function FLIGHTCONTROL:_PlayerRequestInbound(groupname) local dist=UTILS.MetersToNM(distance) -- Message text. - local text=string.format("%s, %s, roger, fly heading %03d for %d nautical miles, hold at angels %d. Report status when entering the pattern", + local text=string.format("%s, %s, roger, fly heading %03d for %d nautical miles, hold at angels %d. Report entering the pattern.", callsign, self.alias, heading, dist, stack.angels) -- Send message. - self:TransmissionTower(text, flight, 15) + self:TransmissionTower(text, flight, 10) -- Set flightcontrol for this flight. This also updates the menu. flight:SetFlightControl(self) @@ -2365,7 +2492,7 @@ function FLIGHTCONTROL:_PlayerVectorInbound(groupname) if flight:IsInbound() and self:IsControlling(flight) and flight.stack then -- Call sign. - local callsign=flight:GetCallsignName() + local callsign=self:_GetCallsignName(flight) -- Get player element. local player=flight:GetPlayerElement() @@ -2377,7 +2504,7 @@ function FLIGHTCONTROL:_PlayerVectorInbound(groupname) local dist=flightcoord:Get2DDistance(self:GetCoordinate()) -- Call sign. - local callsign=flight:GetCallsignName() + local callsign=self:_GetCallsignName(flight) -- Heading to holding point. local heading=flightcoord:HeadingTo(flight.stack.pos0) @@ -2418,7 +2545,7 @@ function FLIGHTCONTROL:_PlayerAbortInbound(groupname) if flight:IsInbound() and self:IsControlling(flight) then -- Call sign. - local callsign=flight:GetCallsignName() + local callsign=self:_GetCallsignName(flight) -- Pilot calls inbound for landing. local text=string.format("%s, %s, abort inbound", self.alias, callsign) @@ -2439,12 +2566,15 @@ function FLIGHTCONTROL:_PlayerAbortInbound(groupname) else self:E(self.lid.."ERROR: No stack!") end - + -- Remove flight. This also updates the menu. self:_RemoveFlight(flight) -- Set flight to cruise. flight:Cruise() + + -- Current base is nil. + flight.currbase=nil -- Create player menu. --flight:_UpdateMenu() @@ -2483,7 +2613,7 @@ function FLIGHTCONTROL:_PlayerHolding(groupname) if self:IsControlling(flight) then -- Callsign. - local callsign=flight:GetCallsignName() + local callsign=self:_GetCallsignName(flight) -- Player element. local player=flight:GetPlayerElement() @@ -2493,7 +2623,7 @@ function FLIGHTCONTROL:_PlayerHolding(groupname) if stack then - -- Pilot calls inbound for landing. + -- Pilot arrived at holding pattern. local text=string.format("%s, %s, arrived at holding pattern", self.alias, callsign) -- Radio message. @@ -2505,13 +2635,15 @@ function FLIGHTCONTROL:_PlayerHolding(groupname) -- Distance. local dist=stack.pos0:Get2DDistance(Coordinate) - local dmax=UTILS.NMToMeters(5) + local dmax=UTILS.NMToMeters(500) if distself.speedLimitTaxi then + + local text="Slow down, you are too fast!" + + self:TransmissionTower(text, flight) + + end + + end + end + end + + end end @@ -3318,12 +3475,8 @@ function FLIGHTCONTROL:_LandAI(flight, parking) -- Debug info. self:T(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 - + + -- Respawn? local respawn=false if respawn then @@ -3399,6 +3552,44 @@ function FLIGHTCONTROL:_GetHoldingStack(flight) return nil end + +--- Count flights in holding pattern. +-- @param #FLIGHTCONTROL self +-- @param #FLIGHTCONTROL.HoldingPattern Pattern The pattern. +-- @return #FLIGHTCONTROL.HoldingStack Holding point. +function FLIGHTCONTROL:_CountFlightsInPattern(Pattern) + + local N=0 + + for _,_stack in pairs(Pattern.stacks) do + local stack=_stack --#FLIGHTCONTROL.HoldingStack + if stack.flightgroup then + N=N+1 + end + end + + return N +end + + +--- AI flight on final. +-- @param #FLIGHTCONTROL self +-- @param Ops.FlightGroup#FLIGHTGROUP flight Flight group. +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:_FlightOnFinal(flight) + + -- Callsign. + local callsign=self:_GetCallsignName(flight) + + -- Message text. + local text=string.format("%s, final", callsign) + + -- Transmit message. + self:TransmissionPilot(text, flight) + + return self +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Radio Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -3418,7 +3609,10 @@ function FLIGHTCONTROL:TransmissionTower(Text, Flight, Delay) -- "Subtitle". if Flight and not Flight.isAI then - self:TextMessageToFlight(Text, Flight, 5, false, Delay) + local playerData=Flight:_GetPlayerData() + if playerData.subtitles then + self:TextMessageToFlight(Text, Flight, 5, false, Delay) + end end -- Set time stamp. Can be in the future. @@ -3436,15 +3630,23 @@ end -- @param #number Delay Delay in seconds before the text is transmitted. Default 0 sec. function FLIGHTCONTROL:TransmissionPilot(Text, Flight, Delay) - -- Spoken text. - local text=self:_GetTextForSpeech(Text) + -- Get player data. + local playerData=Flight:_GetPlayerData() - -- Pilot radio call. - self.msrsPilot:PlayText(text, Delay) - - -- "Subtitle". - if Flight and not Flight.isAI then - self:TextMessageToFlight(Text, Flight, 5, false, Delay) + -- Check if player enabled his "voice". + if playerData==nil or playerData.myvoice then + + -- Spoken text. + local text=self:_GetTextForSpeech(Text) + + -- Pilot radio call. + self.msrsPilot:PlayText(text, Delay) + + -- "Subtitle". + if Flight and not Flight.isAI then + self:TextMessageToFlight(Text, Flight, 5, false, Delay) + end + end -- Set time stamp. @@ -3531,7 +3733,7 @@ function FLIGHTCONTROL:SpawnParkingGuard(unit) -- Turn AI Off. if self.parkingGuard:IsInstanceOf("SPAWN") then - self.parkingGuard:InitAIOff() + --self.parkingGuard:InitAIOff() end -- Group that is spawned. @@ -3560,6 +3762,26 @@ function FLIGHTCONTROL:RemoveParkingGuard(spot, delay) end +--- Check if a flight is on a runway +-- @param #FLIGHTCONTROL self +-- @param Ops.FlightGroup#FLIGHTGROUP flight +-- @param Wrapper.Airbase#AIRBASE.Runway Runway or nil. +function FLIGHTCONTROL:_IsFlightOnRunway(flight) + + for _,_runway in pairs(self.airbase.runways) do + local runway=_runway --Wrapper.Airbase#AIRBASE.Runway + + local inzone=flight:IsInZone(runway.zone) + + if inzone then + return runway + end + + end + + return nil +end + --- Get callsign name of a given flight. -- @param #FLIGHTCONTROL self -- @param Ops.FlightGroup#FLIGHTGROUP flight Flight group. @@ -3568,16 +3790,18 @@ function FLIGHTCONTROL:_GetCallsignName(flight) local callsign=flight:GetCallsignName() - local name=string.match(callsign, "%a+") - local number=string.match(callsign, "%d+") + --local name=string.match(callsign, "%a+") + --local number=string.match(callsign, "%d+") + return callsign end ---- Get text for text +--- Get text for text-to-speech. +-- Numbers are spaced out, e.g. "Heading 180" becomes "Heading 1 8 0 ". -- @param #FLIGHTCONTROL self -- @param #string text --- @return #string Callsign or "Ghostrider 1-1". +-- @return #string Spoken text. function FLIGHTCONTROL:_GetTextForSpeech(text) --- Function to space out text. @@ -3596,6 +3820,8 @@ function FLIGHTCONTROL:_GetTextForSpeech(text) -- Space out numbers. local t=text:gsub("(%d+)", space) + --TODO: 9 to niner. + return t end @@ -3634,6 +3860,79 @@ function FLIGHTCONTROL:_GetPlayerUnitAndName(unitName) return nil,nil end +--- Check holding pattern markers. Draw if they should exists and remove if they should not. +-- @param #FLIGHTCONTROL self +function FLIGHTCONTROL:_CheckMarkHoldingPatterns() + + for _,pattern in pairs(self.holdingpatterns) do + local Pattern=pattern + + if self.markPatterns then + + self:_MarkHoldingPattern(Pattern) + + else + + self:_UnMarkHoldingPattern(Pattern) + + end + + end + +end + +--- Draw marks of holding pattern (if they do not exist. +-- @param #FLIGHTCONTROL self +-- @param #FLIGHTCONTROL.HoldingPattern Pattern Holding pattern table. +function FLIGHTCONTROL:_MarkHoldingPattern(Pattern) + + if not Pattern.markArrow then + Pattern.markArrow=Pattern.pos0:ArrowToAll(Pattern.pos1, nil, {1,0,0}, 1, {1,1,0}, 0.5, 2, true) + end + + if not Pattern.markArrival then + Pattern.markArrival=Pattern.arrivalzone:DrawZone() + end + +end + +--- Removem markers of holding pattern (if they exist). +-- @param #FLIGHTCONTROL self +-- @param #FLIGHTCONTROL.HoldingPattern Pattern Holding pattern table. +function FLIGHTCONTROL:_UnMarkHoldingPattern(Pattern) + + if Pattern.markArrow then + UTILS.RemoveMark(Pattern.markArrow) + Pattern.markArrow=nil + end + + if Pattern.markArrival then + UTILS.RemoveMark(Pattern.markArrival) + Pattern.markArrival=nil + end + +end + +--- Add a holding pattern. +-- @param #FLIGHTCONTROL self +-- @return #FLIGHTCONTROL.HoldingPattern Holding pattern table. +function FLIGHTCONTROL:_AddHoldingPatternBackup() + + local runway=self:GetActiveRunway() + + local heading=runway.heading + + local vec2=self.airbase:GetVec2() + + local Vec2=UTILS.Vec2Translate(vec2, UTILS.NMToMeters(5), heading+90) + + local ArrivalZone=ZONE_RADIUS:New("Arrival Zone", Vec2, 5000) + + -- Add holding pattern with very low priority. + self.holdingBackup=self:AddHoldingPattern(ArrivalZone, heading, 15, 5, 25, 999) + + return self +end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index 79074f312..f6f375e06 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -143,6 +143,8 @@ FLIGHTGROUP = { menu = nil, isHelo = nil, RTBRecallCount = 0, + playerSettings = {}, + playerWarnings = {}, } @@ -183,21 +185,32 @@ FLIGHTGROUP.RadioMessage = { TAXIING={normal="Taxiing", enhanced="Taxiing"}, } ---- Player skill. +--- Skill level. -- @type FLIGHTGROUP.PlayerSkill --- @field #string NOVICE Novice +-- @field #string STUDENT Flight Student. Shows tips and hints in important phases of the approach. +-- @field #string AVIATOR Naval aviator. Moderate number of hints but not really zip lip. +-- @field #string GRADUATE TOPGUN graduate. For people who know what they are doing. Nearly *ziplip*. +-- @field #string INSTRUCTOR TOPGUN instructor. For people who know what they are doing. Nearly *ziplip*. FLIGHTGROUP.PlayerSkill = { - NOVICE="Novice", + STUDENT = "Student", + AVIATOR = "Aviator", + GRADUATE = "Graduate", + INSTRUCTOR = "Instructor", } ---- Player settings. --- @type FLIGHTGROUP.PlayerSettings +--- Player data. +-- @type FLIGHTGROUP.PlayerData +-- @type #string name Player name. -- @field #boolean subtitles Display subtitles. -- @field #string skill Skill level. +--- FLIGHTGROUP players. +-- @field #table Players Player data. +FLIGHTGROUP.Players={} + --- FLIGHTGROUP class version. -- @field #string version -FLIGHTGROUP.version="0.7.9" +FLIGHTGROUP.version="0.8.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -309,17 +322,18 @@ function FLIGHTGROUP:New(group) -- TODO: Add pseudo functions. -- Handle events: - self:HandleEvent(EVENTS.Birth, self.OnEventBirth) - self:HandleEvent(EVENTS.EngineStartup, self.OnEventEngineStartup) - self:HandleEvent(EVENTS.Takeoff, self.OnEventTakeOff) - self:HandleEvent(EVENTS.Land, self.OnEventLanding) - self:HandleEvent(EVENTS.EngineShutdown, self.OnEventEngineShutdown) - self:HandleEvent(EVENTS.PilotDead, self.OnEventPilotDead) - self:HandleEvent(EVENTS.Ejection, self.OnEventEjection) - self:HandleEvent(EVENTS.Crash, self.OnEventCrash) - self:HandleEvent(EVENTS.RemoveUnit, self.OnEventRemoveUnit) - self:HandleEvent(EVENTS.UnitLost, self.OnEventUnitLost) - self:HandleEvent(EVENTS.Kill, self.OnEventKill) + self:HandleEvent(EVENTS.Birth, self.OnEventBirth) + self:HandleEvent(EVENTS.EngineStartup, self.OnEventEngineStartup) + self:HandleEvent(EVENTS.Takeoff, self.OnEventTakeOff) + self:HandleEvent(EVENTS.Land, self.OnEventLanding) + self:HandleEvent(EVENTS.EngineShutdown, self.OnEventEngineShutdown) + self:HandleEvent(EVENTS.PilotDead, self.OnEventPilotDead) + self:HandleEvent(EVENTS.Ejection, self.OnEventEjection) + self:HandleEvent(EVENTS.Crash, self.OnEventCrash) + self:HandleEvent(EVENTS.RemoveUnit, self.OnEventRemoveUnit) + self:HandleEvent(EVENTS.UnitLost, self.OnEventUnitLost) + self:HandleEvent(EVENTS.Kill, self.OnEventKill) + self:HandleEvent(EVENTS.PlayerLeaveUnit, self.OnEventPlayerLeaveUnit) -- Init waypoints. self:_InitWaypoints() @@ -385,7 +399,7 @@ function FLIGHTGROUP:SetReadyForTakeoff(ReadyTO, Delay) return self end ---- Set the FLIGHTCONTROL controlling this flight group. Also updates the player menu after 0.5 sec. +--- Set the FLIGHTCONTROL controlling this flight group. -- @param #FLIGHTGROUP self -- @param Ops.FlightControl#FLIGHTCONTROL flightcontrol The FLIGHTCONTROL object. -- @return #FLIGHTGROUP self @@ -411,11 +425,6 @@ function FLIGHTGROUP:SetFlightControl(flightcontrol) table.insert(flightcontrol.flights, self) end - -- Update flight's F10 menu. - if not self.isAI then - self:_UpdateMenu(0.5) - end - return self end @@ -564,22 +573,34 @@ end --- Check if flight is parking. -- @param #FLIGHTGROUP self +-- @param Ops.OpsGroup#OPSGROUP.Element Element (Optional) Only check status for given element. -- @return #boolean If true, flight is parking after spawned. -function FLIGHTGROUP:IsParking() +function FLIGHTGROUP:IsParking(Element) + if Element then + return Element.status==OPSGROUP.ElementStatus.PARKING + end return self:Is("Parking") end --- Check if is taxiing to the runway. -- @param #FLIGHTGROUP self +-- @param Ops.OpsGroup#OPSGROUP.Element Element (Optional) Only check status for given element. -- @return #boolean If true, flight is taxiing after engine start up. -function FLIGHTGROUP:IsTaxiing() +function FLIGHTGROUP:IsTaxiing(Element) + if Element then + return Element.status==OPSGROUP.ElementStatus.TAXIING + end return self:Is("Taxiing") end --- Check if flight is airborne or cruising. -- @param #FLIGHTGROUP self +-- @param Ops.OpsGroup#OPSGROUP.Element Element (Optional) Only check status for given element. -- @return #boolean If true, flight is airborne. -function FLIGHTGROUP:IsAirborne() +function FLIGHTGROUP:IsAirborne(Element) + if Element then + return Element.status==OPSGROUP.ElementStatus.AIRBORNE + end return self:Is("Airborne") or self:Is("Cruising") end @@ -592,22 +613,34 @@ end --- Check if flight is landing. -- @param #FLIGHTGROUP self +-- @param Ops.OpsGroup#OPSGROUP.Element Element (Optional) Only check status for given element. -- @return #boolean If true, flight is landing, i.e. on final approach. -function FLIGHTGROUP:IsLanding() +function FLIGHTGROUP:IsLanding(Element) + if Element then + return Element.status==OPSGROUP.ElementStatus.LANDING + end return self:Is("Landing") end --- Check if flight has landed and is now taxiing to its parking spot. -- @param #FLIGHTGROUP self +-- @param Ops.OpsGroup#OPSGROUP.Element Element (Optional) Only check status for given element. -- @return #boolean If true, flight has landed -function FLIGHTGROUP:IsLanded() +function FLIGHTGROUP:IsLanded(Element) + if Element then + return Element.status==OPSGROUP.ElementStatus.LANDED + end return self:Is("Landed") end --- Check if flight has arrived at its destination parking spot. -- @param #FLIGHTGROUP self +-- @param Ops.OpsGroup#OPSGROUP.Element Element (Optional) Only check status for given element. -- @return #boolean If true, flight has arrived at its destination and is parking. -function FLIGHTGROUP:IsArrived() +function FLIGHTGROUP:IsArrived(Element) + if Element then + return Element.status==OPSGROUP.ElementStatus.ARRIVED + end return self:Is("Arrived") end @@ -639,9 +672,9 @@ function FLIGHTGROUP:IsLandingAt() return self:Is("LandingAt") end ---- Check if helo(!) flight is currently landed at a specific point. +--- Check if helo(!) flight has landed at a specific point. -- @param #FLIGHTGROUP self --- @return #boolean If true, group is currently landed at the assigned position and waiting until task is complete. +-- @return #boolean If true, has landed somewhere. function FLIGHTGROUP:IsLandedAt() return self:Is("LandedAt") end @@ -745,6 +778,9 @@ function FLIGHTGROUP:ClearToLand(Delay) self:T(self.lid..string.format("Clear to land ==> setting holding flag to 1 (true)")) self.flaghold:Set(1) + -- Not holding any more. + self.Tholding=nil + -- Clear holding stack. if self.stack then self.stack.flightgroup=nil @@ -1123,6 +1159,11 @@ function FLIGHTGROUP:OnEventEngineStartup(EventData) -- TODO: what? else self:T3(self.lid..string.format("EVENT: Element %s started engines ==> taxiing (if AI)", element.name)) + + -- Element started engies. + self:ElementEngineOn(element) + + --[[ -- TODO: could be that this element is part of a human flight group. -- Problem: when player starts hot, the AI does too and starts to taxi immidiately :( -- when player starts cold, ? @@ -1134,6 +1175,7 @@ function FLIGHTGROUP:OnEventEngineStartup(EventData) self:ElementEngineOn(element) end end + ]] end end @@ -1299,6 +1341,10 @@ function FLIGHTGROUP:onafterElementSpawned(From, Event, To, Element) -- Debug info. self:T(self.lid..string.format("Element spawned %s", Element.name)) + + if Element.playerName then + self:_InitPlayerData(Element.playerName) + end -- Set element status. self:_UpdateStatus(Element, OPSGROUP.ElementStatus.SPAWNED) @@ -1524,12 +1570,13 @@ end -- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. function FLIGHTGROUP:onafterElementDead(From, Event, To, Element) - -- Call OPSGROUP function. - self:GetParent(self).onafterElementDead(self, From, Event, To, Element) - + -- Check for flight control. if self.flightcontrol and Element.parking then self.flightcontrol:SetParkingFree(Element.parking) end + + -- Call OPSGROUP function. This will remove the flightcontrol. Therefore, has to be after setting parking free. + self:GetParent(self).onafterElementDead(self, From, Event, To, Element) -- Not parking any more. Element.parking=nil @@ -1628,8 +1675,6 @@ function FLIGHTGROUP:onafterSpawned(From, Event, To) self:__UpdateRoute(-0.5) else - - env.info("FF Spawned update menu") -- Set flightcontrol. if self.currbase then @@ -1687,15 +1732,10 @@ function FLIGHTGROUP:onafterParking(From, Event, To) -- Set flight status. self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.PARKING) - -- Update player menu. - if not self.isAI then - self:_UpdateMenu(0.5) - end - end else - self:E(self.lid.."ERROR: FF no flight control in onAfterParking!") + self:T3(self.lid.."INFO: No flight control in onAfterParking!") end end @@ -1719,9 +1759,6 @@ function FLIGHTGROUP:onafterTaxiing(From, Event, To) else -- Human flights go to TAXI OUT queue. They will go to the ready for takeoff queue when they request it. self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.TAXIOUT) - - -- Update menu. - self:_UpdateMenu() end end @@ -1802,8 +1839,23 @@ end function FLIGHTGROUP:onafterLanding(From, Event, To) self:T(self.lid..string.format("Flight is landing")) + -- Everyone is landing now. self:_SetElementStatusAll(OPSGROUP.ElementStatus.LANDING) + if self.flightcontrol and self.flightcontrol:IsControlling(self) then + -- Add flight to landing queue. + self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.LANDING) + end + + -- Not holding any more. + self.Tholding=nil + + -- Clear holding stack. + if self.stack then + self.stack.flightgroup=nil + self.stack=nil + end + end @@ -1816,7 +1868,7 @@ end function FLIGHTGROUP:onafterLanded(From, Event, To, airbase) self:T(self.lid..string.format("Flight landed at %s", airbase and airbase:GetName() or "unknown place")) - if self.flightcontrol and airbase and self.flightcontrol.airbasename==airbase:GetName() then + if self.flightcontrol and self.flightcontrol:IsControlling(self) then -- Add flight to taxiinb queue. self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.TAXIINB) end @@ -2613,7 +2665,7 @@ function FLIGHTGROUP:_LandAtAirbase(airbase, SpeedTo, SpeedHold, SpeedLand) local h2=x2*math.tan(alpha) -- Get active runway. - local runway=airbase:GetActiveRunway() + local runway=airbase:GetActiveRunwayLanding() -- Set holding flag to 0=false. self.flaghold:Set(0) @@ -2648,8 +2700,12 @@ function FLIGHTGROUP:_LandAtAirbase(airbase, SpeedTo, SpeedHold, SpeedLand) -- Airdrome --- + -- Call a function to tell everyone we are on final. + local TaskFinal = self.group:TaskFunction("FLIGHTGROUP._OnFinal", self) + + -- Final approach waypoint. local papp=airbase:GetCoordinate():Translate(x1, runway.heading-180):SetAltitude(h1) - wp[#wp+1]=papp:WaypointAirTurningPoint("BARO", UTILS.KnotsToKmph(SpeedLand), {}, "Final Approach") + wp[#wp+1]=papp:WaypointAirTurningPoint("BARO", UTILS.KnotsToKmph(SpeedLand), {TaskFinal}, "Final Approach") -- Okay, it looks like it's best to specify the coordinates not at the airbase but a bit away. This causes a more direct landing approach. local pland=airbase:GetCoordinate():Translate(x2, runway.heading-180):SetAltitude(h2) @@ -2855,13 +2911,9 @@ function FLIGHTGROUP:onafterHolding(From, Event, To) -- Add flight to waiting/holding queue. if self.flightcontrol then - -- Set flight status to holding + -- Set flight status to holding. self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.HOLDING) - if not self.isAI then - self:_UpdateMenu() - end - elseif self.airboss then if self.isHelo then @@ -3112,6 +3164,20 @@ function FLIGHTGROUP._ClearedToLand(group, flightgroup) flightgroup:__Landing(-1) end +--- Function called when flight is on final. +-- @param Wrapper.Group#GROUP group Group object. +-- @param #FLIGHTGROUP flightgroup Flight group object. +function FLIGHTGROUP._OnFinal(group, flightgroup) + flightgroup:T2(flightgroup.lid..string.format("Group on final approach")) + + local fc=flightgroup.flightcontrol + + if fc and fc:IsControlling(flightgroup) then + fc:_FlightOnFinal(flightgroup) + end + +end + --- Function called when flight finished refuelling. -- @param Wrapper.Group#GROUP group Group object. -- @param #FLIGHTGROUP flightgroup Flight group object. @@ -3667,6 +3733,20 @@ function FLIGHTGROUP:GetPlayerElement() return nil end +--- Get player element. +-- @param #FLIGHTGROUP self +-- @return #string Player name or `nil`. +function FLIGHTGROUP:GetPlayerName() + + local playerElement=self:GetPlayerElement() + + if playerElement then + return playerElement.playerName + end + + return nil +end + --- Set parking spot of element. -- @param #FLIGHTGROUP self -- @param Ops.OpsGroup#OPSGROUP.Element Element The element. @@ -4181,65 +4261,75 @@ function FLIGHTGROUP:_UpdateMenu(delay) -- Delayed call. self:ScheduleOnce(delay, FLIGHTGROUP._UpdateMenu, self) else - - -- Message to group. - MESSAGE:New("Updating MENU state="..self:GetState(), 5):ToGroup(self.group) - env.info(self.lid.."updating menu state="..self:GetState()) - + -- Player element. local player=self:GetPlayerElement() - - -- Get current position of player. - local position=self:GetCoordinate(nil, player.name) - - -- Get all FLIGHTCONTROLS - local fc={} - for airbasename,_flightcontrol in pairs(_DATABASE.FLIGHTCONTROLS) do - local flightcontrol=_flightcontrol --Ops.FlightControl#FLIGHTCONTROL - - -- Get coord of airbase. - local coord=flightcontrol:GetCoordinate() - - -- Distance to flight. - local dist=coord:Get2DDistance(position) - - -- Add to table. - table.insert(fc, {airbasename=airbasename, dist=dist}) - end - - -- Sort table wrt distance to airbases. - local function _sort(a,b) - return a.dist dead", element.name)) + self:ElementDead(element) + end + + end + +end + --- Event function handling the event that a unit achieved a kill. -- @param #OPSGROUP self -- @param Core.Event#EVENTDATA EventData Event data. function OPSGROUP:OnEventKill(EventData) + --self:I("FF event kill") + --self:I(EventData) -- Check that this is the right group. if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then @@ -7354,14 +7381,11 @@ function OPSGROUP:onafterDead(From, Event, To) -- All elements were destroyed ==> Asset group is gone. self.cohort:DelGroup(self.groupname) end - if self.legion then - --self.legion:Get - --self.legion:AssetDead() - end else -- Not all assets were destroyed (despawn) ==> Add asset back to legion? end + if self.legion then if not self:IsInUtero() then @@ -7377,6 +7401,10 @@ function OPSGROUP:onafterDead(From, Event, To) -- Stop in 5 sec to give possible respawn attempts a chance. self:__Stop(-5) + + elseif not self.isAI then + -- Stop player flights. + self:__Stop(-1) end end @@ -11679,15 +11707,15 @@ function OPSGROUP:SwitchCallsign(CallsignName, CallsignNumber) return self end ---- Get callsign +--- Get callsign of the first element alive. -- @param #OPSGROUP self --- @return #string Callsign name, e.g. Uzi-1 +-- @return #string Callsign name, e.g. Uzi11, or "Ghostrider11". function OPSGROUP:GetCallsignName() local element=self:GetElementAlive() if element then - env.info("FF callsign "..tostring(element.callsign)) + self:T2(self.lid..string.format("Callsign %s", tostring(element.callsign))) return element.callsign end @@ -11710,7 +11738,7 @@ function OPSGROUP:GetCallsignName() ]] - return callsign + return "Ghostrider11" end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -12666,6 +12694,7 @@ function OPSGROUP:_AddElementByName(unitname) if element.skill=="Client" or element.skill=="Player" then element.ai=false element.client=CLIENT:FindByName(unitname) + element.playerName=element.DCSunit:getPlayerName() else element.ai=true end diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index c6fc961d8..92a7bf210 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -25,10 +25,11 @@ -- @field #boolean isShip Airbase is a ship. -- @field #table parking Parking spot data. -- @field #table parkingByID Parking spot data table with ID as key. --- @field #number activerwyno Active runway number (forced). -- @field #table parkingWhitelist List of parking spot terminal IDs considered for spawning. -- @field #table parkingBlacklist List of parking spot terminal IDs **not** considered for spawning. -- @field #table runways Runways of airdromes. +-- @field #AIRBASE.Runway runwayLanding Runway used for landing. +-- @field #AIRBASE.Runway runwayTakeoff Runway used for takeoff. -- @extends Wrapper.Positionable#POSITIONABLE --- Wrapper class to handle the DCS Airbase objects: @@ -70,7 +71,6 @@ AIRBASE = { [Airbase.Category.HELIPAD] = "Helipad", [Airbase.Category.SHIP] = "Ship", }, - activerwyno=nil, } --- Enumeration to identify the airbases in the Caucasus region. @@ -612,6 +612,9 @@ function AIRBASE:Register(AirbaseName) -- Init Runways. self:_InitRunways() + + -- Set the active runways based on wind direction. + self:SetActiveRunway() -- Init parking spots. self:_InitParkingSpots() @@ -1531,6 +1534,33 @@ function AIRBASE:GetRunways() return self.runways or {} end +--- Get runway by its name. +-- @param #AIRBASE self +-- @param #string Name Name of the runway, e.g. "31" or "21L". +-- @return #AIRBASE.Runway Runway data. +function AIRBASE:GetRunwayByName(Name) + + if Name==nil then + return + end + + if Name then + for _,_runway in pairs(self.runways) do + local runway=_runway --#AIRBASE.Runway + + -- Name including L or R, e.g. "31L". + local name=self:GetRunwayName(runway) + + if name==Name:upper() then + return runway + end + end + end + + self:E("ERROR: Could not find runway with name "..tostring(Name)) + return nil +end + --- Init runways. -- @param #AIRBASE self -- @param #boolean IncludeInverse If `true` or `nil`, include inverse runways. @@ -1906,30 +1936,101 @@ function AIRBASE:GetRunwayData(magvar, mark) return runways end ---- Set the active runway in case it cannot be determined by the wind direction. +--- Set the active runway for landing and takeoff. -- @param #AIRBASE self --- @param #number iactive Number of the active runway in the runway data table. -function AIRBASE:SetActiveRunway(iactive) - self.activerwyno=iactive +-- @param #string Name Name of the runway, e.g. "31" or "02L" or "90R". If not given, the runway is determined from the wind direction. +-- @param #boolean PreferLeft If `true`, perfer the left runway. If `false`, prefer the right runway. If `nil` (default), do not care about left or right. +function AIRBASE:SetActiveRunway(Name, PreferLeft) + + self:SetActiveRunwayTakeoff(Name, PreferLeft) + + self:SetActiveRunwayLanding(Name,PreferLeft) + end ---- Get the active runway based on current wind direction. +--- Set the active runway for landing. -- @param #AIRBASE self --- @param #number magvar (Optional) Magnetic variation in degrees. --- @return #AIRBASE.Runway Active runway data table. -function AIRBASE:GetActiveRunway(magvar) +-- @param #string Name Name of the runway, e.g. "31" or "02L" or "90R". If not given, the runway is determined from the wind direction. +-- @param #boolean PreferLeft If `true`, perfer the left runway. If `false`, prefer the right runway. If `nil` (default), do not care about left or right. +-- @return #AIRBASE.Runway The active runway for landing. +function AIRBASE:SetActiveRunwayLanding(Name, PreferLeft) - -- Get runways data (initialize if necessary). - --local runways=self:GetRunwayData(magvar) + local runway=self:GetRunwayByName(Name) + + if not runway then + runway=self:GetRunwayIntoWind(PreferLeft) + end + + if runway then + self:I("Setting active runway for landing as "..self:GetRunwayName(runway)) + else + self:E("ERROR: Could not set the runway for landing!") + end + + self.runwayLanding=runway + + return runway +end + +--- Get the active runways. +-- @param #AIRBASE self +-- @return #AIRBASE.Runway The active runway for landing. +-- @return #AIRBASE.Runway The active runway for takeoff. +function AIRBASE:GetActiveRunway() + return self.runwayLanding, self.runwayTakeoff +end + + +--- Get the active runway for landing. +-- @param #AIRBASE self +-- @return #AIRBASE.Runway The active runway for landing. +function AIRBASE:GetActiveRunwayLanding() + return self.runwayLanding +end + +--- Get the active runway for takeoff. +-- @param #AIRBASE self +-- @return #AIRBASE.Runway The active runway for takeoff. +function AIRBASE:GetActiveRunwayTakeoff() + return self.runwayTakeoff +end + + +--- Set the active runway for takeoff. +-- @param #AIRBASE self +-- @param #string Name Name of the runway, e.g. "31" or "02L" or "90R". If not given, the runway is determined from the wind direction. +-- @param #boolean PreferLeft If `true`, perfer the left runway. If `false`, prefer the right runway. If `nil` (default), do not care about left or right. +-- @return #AIRBASE.Runway The active runway for landing. +function AIRBASE:SetActiveRunwayTakeoff(Name, PreferLeft) + + local runway=self:GetRunwayByName(Name) + + if not runway then + runway=self:GetRunwayIntoWind(PreferLeft) + end + + if runway then + self:I("Setting active runway for takeoff as "..self:GetRunwayName(runway)) + else + self:E("ERROR: Could not set the runway for takeoff!") + end + + self.runwayTakeoff=runway + + return runway +end + + +--- Get the runway where aircraft would be taking of or landing into the direction of the wind. +-- NOTE that this requires the wind to be non-zero as set in the mission editor. +-- @param #AIRBASE self +-- @param #boolean PreferLeft If `true`, perfer the left runway. If `false`, prefer the right runway. If `nil` (default), do not care about left or right. +-- @return #AIRBASE.Runway Active runway data table. +function AIRBASE:GetRunwayIntoWind(PreferLeft) -- Get runway data. local runways=self:GetRunways() - -- Return user forced active runway if it was set. - if self.activerwyno then - return runways[self.activerwyno] - end - -- Get wind vector. local Vwind=self:GetCoordinate():GetWindWithTurbulenceVec3() local norm=UTILS.VecNorm(Vwind) @@ -1949,39 +2050,43 @@ function AIRBASE:GetActiveRunway(magvar) local dotmin=nil for i,_runway in pairs(runways) do local runway=_runway --#AIRBASE.Runway + + if PreferLeft==nil or PreferLeft==runway.isLeft then - -- Angle in rad. - local alpha=math.rad(runway.heading) - - -- Runway vector. - local Vrunway={x=math.cos(alpha), y=0, z=math.sin(alpha)} - - -- Dot product: parallel component of the two vectors. - local dot=UTILS.VecDot(Vwind, Vrunway) - - -- New min? - if dotmin==nil or dot Date: Tue, 28 Jun 2022 18:55:43 +0200 Subject: [PATCH 15/20] OPS - Improved FLIGHTCONTROL and minor other classes --- .../Moose/Functional/Warehouse.lua | 1 - Moose Development/Moose/Ops/FlightControl.lua | 1011 ++++++++++++++--- Moose Development/Moose/Ops/FlightGroup.lua | 128 ++- Moose Development/Moose/Ops/Legion.lua | 10 +- Moose Development/Moose/Ops/Operation.lua | 63 +- Moose Development/Moose/Ops/OpsGroup.lua | 32 +- Moose Development/Moose/Sound/SRS.lua | 60 +- Moose Development/Moose/Wrapper/Airbase.lua | 26 +- 8 files changed, 1047 insertions(+), 284 deletions(-) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 10eb19150..7b51d8da1 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -5376,7 +5376,6 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param DCS#coalition.side Coalition Coalition side which originally captured the warehouse. function WAREHOUSE:onafterRunwayDestroyed(From, Event, To) -- Message. diff --git a/Moose Development/Moose/Ops/FlightControl.lua b/Moose Development/Moose/Ops/FlightControl.lua index 845716cff..39e23ebc5 100644 --- a/Moose Development/Moose/Ops/FlightControl.lua +++ b/Moose Development/Moose/Ops/FlightControl.lua @@ -4,9 +4,9 @@ -- -- * Manage aircraft departure and arrival -- * Handles AI and human players --- * Limit number of AI groups taxiing and landing simultaniously +-- * Limit number of AI groups taxiing, taking off and landing simultaniously -- * Immersive voice overs via SRS text-to-speech --- * Define holding zones for airdromes +-- * Define holding patterns for airdromes -- -- === -- @@ -57,6 +57,9 @@ -- @field #number dTmessage Time interval between messages. -- @field #boolean markPatterns If `true`, park holding pattern. -- @field #number speedLimitTaxi Taxi speed limit in m/s. +-- @field #number runwaydestroyed Time stamp (abs), when runway was destroyed. If `nil`, runway is operational. +-- @field #number runwayrepairtime Time in seconds until runway will be repaired after it was destroyed. Default is 3600 sec (one hour). +-- @field #boolean markerParking If `true`, occupied parking spots are marked. -- @extends Core.Fsm#FSM --- **Ground Control**: Airliner X, Good news, you are clear to taxi to the active. @@ -122,19 +125,129 @@ -- -- # Parking Guard -- --- # Taxi Limits +-- A "parking guard" is a group or static object, that is spawned in front of parking aircraft. This is useful to stop AI groups from taxiing if they are spawned with hot engines. +-- It is also handy to forbid human players to taxi until they ask for clearance. +-- +-- You can activate the parking guard with the @{#FLIGHTCONTROL.SetParkingGuard}(*GroupName*) function, where the parameter `GroupName` is the name of a late activated template group. +-- This should consist of only *one* unit, *e.g.* a single infantry soldier. +-- +-- You can also use static objects as parking guards with the @{#FLIGHTCONTROL.SetParkingGuardStatic}(*StaticName*), where the parameter `StaticName` is the name of a static object placed +-- somewhere in the mission editor. +-- +-- # Limits for Inbound and Outbound Flights -- -- You can define limits on how many aircraft are simultaniously landing and taking off. This avoids (DCS) problems where taxiing aircraft cause a "traffic jam" on the taxi way(s) -- and bring the whole airbase effectively to a stand still. -- -- ## Landing Limits -- +-- The number of groups getting landing clearance can be set with the @{#FLIGHTCONTROL.SetLimitLanding}(*Nlanding, Ntakeoff*) function. +-- The first parameter, `Nlanding`, defines how many groups get clearance simultaniously. -- --- ## Takeoff Limits +-- The second parameter, `Ntakeoff`, sets a limit on how many flights can take off whilst inbound flights still get clearance. By default, this is set to zero because the runway can only be used for takeoff *or* +-- landing. So if you have a flight taking off, inbound fights will have to wait until the runway is clear. +-- If you have an airport with more than one runway, *e.g.* Nellis AFB, you can allow simultanious landings and takeoffs by setting this number greater zero. +-- +-- The time interval between clerances can be set with the @{#FLIGHTCONTROL.SetLandingInterval}(`dt`) function, where the parameter `dt` specifies the time interval in seconds before +-- the next flight get clearance. This only has an effect if `Nlanding` is greater than one. +-- +-- ## Taxiing/Takeoff Limits +-- +-- The number of AI flight groups getting clearance to taxi to the runway can be set with the @{#FLIGHTCONTROL.SetLimitTaxi}(*Nlanding, Ntakeoff*) function. +-- The first parameter, `Ntaxi`, defines how many groups are allowed to taxi to the runway simultaniously. Note that once the AI starts to taxi, we loose complete control over it. +-- They will follow their internal logic to get the the runway and take off. Therefore, giving clearance to taxi is equivalent to giving them clearance for takeoff. +-- +-- By default, the parameter only counts the number of flights taxiing *to* the runway. If you set the second parameter, `IncludeInbound`, to `true`, this will also count the flights +-- that are taxiing to their parking spot(s) after they landed. +-- +-- The third parameter, `Nlanding`, defines how many aircraft can land whilst outbound fights still get taxi/takeoff clearance. By default, this is set to zero because the runway +-- can only be used for takeoff *or* landing. If you have an airport with more than one runway, *e.g.* Nellis AFB, you can allow aircraft to taxi to the runway while other flights are landing +-- by setting this number greater zero. -- --- Note that the limits here are only affecting AI aircraft groups. Human players are assumed to be a lot more well behaved and capable as they are able to taxi around obstacles, *e.g.* --- other aircraft etc. +-- Note that the limits here are only affecting **AI** aircraft groups. *Human players* are assumed to be a lot more well behaved and capable as they are able to taxi around obstacles, *e.g.* +-- other aircraft etc. Therefore, players will get taxi clearance independent of the number of inbound and/or outbound flights. Players will, however, still need to ask for takeoff clearance once +-- they are holding short of the runway. -- +-- # Speeding Violations +-- +-- You can set a speed limit for taxiing players with the @{#FLIGHTCONTROL.SetSpeedLimitTaxi}(*SpeedLimit*) function, where the parameter `SpeedLimit` is the max allowed speed in knots. +-- If players taxi faster, they will get a radio message. Additionally, the FSM event `PlayerSpeeding` is triggered and can be captured with the `OnAfterPlayerSpeeding` function. +-- For example, this can be used to kick players that do not behave well. +-- +-- # Runway Destroyed +-- +-- Once a runway is damaged, DCS AI will stop taxiing. Therefore, this class monitors if a runway is destroyed. If this is the case, all AI taxi and landing clearances will be suspended for +-- one hour. This is the hard coded time in DCS until the runway becomes operational again. If that ever changes, you can manually set the repair time with the +-- @{#FLIGHTCONTROL.SetRunwayRepairtime} function. +-- +-- Note that human players we still get taxi, takeoff and landing clearances. +-- +-- If the runway is destroyed, the FSM event `RunwayDestroyed` is triggered and can be captured with the @{#FLIGHTCONTROL.OnAfterRunwayDestroyed} function. +-- +-- If the runway is repaired, the FSM event `RunwayRepaired` is triggered and can be captured with the @{#FLIGHTCONTROL.OnAfterRunwayRepaired} function. +-- +-- # SRS +-- +-- SRS text-to-speech is used to send radio messages from the tower and pilots. +-- +-- ## Tower +-- +-- You can set the options for the tower SRS voice with the @{#FLIGHTCONTROL.SetSRSTower}() function. +-- +-- ## Pilot +-- +-- You can set the options for the pilot SRS voice with the @{#FLIGHTCONTROL.SetSRSPilot}() function. +-- +-- # Runways +-- +-- First note, that we have extremely limited control over which runway the DCS AI groups use. The only parameter we can adjust is the direction of the wind. In many cases, the AI will try to takeoff and land +-- against the wind, which therefore determines the active runway. There are, however, cases where this does not hold true. For example, at Nellis AFB the runway for takeoff is `03L` while the runway for +-- landing is `21L`. +-- +-- By default, the runways for landing and takeoff are determined from the wind direction as described above. For cases where this gives wrong results, you can set the active runways manually. This is +-- done via @{Wrappper.Airbase#AIRBASE} class. +-- +-- More specifically, you can use the @{Wrappper.Airbase#AIRBASE.SetActiveRunwayLanding} function to set the landing runway and the @{Wrappper.Airbase#AIRBASE.SetActiveRunwayTakeoff} function to set +-- the runway for takeoff. +-- +-- ## Example for Nellis AFB +-- +-- For Nellis, you can use: +-- +-- -- Nellis AFB. +-- local Nellis=AIRBASE:FindByName(AIRBASE.Nevada.Nellis_AFB) +-- Nellis:SetActiveRunwayLanding("21L") +-- Nellis:SetActiveRunwayTakeoff("03L") +-- +-- # DCS ATC +-- +-- You can disable the DCS ATC with the @{Wrappper.Airbase#AIRBASE.SetRadioSilentMode}(*true*). This does not remove the DCS ATC airbase from the F10 menu but makes the ATC unresponsive. +-- +-- +-- # Examples +-- +-- In this section, you find examples for different airdromes. +-- +-- ## Nellis AFB +-- +-- -- Create a new FLIGHTCONTROL object at Nellis AFB. The tower frequency is 251 MHz AM. Path to SRS has to be adjusted. +-- local atcNellis=FLIGHTCONTROL:New(AIRBASE.Nevada.Nellis_AFB, 251, nil, "D:\\My SRS Directory") +-- -- Set a parking guard from a static named "Static Generator F Template". +-- atcNellis:SetParkingGuardStatic("Static Generator F Template") +-- -- Set taxi speed limit to 25 knots. +-- atcNellis:SetSpeedLimitTaxi(25) +-- -- Set that max 3 groups are allowed to taxi simultaniously. +-- atcNellis:SetLimitTaxi(3, false, 1) +-- -- Set that max 2 groups are allowd to land simultaniously and unlimited number (99) groups can land, while other groups are taking off. +-- atcNellis:SetLimitLanding(2, 99) +-- -- Use Google for text-to-speech. +-- atcNellis:SetSRSTower(nil, nil, "en-AU-Standard-A", nil, nil, "D:\\Path To Google\\GoogleCredentials.json") +-- atcNellis:SetSRSPilot(nil, nil, "en-US-Wavenet-I", nil, nil, "D:\\Path To Google\\GoogleCredentials.json") +-- -- Define two holding zones. +-- atcNellis:AddHoldingPattern(ZONE:New("Nellis Holding Alpha"), 030, 15, 6, 10, 10) +-- atcNellis:AddHoldingPattern(ZONE:New("Nellis Holding Bravo"), 090, 15, 6, 10, 20) +-- -- Start the ATC. +-- atcNellis:Start() -- -- @field #FLIGHTCONTROL FLIGHTCONTROL = { @@ -213,18 +326,18 @@ FLIGHTCONTROL.FlightStatus={ --- FlightControl class version. -- @field #string version -FLIGHTCONTROL.version="0.6.0" +FLIGHTCONTROL.version="0.7.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list --- TODO: Runway destroyed. -- TODO: Switch to enable/disable AI messages. --- TODO: Improve ATC TTS messages. -- TODO: Talk me down option. --- TODO: ATIS option. -- TODO: Check runways and clean up. -- TODO: Add FARPS? +-- DONE: Improve ATC TTS messages. +-- DONE: ATIS option. +-- DONE: Runway destroyed. -- DONE: Accept and forbit parking spots. DONE via AIRBASE black/white lists and airwing features. -- DONE: Support airwings. Dont give clearance for Alert5 or if mission has not started. -- DONE: Define holding zone. @@ -286,20 +399,19 @@ function FLIGHTCONTROL:New(AirbaseName, Frequency, Modulation, PathToSRS) -- Defaults: self:SetLimitLanding(2, 0) - self:SetLimitTaxi(1, false, 0) + self:SetLimitTaxi(2, false, 0) self:SetLandingInterval() self:SetFrequency(Frequency, Modulation) self:SetMarkHoldingPattern(true) + self:SetRunwayRepairtime() -- SRS for Tower. self.msrsTower=MSRS:New(PathToSRS, Frequency, Modulation) - self.msrsTower:SetLabel(self.alias) + self:SetSRSTower() -- SRS for Pilot. self.msrsPilot=MSRS:New(PathToSRS, Frequency, Modulation) - self.msrsPilot:SetGender("male") - self.msrsPilot:SetCulture("en-US") - self.msrsPilot:SetLabel("Pilot") + self:SetSRSPilot() -- Wait at least 10 seconds after last radio message before calling the next status update. self.dTmessage=10 @@ -314,6 +426,9 @@ function FLIGHTCONTROL:New(AirbaseName, Frequency, Modulation, PathToSRS) self:AddTransition("*", "PlayerKilledGuard", "*") -- Player killed parking guard self:AddTransition("*", "PlayerSpeeding", "*") -- Player speeding on taxi way. + + self:AddTransition("*", "RunwayDestroyed", "*") -- Runway of the airbase was destroyed. + self:AddTransition("*", "RunwayRepaired", "*") -- Runway of the airbase was repaired. self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. @@ -354,6 +469,61 @@ function FLIGHTCONTROL:New(AirbaseName, Frequency, Modulation, PathToSRS) -- @param #FLIGHTCONTROL self -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "RunwayDestroyed". + -- @function [parent=#FLIGHTCONTROL] RunwayDestroyed + -- @param #FLIGHTCONTROL self + + --- Triggers the FSM event "RunwayDestroyed" after a delay. + -- @function [parent=#FLIGHTCONTROL] __RunwayDestroyed + -- @param #FLIGHTCONTROL self + -- @param #number delay Delay in seconds. + + --- On after "RunwayDestroyed" event. + -- @function [parent=#FLIGHTCONTROL] OnAfterRunwayDestroyed + -- @param #FLIGHTCONTROL self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "RunwayRepaired". + -- @function [parent=#FLIGHTCONTROL] RunwayRepaired + -- @param #FLIGHTCONTROL self + + --- Triggers the FSM event "RunwayRepaired" after a delay. + -- @function [parent=#FLIGHTCONTROL] __RunwayRepaired + -- @param #FLIGHTCONTROL self + -- @param #number delay Delay in seconds. + + --- On after "RunwayRepaired" event. + -- @function [parent=#FLIGHTCONTROL] OnAfterRunwayRepaired + -- @param #FLIGHTCONTROL self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "PlayerSpeeding". + -- @function [parent=#FLIGHTCONTROL] PlayerSpeeding + -- @param #FLIGHTCONTROL self + -- @param Ops.FlightGroup#FLIGHTGROUP.PlayerData Player data. + + --- Triggers the FSM event "PlayerSpeeding" after a delay. + -- @function [parent=#FLIGHTCONTROL] __PlayerSpeeding + -- @param #FLIGHTCONTROL self + -- @param #number delay Delay in seconds. + -- @param Ops.FlightGroup#FLIGHTGROUP.PlayerData Player data. + + --- On after "PlayerSpeeding" event. + -- @function [parent=#FLIGHTCONTROL] OnAfterPlayerSpeeding + -- @param #FLIGHTCONTROL self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.FlightGroup#FLIGHTGROUP.PlayerData Player data. + + return self end @@ -393,6 +563,72 @@ function FLIGHTCONTROL:SetFrequency(Frequency, Modulation) return self end +--- Set SRS options for a given MSRS object. +-- @param #FLIGHTCONTROL self +-- @param Sound.SRS#MSRS msrs Moose SRS object. +-- @param #string Gender Gender: "male" or "female" (default). +-- @param #string Culture Culture, e.g. "en-GB" (default). +-- @param #string Voice Specific voice. Overrides `Gender` and `Culture`. +-- @param #number Volume Volume. Default 1.0. +-- @param #string Label Name under which SRS transmitts. +-- @param #string PathToGoogleCredentials Path to google credentials json file. +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:_SetSRSOptions(msrs, Gender, Culture, Voice, Volume, Label, PathToGoogleCredentials) + + -- Defaults: + Gender=Gender or "female" + Culture=Culture or "en-GB" + Volume=Volume or 1.0 + + if msrs then + msrs:SetGender(Gender) + msrs:SetCulture(Culture) + msrs:SetVoice(Voice) + msrs:SetVolume(Volume) + msrs:SetLabel(Label) + msrs:SetGoogle(PathToGoogleCredentials) + end + + return self +end + +--- Set SRS options for tower voice. +-- @param #FLIGHTCONTROL self +-- @param #string Gender Gender: "male" or "female" (default). +-- @param #string Culture Culture, e.g. "en-GB" (default). +-- @param #string Voice Specific voice. Overrides `Gender` and `Culture`. See [Google Voices](https://cloud.google.com/text-to-speech/docs/voices). +-- @param #number Volume Volume. Default 1.0. +-- @param #string Label Name under which SRS transmitts. Default `self.alias`. +-- @param #string PathToGoogleCredentials Path to google credentials json file. +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:SetSRSTower(Gender, Culture, Voice, Volume, Label, PathToGoogleCredentials) + + if self.msrsTower then + self:_SetSRSOptions(self.msrsTower, Gender or "female", Culture or "en-GB", Voice, Volume, Label or self.alias, PathToGoogleCredentials) + end + + return self +end + +--- Set SRS options for pilot voice. +-- @param #FLIGHTCONTROL self +-- @param #string Gender Gender: "male" (default) or "female". +-- @param #string Culture Culture, e.g. "en-US" (default). +-- @param #string Voice Specific voice. Overrides `Gender` and `Culture`. +-- @param #number Volume Volume. Default 1.0. +-- @param #string Label Name under which SRS transmitts. Default "Pilot". +-- @param #string PathToGoogleCredentials Path to google credentials json file. +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:SetSRSPilot(Gender, Culture, Voice, Volume, Label, PathToGoogleCredentials) + + if self.msrsPilot then + self:_SetSRSOptions(self.msrsPilot, Gender or "male", Culture or "en-US", Voice, Volume, Label or "Pilot", PathToGoogleCredentials) + end + + return self +end + + --- Set the number of aircraft groups, that are allowed to land simultaniously. -- Note that this restricts AI and human players. -- @@ -552,7 +788,7 @@ end function FLIGHTCONTROL:SetSpeedLimitTaxi(SpeedLimit) if SpeedLimit then - self.speedLimitTaxi=UTILS.MpsToKnots(SpeedLimit) + self.speedLimitTaxi=UTILS.KnotsToMps(SpeedLimit) else self.speedLimitTaxi=nil end @@ -619,6 +855,58 @@ function FLIGHTCONTROL:GetCountry() return self.airbase:GetCountry() end +--- Set the time until the runway(s) of an airdrome are repaired after it has been destroyed. +-- Note that this is the time, the DCS engine uses not something we can control on a user level or we could get via scripting. +-- You need to input the value. On the DCS forum it was stated that this is currently one hour. Hence this is the default value. +-- @param #FLIGHTCONTROL self +-- @param #number RepairTime Time in seconds until the runway is repaired. Default 3600 sec (one hour). +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:SetRunwayRepairtime(RepairTime) + self.runwayrepairtime=RepairTime or 3600 + return self +end + +--- Check if runway is operational. +-- @param #FLIGHTCONTROL self +-- @return #number Time in seconds until the runway is repaired. Will return 0 if runway is repaired. +function FLIGHTCONTROL:GetRunwayRepairtime() + if self.runwaydestroyed then + local Tnow=timer.getAbsTime() + local Tsince=Tnow-self.runwaydestroyed + local Trepair=math.max(self.runwayrepairtime-Tsince, 0) + return Trepair + end + return 0 +end + +--- Check if runway is operational. +-- @param #FLIGHTCONTROL self +-- @return #boolean If `true`, runway is operational. +function FLIGHTCONTROL:IsRunwayOperational() + if self.airbase then + if self.runwaydestroyed then + return false + else + return true + end + end + return nil +end + +--- Check if runway is destroyed. +-- @param #FLIGHTCONTROL self +-- @return #boolean If `true`, runway is destroyed. +function FLIGHTCONTROL:IsRunwayDestroyed() + if self.airbase then + if self.runwaydestroyed then + return true + else + return false + end + end + return nil +end + --- Is flight in queue of this flightcontrol. -- @param #FLIGHTCONTROL self -- @param Ops.FlightGroup#FLIGHTGROUP Flight Flight group. @@ -635,6 +923,28 @@ function FLIGHTCONTROL:IsFlight(Flight) return false end +--- Check if coordinate is on runway. +-- @param #FLIGHTCONTROL self +-- @param Core.Point#COORDINATE Coordinate +-- @return #boolean If `true`, coordinate is on a runway. +function FLIGHTCONTROL:IsCoordinateRunway(Coordinate) + + -- Get runways. + local runways=self.airbase:GetRunways() + + -- Check all runways. + for _,_runway in pairs(runways) do + local runway=_runway --Wrapper.Airbase#AIRBASE.Runway + + -- Check if coordinate is in zone. + if runway.zone:IsCoordinateInZone(Coordinate) then + return true + end + end + + return false +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Status ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -655,16 +965,17 @@ function FLIGHTCONTROL:onafterStart() self:HandleEvent(EVENTS.Takeoff) self:HandleEvent(EVENTS.Land) self:HandleEvent(EVENTS.EngineShutdown) - self:HandleEvent(EVENTS.Crash) + self:HandleEvent(EVENTS.Crash, FLIGHTCONTROL.OnEventCrashOrDead) + self:HandleEvent(EVENTS.Dead, FLIGHTCONTROL.OnEventCrashOrDead) self:HandleEvent(EVENTS.Kill) -- Init status updates. self:__StatusUpdate(-1) end ---- Update status. +--- On Before Update status. -- @param #FLIGHTCONTROL self -function FLIGHTCONTROL:onbeforeStatus() +function FLIGHTCONTROL:onbeforeStatusUpdate() if self.Tlastmessage then local Tnow=timer.getAbsTime() @@ -682,7 +993,7 @@ function FLIGHTCONTROL:onbeforeStatus() self:T(self.lid..text) -- Call status again in dt seconds. - self:__Status(-dt) + self:__StatusUpdate(-dt) -- Deny transition. return false @@ -703,6 +1014,15 @@ function FLIGHTCONTROL:onafterStatusUpdate() -- Check markers of holding patterns. self:_CheckMarkHoldingPatterns() + + -- Check if runway was repaired. + if self:IsRunwayOperational()==false then + local Trepair=self:GetRunwayRepairtime() + self:I(self.lid..string.format("Runway still destroyed! Will be repaired in %d sec", Trepair)) + if Trepair==0 then + self:RunwayRepaired() + end + end -- Check status of all registered flights. self:_CheckFlights() @@ -839,6 +1159,24 @@ function FLIGHTCONTROL:OnEventBirth(EventData) end +--- Event handling function. +-- @param #FLIGHTCONTROL self +-- @param Core.Event#EVENTDATA EventData Event data. +function FLIGHTCONTROL:OnEventCrashOrDead(EventData) + + if EventData then + + -- Check if out runway was destroyed. + if EventData.IniUnitName then + if self.airbase and self.airbasename and self.airbasename==EventData.IniUnitName then + self:RunwayDestroyed() + end + end + + end + +end + --- Event handler for event land. -- @param #FLIGHTCONTROL self -- @param Core.Event#EVENTDATA EventData @@ -895,18 +1233,7 @@ function FLIGHTCONTROL:OnEventEngineShutdown(EventData) 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:T3(self.lid..string.format("CRASH: group = %s", tostring(EventData.IniGroupName))) - -end - ---- Event handler for event crash. +--- Event handler for event kill. -- @param #FLIGHTCONTROL self -- @param Core.Event#EVENTDATA EventData function FLIGHTCONTROL:OnEventKill(EventData) @@ -941,6 +1268,42 @@ function FLIGHTCONTROL:OnEventKill(EventData) end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "RunwayDestroyed" event. +-- @param #FLIGHTCONTROL self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTCONTROL:onafterRunwayDestroyed(From, Event, To) + + -- Debug Message. + self:T(self.lid..string.format("Runway destoyed!")) + + -- Set time stamp. + self.runwaydestroyed=timer.getAbsTime() + + self:TransmissionTower("All flights, our runway was destroyed. All operations are suspended for one hour.",Flight,Delay) + +end + +--- On after "RunwayRepaired" event. +-- @param #FLIGHTCONTROL self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTCONTROL:onafterRunwayRepaired(From, Event, To) + + -- Debug Message. + self:T(self.lid..string.format("Runway repaired!")) + + -- Set parameter. + self.runwaydestroyed=nil + +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Queue Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -997,7 +1360,7 @@ function FLIGHTCONTROL:_CheckQueues() local text=string.format("Runway %s, cleared to land, %s", runway, callsign) -- Transmit message. - self:TransmissionPilot(text, flight, 10) + self:TransmissionPilot(text, flight, 10) -- Land AI. self:_LandAI(flight, parking) @@ -1296,8 +1659,14 @@ end -- @return Ops.FlightGroup#FLIGHTGROUP Marshal flight next in line and ready to enter the pattern. Or nil if no flight is ready. function FLIGHTCONTROL:_GetNextFightHolding() + -- Return only AI or human player flights. + local OnlyAI=nil + if self:IsRunwayDestroyed() then + OnlyAI=false -- If false, we return only player flights. + end + -- Get all flights holding. - local Qholding=self:GetFlights(FLIGHTCONTROL.FlightStatus.HOLDING) + local Qholding=self:GetFlights(FLIGHTCONTROL.FlightStatus.HOLDING, nil, OnlyAI) -- Min holding time in seconds. local TholdingMin=30 @@ -1365,8 +1734,14 @@ end -- @return Ops.FlightGroup#FLIGHTGROUP Marshal flight next in line and ready to enter the pattern. Or nil if no flight is ready. function FLIGHTCONTROL:_GetNextFightParking() + -- Return only AI or human player flights. + local OnlyAI=nil + if self:IsRunwayDestroyed() then + OnlyAI=false -- If false, we return only player flights. + end + -- Get flights ready for take off. - local QreadyTO=self:GetFlights(FLIGHTCONTROL.FlightStatus.READYTO, OPSGROUP.GroupStatus.TAXIING) + local QreadyTO=self:GetFlights(FLIGHTCONTROL.FlightStatus.READYTO, OPSGROUP.GroupStatus.TAXIING, OnlyAI) -- First check human players. if #QreadyTO>0 then @@ -1375,7 +1750,7 @@ function FLIGHTCONTROL:_GetNextFightParking() end -- Get flights ready to taxi. - local QreadyTX=self:GetFlights(FLIGHTCONTROL.FlightStatus.READYTX, OPSGROUP.GroupStatus.PARKING) + local QreadyTX=self:GetFlights(FLIGHTCONTROL.FlightStatus.READYTX, OPSGROUP.GroupStatus.PARKING, OnlyAI) -- First check human players. if #QreadyTX>0 then @@ -1383,9 +1758,16 @@ function FLIGHTCONTROL:_GetNextFightParking() return QreadyTX[1] end + -- Check if runway is destroyed. + if self:IsRunwayDestroyed() then + -- Runway destroyed. As we only look for AI later on, we return nil here. + return nil + end + -- Get AI flights parking. local Qparking=self:GetFlights(FLIGHTCONTROL.FlightStatus.PARKING, nil, true) + -- Number of flights parking. local Nparking=#Qparking -- Check special cases where only up to one flight is waiting for takeoff. @@ -1408,7 +1790,7 @@ function FLIGHTCONTROL:_GetNextFightParking() local text="Parking flights:" for i,_flight in pairs(Qparking) do local flight=_flight --Ops.FlightGroup#FLIGHTGROUP - text=text..string.format("\n[%d] %s %.1f", i, flight.groupname, flight:GetParkingTime()) + text=text..string.format("\n[%d] %s [%s], state=%s [%s]: Tparking=%.1f sec", i, flight.groupname, flight.actype, flight:GetState(), self:GetFlightStatus(flight), flight:GetParkingTime()) end self:I(self.lid..text) end @@ -1653,7 +2035,7 @@ function FLIGHTCONTROL:GetActiveRunwayText(Takeoff) runway=self:GetActiveRunwayLanding() end - local name=self.airbase:GetRunwayName(runway) + local name=self.airbase:GetRunwayName(runway, true) return name or "XX" end @@ -1677,14 +2059,14 @@ function FLIGHTCONTROL:_InitParkingSpots() 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) + local text=string.format("Parking ID=%d, Terminal=%d: Free=%s, Client=%s, Dist=%.1f", spot.TerminalID, spot.TerminalType, tostring(spot.Free), tostring(spot.ClientName), spot.DistToRwy) + self:T3(self.lid..text) -- Add to table. self.parking[spot.TerminalID]=spot -- Marker. - spot.Marker=MARKER:New(spot.Coordinate, "Spot"):ReadOnly():ToCoalition(self:GetCoalition()) + --spot.Marker=MARKER:New(spot.Coordinate, "Spot"):ReadOnly():ToCoalition(self:GetCoalition()) -- Check if spot is initially free or occupied. if spot.Free then @@ -1743,14 +2125,33 @@ 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. +-- @param #string status New status. +-- @param #string unitname Name of the unit. +function FLIGHTCONTROL:_UpdateSpotStatus(spot, status, unitname) + + -- Debug message. + self:T2(self.lid..string.format("Updating parking spot %d status: %s --> %s (unit=%s)", spot.TerminalID, tostring(spot.Status), status, tostring(unitname))) + + -- Set new status. + spot.Status=status + +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) + -- Get spot. local spot=self:GetParkingSpotByID(spot.TerminalID) - spot.Status=AIRBASE.SpotStatus.FREE + -- Update spot status. + self:_UpdateSpotStatus(spot, AIRBASE.SpotStatus.FREE, spot.OccupiedBy or spot.ReservedBy) + + -- Not occupied or reserved. spot.OccupiedBy=nil spot.ReservedBy=nil @@ -1768,11 +2169,16 @@ end -- @param #string unitname Name of the unit occupying the spot. Default "unknown". function FLIGHTCONTROL:SetParkingReserved(spot, unitname) + -- Get spot. local spot=self:GetParkingSpotByID(spot.TerminalID) - spot.Status=AIRBASE.SpotStatus.RESERVED + -- Update spot status. + self:_UpdateSpotStatus(spot, AIRBASE.SpotStatus.RESERVED, unitname) + + -- Reserved. spot.ReservedBy=unitname or "unknown" + -- Update marker. self:UpdateParkingMarker(spot) end @@ -1783,11 +2189,16 @@ end -- @param #string unitname Name of the unit occupying the spot. Default "unknown". function FLIGHTCONTROL:SetParkingOccupied(spot, unitname) + -- Get spot. local spot=self:GetParkingSpotByID(spot.TerminalID) + + -- Update spot status. + self:_UpdateSpotStatus(spot, AIRBASE.SpotStatus.OCCUPIED, unitname) - spot.Status=AIRBASE.SpotStatus.OCCUPIED + -- Occupied. spot.OccupiedBy=unitname or "unknown" + -- Update marker. self:UpdateParkingMarker(spot) end @@ -1797,43 +2208,46 @@ end -- @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 self.markerParking then + + -- Get spot. + local spot=self:GetParkingSpotByID(spot.TerminalID) - if spot.Marker then + -- Only mark OCCUPIED and RESERVED spots. + if spot.Status==AIRBASE.SpotStatus.FREE then - if text~=spot.Marker.text then - spot.Marker:UpdateText(text) + if spot.Marker then + spot.Marker:Remove() end - + else - spot.Marker=MARKER:New(spot.Coordinate, text):ToAll() - + 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", tostring(spot.OccupiedBy)) + end + if spot.ReservedBy then + text=text..string.format("\nReserved for %s", tostring(spot.ReservedBy)) + end + if spot.ClientSpot then + text=text..string.format("\nClient %s", tostring(spot.ClientName)) + end + + if spot.Marker then + + if text~=spot.Marker.text or not spot.Marker.shown then + spot.Marker:UpdateText(text) + end + + else + + spot.Marker=MARKER:New(spot.Coordinate, text):ToAll() + + end + end - end + end --- Check if parking spot is free. @@ -1868,21 +2282,6 @@ function FLIGHTCONTROL:IsParkingReserved(spot) 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. @@ -1942,6 +2341,25 @@ function FLIGHTCONTROL:GetClosestParkingSpot(Coordinate, TerminalType, Status) return spotmin end + +--- Get parking spot this player was initially spawned on. +-- @param #FLIGHTCONTROL self +-- @param #string UnitName Name of the player unit. +-- @return #FLIGHTCONTROL.ParkingSpot Player spot or nil. +function FLIGHTCONTROL:_GetPlayerSpot(UnitName) + + for TerminalID, Spot in pairs(self.parking) do + local spot=Spot --Wrapper.Airbase#AIRBASE.ParkingSpot + + if spot.ClientName and spot.ClientName==UnitName and (spot.Status==AIRBASE.SpotStatus.FREE or spot.ReservedBy==UnitName) then + return spot + end + + end + + return nil +end + --- Count number of parking spots. -- @param #FLIGHTCONTROL self -- @param #string SpotStatus (Optional) Status of spot. @@ -2036,7 +2454,7 @@ function FLIGHTCONTROL:_CreatePlayerMenu(flight, mainmenu) --- if status==FLIGHTCONTROL.FlightStatus.READYTX then - MENU_GROUP_COMMAND:New(group, "Abort Taxi", rootmenu, self._PlayerAbortTaxi, self, groupname) + MENU_GROUP_COMMAND:New(group, "Cancel Taxi", rootmenu, self._PlayerAbortTaxi, self, groupname) else MENU_GROUP_COMMAND:New(group, "Request Taxi", rootmenu, self._PlayerRequestTaxi, self, groupname) end @@ -2059,7 +2477,11 @@ function FLIGHTCONTROL:_CreatePlayerMenu(flight, mainmenu) elseif status==FLIGHTCONTROL.FlightStatus.TAXIINB then -- Could be after "abort taxi" call and we changed our mind (again) MENU_GROUP_COMMAND:New(group, "Request Taxi", rootmenu, self._PlayerRequestTaxi, self, groupname) - MENU_GROUP_COMMAND:New(group, "Request Parking", rootmenu, self._PlayerRequestParking, self, groupname) + if player.parking then + MENU_GROUP_COMMAND:New(group, "Cancel Parking", rootmenu, self._PlayerCancelParking, self, groupname) + else + MENU_GROUP_COMMAND:New(group, "Reserve Parking", rootmenu, self._PlayerRequestParking, self, groupname) + end MENU_GROUP_COMMAND:New(group, "Arrived at Parking", rootmenu, self._PlayerArrived, self, groupname) end @@ -2068,9 +2490,20 @@ function FLIGHTCONTROL:_CreatePlayerMenu(flight, mainmenu) -- Inbound --- - MENU_GROUP_COMMAND:New(group, "Holding!", rootmenu, self._PlayerHolding, self, groupname) - MENU_GROUP_COMMAND:New(group, "Abort Inbound", rootmenu, self._PlayerAbortInbound, self, groupname) - MENU_GROUP_COMMAND:New(group, "Request Parking", rootmenu, self._PlayerRequestParking, self, groupname) + if status==FLIGHTCONTROL.FlightStatus.LANDING then + -- After direct approach. + MENU_GROUP_COMMAND:New(group, "Confirm Landing!", rootmenu, self._PlayerConfirmLanding, self, groupname) + MENU_GROUP_COMMAND:New(group, "Abort Landing", rootmenu, self._PlayerAbortLanding, self, groupname) + else + MENU_GROUP_COMMAND:New(group, "Holding!", rootmenu, self._PlayerHolding, self, groupname) + MENU_GROUP_COMMAND:New(group, "Direct Approach", rootmenu, self._PlayerRequestDirectLanding, self, groupname) + MENU_GROUP_COMMAND:New(group, "Abort Inbound", rootmenu, self._PlayerAbortInbound, self, groupname) + end + if player.parking then + MENU_GROUP_COMMAND:New(group, "Cancel Parking", rootmenu, self._PlayerCancelParking, self, groupname) + else + MENU_GROUP_COMMAND:New(group, "Reserve Parking", rootmenu, self._PlayerRequestParking, self, groupname) + end elseif flight:IsHolding() then @@ -2080,7 +2513,11 @@ function FLIGHTCONTROL:_CreatePlayerMenu(flight, mainmenu) MENU_GROUP_COMMAND:New(group, "Confirm Landing!", rootmenu, self._PlayerConfirmLanding, self, groupname) MENU_GROUP_COMMAND:New(group, "Abort Holding", rootmenu, self._PlayerAbortHolding, self, groupname) - MENU_GROUP_COMMAND:New(group, "Request Parking", rootmenu, self._PlayerRequestParking, self, groupname) + if player.parking then + MENU_GROUP_COMMAND:New(group, "Cancel Parking", rootmenu, self._PlayerCancelParking, self, groupname) + else + MENU_GROUP_COMMAND:New(group, "Reserve Parking", rootmenu, self._PlayerRequestParking, self, groupname) + end elseif flight:IsLanding(player) then --- @@ -2088,7 +2525,11 @@ function FLIGHTCONTROL:_CreatePlayerMenu(flight, mainmenu) --- MENU_GROUP_COMMAND:New(group, "Abort Landing", rootmenu, self._PlayerAbortLanding, self, groupname) - MENU_GROUP_COMMAND:New(group, "Request Parking", rootmenu, self._PlayerRequestParking, self, groupname) + if player.parking then + MENU_GROUP_COMMAND:New(group, "Cancel Parking", rootmenu, self._PlayerCancelParking, self, groupname) + else + MENU_GROUP_COMMAND:New(group, "Reserve Parking", rootmenu, self._PlayerRequestParking, self, groupname) + end elseif flight:IsLanded(player) then --- @@ -2096,7 +2537,11 @@ function FLIGHTCONTROL:_CreatePlayerMenu(flight, mainmenu) --- MENU_GROUP_COMMAND:New(group, "Arrived at Parking", rootmenu, self._PlayerArrived, self, groupname) - MENU_GROUP_COMMAND:New(group, "Request Parking", rootmenu, self._PlayerRequestParking, self, groupname) + if player.parking then + MENU_GROUP_COMMAND:New(group, "Cancel Parking", rootmenu, self._PlayerCancelParking, self, groupname) + else + MENU_GROUP_COMMAND:New(group, "Reserve Parking", rootmenu, self._PlayerRequestParking, self, groupname) + end elseif flight:IsArrived(player) then --- @@ -2325,7 +2770,7 @@ function FLIGHTCONTROL:_PlayerInfoTraffic(groupname) 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 NQarrived=self:CountFlights(FLIGHTCONTROL.FlightStatus.ARRIVED) -- local text=string.format("Traffic %s airbase:", self.airbasename) @@ -2643,7 +3088,7 @@ function FLIGHTCONTROL:_PlayerHolding(groupname) local text=string.format("%s, roger, fly heading %d at angels %d and wait for landing clearance", callsign, stack.heading, stack.angels) -- Radio message from tower. - self:TransmissionTower(text,flight, 10) + self:TransmissionTower(text, flight, 10) -- Call holding event. flight:Holding() @@ -2760,7 +3205,7 @@ function FLIGHTCONTROL:_PlayerConfirmLanding(groupname) if flight then - if flight:IsHolding() and self:IsControlling(flight) then + if (flight:IsHolding() or flight:IsInbound()) and self:IsControlling(flight) then -- Call sign. local callsign=self:_GetCallsignName(flight) @@ -2807,7 +3252,7 @@ function FLIGHTCONTROL:_PlayerConfirmLanding(groupname) else -- Error you are not airborne! - local text=string.format("Negative, you must be HOLDING and CONTROLLED by us!") + local text=string.format("Negative, you must be HOLDING or INBOUND and CONTROLLED by us!") -- Send message. self:TextMessageToFlight(text, flight, 10) @@ -2878,6 +3323,67 @@ function FLIGHTCONTROL:_PlayerAbortLanding(groupname) end +--- Player request direct approach. +-- @param #FLIGHTCONTROL self +-- @param #string groupname Name of the flight group. +function FLIGHTCONTROL:_PlayerRequestDirectLanding(groupname) + + -- Get flight group. + local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP + + if flight then + + if flight:IsInbound() and self:IsControlling(flight) then + + -- Call sign. + local callsign=self:_GetCallsignName(flight) + + -- Number of flights taking off. + local nTakeoff=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAKEOFF) + + + -- Message. + local text=string.format("%s, request direct approach.", callsign) + + -- Transmit message. + self:TransmissionPilot(text, flight) + + if nTakeoff>self.NlandingTakeoff then + + -- Message text. + local text=string.format("%s, negative! We have currently traffic taking off", callsign) + + -- Send message. + self:TransmissionTower(text, flight, 10) + + else + + -- Message text. + local text=string.format("%s, affirmative! Confirm approach", callsign) + + -- Send message. + self:TransmissionTower(text, flight, 10) + + -- Set flight status to landing. + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.LANDING) + + end + + else + + -- Error you are not airborne! + local text=string.format("Negative, you must be INBOUND and CONTROLLED by us!") + + -- Send message. + self:TextMessageToFlight(text, flight, 10) + end + + else + self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) + end + +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Player Menu: Taxi ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2899,7 +3405,7 @@ function FLIGHTCONTROL:_PlayerRequestTaxi(groupname) local text=string.format("%s, %s, request taxi to runway.", self.alias, callsign) self:TransmissionPilot(text, flight) - if flight:IsParking() or flight:IsTaxiing() then + if flight:IsParking() then -- Tell pilot to wait until cleared. local text=string.format("%s, %s, hold position until further notice.", callsign, self.alias) @@ -2908,8 +3414,25 @@ function FLIGHTCONTROL:_PlayerRequestTaxi(groupname) -- Set flight status to "Ready to Taxi". self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.READYTX) - -- Update menu. - --flight:_UpdateMenu(0.5) + elseif flight:IsTaxiing() then + + -- Runway for takeoff. + local runway=self:GetActiveRunwayText(true) + + -- Tell pilot to wait until cleared. + local text=string.format("%s, %s, taxi to runway %s, hold short.", callsign, self.alias, runway) + self:TransmissionTower(text, flight, 10) + + -- Taxi out. + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.TAXIOUT) + + -- Get player element. + local playerElement=flight:GetPlayerElement() + + -- Set parking to free. Could be reserved. + if playerElement and playerElement.parking then + self:SetParkingFree(playerElement.parking) + end else self:TextMessageToFlight(string.format("Negative, you must be PARKING to request TAXI!"), flight) @@ -2935,7 +3458,7 @@ function FLIGHTCONTROL:_PlayerAbortTaxi(groupname) local callsign=self:_GetCallsignName(flight) -- Pilot request for taxi. - local text=string.format("%s, %s, abort taxi request.", self.alias, callsign) + local text=string.format("%s, %s, cancel my taxi request.", self.alias, callsign) self:TransmissionPilot(text, flight) if flight:IsParking() then @@ -2944,11 +3467,16 @@ function FLIGHTCONTROL:_PlayerAbortTaxi(groupname) local text=string.format("%s, %s, roger, remain on your parking position.", callsign, self.alias) self:TransmissionTower(text, flight, 10) - -- Set flight status to "Ready for Take-off". + -- Set flight status to "Parking". self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.PARKING) - -- Update menu. - --flight:_UpdateMenu(0.5) + -- Get player element. + local playerElement=flight:GetPlayerElement() + + -- Set parking guard. + if playerElement then + self:SpawnParkingGuard(playerElement.unit) + end elseif flight:IsTaxiing() then @@ -2958,9 +3486,6 @@ function FLIGHTCONTROL:_PlayerAbortTaxi(groupname) -- Set flight status to "Taxi Inbound". self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.TAXIINB) - - -- Update menu. - --flight:_UpdateMenu(0.5) else self:TextMessageToFlight(string.format("Negative, you must be PARKING or TAXIING to abort TAXI!"), flight) @@ -3024,7 +3549,11 @@ function FLIGHTCONTROL:_PlayerRequestTakeoff(groupname) -- We only check for landing flights. local text=string.format("%s, %s, ", callsign, self.alias) if Nlanding==0 then + + -- No traffic. text=text.."no current traffic. You are cleared for takeoff." + + -- Set status to "Take off". self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.TAKEOFF) elseif Nlanding>0 then if Nlanding==1 then @@ -3036,9 +3565,6 @@ function FLIGHTCONTROL:_PlayerRequestTakeoff(groupname) -- Message from tower. self:TransmissionTower(text, flight, 10) - - -- Update menu. - --flight:_UpdateMenu(0.5) else self:TextMessageToFlight(string.format("Negative, you must request TAXI before you can request TAKEOFF!"), flight) @@ -3075,8 +3601,19 @@ function FLIGHTCONTROL:_PlayerAbortTakeoff(groupname) -- Set new flight status. if flight:IsParking() then + text=string.format("%s, %s, affirm, remain on your parking position.", callsign, self.alias) + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.PARKING) + + -- Get player element. + local playerElement=flight:GetPlayerElement() + + -- Set parking guard. + if playerElement then + self:SpawnParkingGuard(playerElement.unit) + end + elseif flight:IsTaxiing() then text=string.format("%s, %s, roger, report whether you want to taxi back or takeoff later.", callsign, self.alias) self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.TAXIOUT) @@ -3085,10 +3622,7 @@ function FLIGHTCONTROL:_PlayerAbortTakeoff(groupname) end -- Message from tower. - self:TransmissionTower(text, flight, 10) - - -- Update menu. - --flight:_UpdateMenu(0.5) + self:TransmissionTower(text, flight, 10) else self:TextMessageToFlight("Negative, You are NOT in the takeoff queue", flight) @@ -3104,7 +3638,7 @@ end -- Player Menu: Parking ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Player menu request info. +--- Player reserves a parking spot. -- @param #FLIGHTCONTROL self -- @param #string groupname Name of the flight group. function FLIGHTCONTROL:_PlayerRequestParking(groupname) @@ -3119,11 +3653,7 @@ function FLIGHTCONTROL:_PlayerRequestParking(groupname) -- Get player element. local player=flight:GetPlayerElement() - - --TODO: Check if player has already a parking spot assigned. If so, remind him. Should we stick to it or give him a new position? - - --TODO: Check if player is currently parking on a spot. If so, he first needs to leave it. - + -- Set terminal type. local TerminalType=AIRBASE.TerminalType.FighterAircraft if flight.isHelo then @@ -3132,25 +3662,22 @@ function FLIGHTCONTROL:_PlayerRequestParking(groupname) -- Current coordinate. local coord=flight:GetCoordinate(nil, player.name) - -- Get closest FREE parking spot. - local spot=self:GetClosestParkingSpot(coord, TerminalType, AIRBASE.SpotStatus.FREE) + -- Get spawn position if any. + local spot=self:_GetPlayerSpot(player.name) + + -- Get closest FREE parking spot if player was not spawned here or spot is already taken. + if not spot then + spot=self:GetClosestParkingSpot(coord, TerminalType, AIRBASE.SpotStatus.FREE) + end if spot then -- Message text. - local text=string.format("%s, your assigned parking position is terminal ID %d. Check the F10 map for details.", callsign, spot.TerminalID) + local text=string.format("%s, your assigned parking position is terminal ID %d.", callsign, spot.TerminalID) -- Transmit message. self:TransmissionTower(text, flight) - - -- Create mark on F10 map. - --[[ - if spot.Marker then - spot.Marker:Remove() - end - spot.Marker:SetText("Your assigned parking spot!"):ReadWrite():ToGroup(flight.group) - ]] - + -- If player already has a spot. if player.parking then self:SetParkingFree(player.parking) @@ -3160,6 +3687,9 @@ function FLIGHTCONTROL:_PlayerRequestParking(groupname) player.parking=spot self:SetParkingReserved(spot, player.name) + -- Update menu ==> Cancel Parking. + flight:_UpdateMenu(0.2) + else -- Message text. @@ -3176,6 +3706,40 @@ function FLIGHTCONTROL:_PlayerRequestParking(groupname) end +--- Player cancels parking spot reservation. +-- @param #FLIGHTCONTROL self +-- @param #string groupname Name of the flight group. +function FLIGHTCONTROL:_PlayerCancelParking(groupname) + + -- Get flight group. + local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP + + if flight then + + -- Get callsign. + local callsign=self:_GetCallsignName(flight) + + -- Get player element. + local player=flight:GetPlayerElement() + + -- If player already has a spot. + if player.parking then + self:SetParkingFree(player.parking) + player.parking=nil + self:TextMessageToFlight(string.format("%s, your parking spot reservation at terminal ID %d was cancelled.", callsign, player.parking.TerminalID), flight) + else + self:TextMessageToFlight("You did not have a valid parking spot reservation.", flight) + end + + -- Update menu ==> Reserve Parking. + flight:_UpdateMenu(0.2) + + else + self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) + end + +end + --- Player arrived at parking position. -- @param #FLIGHTCONTROL self -- @param #string groupname Name of the flight group. @@ -3188,13 +3752,20 @@ function FLIGHTCONTROL:_PlayerArrived(groupname) -- Player element. local player=flight:GetPlayerElement() - + -- Get current coordinate. local coord=flight:GetCoordinate(nil, player.name) - - --Closest parking spot. - local spot=self:GetClosestParkingSpot(coord) + -- Parking spot. + local spot=self:_GetPlayerSpot(player.name) --#FLIGHTCONTROL.ParkingSpot + if player.parking then + spot=self:GetParkingSpotByID(player.parking.TerminalID) + else + if not spot then + spot=self:GetClosestParkingSpot(coord) + end + end + if spot then -- Get callsign. @@ -3203,23 +3774,38 @@ function FLIGHTCONTROL:_PlayerArrived(groupname) -- Distance to parking spot. local dist=coord:Get2DDistance(spot.Coordinate) - if dist<20 then + if dist<12 then -- Message text. local text=string.format("%s, %s, arrived at parking position. Terminal ID %d.", self.alias, callsign, spot.TerminalID) -- Transmit message. self:TransmissionPilot(text, flight) - - -- Set player element to parking. - flight:ElementParking(player, spot) - - -- Set flight status to PARKING. - self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.PARKING) - -- Message text. - local text=string.format("%s, %s, roger. Enjoy a cool bevarage in the officers' club.", callsign, self.alias) + local text="" + if spot.ReservedBy and spot.ReservedBy~=player.name then + -- Reserved by someone else. + text=string.format("%s, this spot is already reserved for %s. Find yourself a different parking position.", callsign, self.alias, spot.ReservedBy) + + else + + -- Okay, have a drink... + text=string.format("%s, %s, roger. Enjoy a cool bevarage in the officers' club.", callsign, self.alias) + + -- Set player element to parking. + flight:ElementParking(player, spot) + + -- Set flight status to PARKING. + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.PARKING) + + -- Set parking guard. + if player then + self:SpawnParkingGuard(player.unit) + end + + end + -- Transmit message. self:TransmissionTower(text, flight, 10) @@ -3231,8 +3817,20 @@ function FLIGHTCONTROL:_PlayerArrived(groupname) -- Transmit message. self:TransmissionPilot(text, flight) - -- Message text. - local text=string.format("%s, %s, you are still %d meters away from the closest parking position. Continue taxiing to a proper spot!", callsign, self.alias, dist) + local text="" + if spot.ReservedBy then + if spot.ReservedBy==player.name then + -- To far from reserved spot. + text=string.format("%s, %s, you are still %d meters away from your reserved parking position at terminal ID %d. Continue taxiing!", callsign, self.alias, dist, spot.TerminalID) + else + -- Closest spot is reserved by someone else. + --local spotFree=self:GetClosestParkingSpot(coord, nil, AIRBASE.SpotStatus.Free) + text=string.format("%s, %s, the closest parking spot is already reserved. Continue taxiing to a free spot!", callsign, self.alias) + end + else + -- Too far from closest spot. + text=string.format("%s, %s, you are still %d meters away from the closest parking position. Continue taxiing to a proper spot!", callsign, self.alias, dist) + end -- Transmit message. self:TransmissionTower(text, flight, 10) @@ -3405,20 +4003,46 @@ function FLIGHTCONTROL:_CheckFlights() local flight=_flight --Ops.FlightGroup#FLIGHTGROUP if not flight.isAI then + + -- Get player element. local playerElement=flight:GetPlayerElement() + -- Current flight status. + local flightstatus=self:GetFlightStatus(flight) + if playerElement then - local speed=playerElement.unit:GetVelocityMPS() - - env.info(string.format("FF player %s speed %.1f knots (max=%.1f)", playerElement.playerName, UTILS.MpsToKnots(speed), UTILS.MpsToKnots(self.speedLimitTaxi))) - - if speed and speed>self.speedLimitTaxi then - - local text="Slow down, you are too fast!" + -- Check if speeding while taxiing. + if (flightstatus==FLIGHTCONTROL.FlightStatus.TAXIINB or flightstatus==FLIGHTCONTROL.FlightStatus.TAXIOUT) and self.speedLimitTaxi then + + -- Current speed in m/s. + local speed=playerElement.unit:GetVelocityMPS() + + -- Current position. + local coord=playerElement.unit:GetCoord() + + -- We do not want to check speed on runways. + local onRunway=self:IsCoordinateRunway(coord) + + -- Debug output. + self:I(self.lid..string.format("Player %s speed %.1f knots (max=%.1f) onRunway=%s", playerElement.playerName, UTILS.MpsToKnots(speed), UTILS.MpsToKnots(self.speedLimitTaxi), tostring(onRunway))) + + if speed and speed>self.speedLimitTaxi and not onRunway then + + -- Radio text. + local text="Slow down, you are taxiing too fast!" + + -- Radio message to player. + self:TransmissionTower(text, flight) + + -- Get player data. + local PlayerData=flight:_GetPlayerData() + + -- Trigger FSM speeding event. + self:PlayerSpeeding(PlayerData) + + end - self:TransmissionTower(text, flight) - end end @@ -3638,9 +4262,18 @@ function FLIGHTCONTROL:TransmissionPilot(Text, Flight, Delay) -- Spoken text. local text=self:_GetTextForSpeech(Text) - - -- Pilot radio call. - self.msrsPilot:PlayText(text, Delay) + + if Flight.useSRS and Flight.msrs then + + -- Pilot radio call using settings of the FLIGHTGROUP. We just overwrite the frequency. + Flight.msrs:PlayTextExt(text, Delay, self.frequency, self.modulation, Gender, Culture, Voice, Volume, Label) + + else + + -- Pilot radio call using the default settings. + self.msrsPilot:PlayText(text, Delay) + + end -- "Subtitle". if Flight and not Flight.isAI then @@ -3713,32 +4346,38 @@ function FLIGHTCONTROL:SpawnParkingGuard(unit) -- Parking spot. local spot=self:GetClosestParkingSpot(coordinate) - -- Current heading of the unit. - local heading=unit:GetHeading() + if not spot.ParkingGuard then - -- Length of the unit + 3 meters. - local size, x, y, z=unit:GetObjectSize() - - -- Debug message. - self:T2(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) - - -- Turn AI Off. - if self.parkingGuard:IsInstanceOf("SPAWN") then - --self.parkingGuard:InitAIOff() + -- Current heading of the unit. + local heading=unit:GetHeading() + + -- Length of the unit + 3 meters. + local size, x, y, z=unit:GetObjectSize() + + -- Debug message. + self:T2(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) + + -- Turn AI Off. + if self.parkingGuard:IsInstanceOf("SPAWN") then + --self.parkingGuard:InitAIOff() + end + + -- Group that is spawned. + spot.ParkingGuard=self.parkingGuard:SpawnFromCoordinate(Coordinate) + + else + self:E(self.lid.."ERROR: Parking Guard already exists!") end - -- Group that is spawned. - spot.ParkingGuard=self.parkingGuard:SpawnFromCoordinate(Coordinate) - end end diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index f6f375e06..1d5b27eb3 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -870,21 +870,21 @@ function FLIGHTGROUP:Status() if self:IsParking() then for _,_element in pairs(self.elements) do local element=_element --Ops.OpsGroup#OPSGROUP.Element + + -- Check for parking spot. if element.parking then -- Get distance to assigned parking spot. - local dist=element.unit:GetCoordinate():Get2DDistance(element.parking.Coordinate) + local dist=element.unit:GetCoord():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. - -- At least for fighters, the initial distance seems to be 1.8 meters. - -- TODO: Check distance threshold! If element is taxiing, the parking spot is free again. - -- When the next plane is spawned on this spot, collisions should be avoided! - if dist>10 then - if element.status==OPSGROUP.ElementStatus.ENGINEON then + -- At least for fighters, the initial distance seems to be around 1.8 meters. + if dist>12 and element.engineOn then + --if element.status==OPSGROUP.ElementStatus.ENGINEON then self:ElementTaxiing(element) - end + --end end else @@ -1163,6 +1163,9 @@ function FLIGHTGROUP:OnEventEngineStartup(EventData) -- Element started engies. self:ElementEngineOn(element) + -- Engines are on. + element.engineOn=true + --[[ -- TODO: could be that this element is part of a human flight group. -- Problem: when player starts hot, the AI does too and starts to taxi immidiately :( @@ -1253,6 +1256,9 @@ function FLIGHTGROUP:OnEventEngineShutdown(EventData) local element=self:GetElementByName(unitname) if element then + + -- Engines are off. + element.engineOn=false if element.unit and element.unit:IsAlive() then @@ -1395,8 +1401,10 @@ function FLIGHTGROUP:onafterElementParking(From, Event, To, Element, Spot) -- Wait for engine startup event. elseif self:IsTakeoffHot() then self:__ElementEngineOn(0.5, Element) -- delay a bit to allow all elements + Element.engineOn=true elseif self:IsTakeoffRunway() then self:__ElementEngineOn(0.5, Element) + Element.engineOn=true end end @@ -1618,6 +1626,11 @@ function FLIGHTGROUP:onafterSpawned(From, Event, To) text=text..string.format("Start Cold = %s\n", tostring(self:IsTakeoffCold())) text=text..string.format("Start Hot = %s\n", tostring(self:IsTakeoffHot())) text=text..string.format("Start Rwy = %s\n", tostring(self:IsTakeoffRunway())) + text=text..string.format("Elements:") + for i,_element in pairs(self.elements) do + local element=_element --Ops.OpsGroup#OPSGROUP.Element + text=text..string.format("\n[%d] %s: callsign=%s, modex=%s, player=%s", i, element.name, tostring(element.callsign), tostring(element.modex), tostring(element.playerName)) + end self:I(self.lid..text) end @@ -2651,6 +2664,22 @@ function FLIGHTGROUP:_LandAtAirbase(airbase, SpeedTo, SpeedHold, SpeedLand) -- Add flight to inbound queue. self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.INBOUND) + + -- Callsign. + local callsign=self:GetCallsignName() + + -- Pilot calls inbound for landing. + local text=string.format("%s, %s, inbound for landing", fc.alias, callsign) + + -- Radio message. + fc:TransmissionPilot(text, self) + + -- Message text. + local text=string.format("%s, %s, roger, hold at angels %d. Report entering the pattern.", callsign, fc.alias, stack.angels) + + -- Send message. + fc:TransmissionTower(text, self, 10) + end -- Some intermediate coordinate to climb to the default cruise alitude. @@ -2913,6 +2942,29 @@ function FLIGHTGROUP:onafterHolding(From, Event, To) -- Set flight status to holding. self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.HOLDING) + + if self.isAI then + + -- Callsign. + local callsign=self:GetCallsignName() + + -- Pilot arrived at holding pattern. + local text=string.format("%s, %s, arrived at holding pattern", self.flightcontrol.alias, callsign) + + if self.stack then + text=text..string.format(", angels %d.", self.stack.angels) + end + + -- Radio message. + self.flightcontrol:TransmissionPilot(text, self) + + -- Message to flight + local text=string.format("%s, roger, fly heading %d and wait for landing clearance", callsign, self.stack.heading) + + -- Radio message from tower. + self.flightcontrol:TransmissionTower(text, self, 10) + + end elseif self.airboss then @@ -3900,16 +3952,16 @@ function FLIGHTGROUP:GetParkingSpot(element, maxdist, airbase) local coord=element.unit:GetCoordinate() -- Airbase. - airbase=airbase or self:GetClosestAirbase() --coord:GetClosestAirbase(nil, self:GetCoalition()) + airbase=airbase or self:GetClosestAirbase() - -- TODO: replace by airbase.parking if AIRBASE is updated. - local parking=airbase:GetParkingSpotsTable() + -- Parking table of airbase. + local parking=airbase.parking --:GetParkingSpotsTable() -- If airbase is ship, translate parking coords. Alternatively, we just move the coordinate of the unit to the origin of the map, which is way more efficient. if airbase and airbase:IsShip() then coord.x=0 coord.z=0 - maxdist=500 -- 100 meters was not enough, e.g. on the Seawise Giant, where the spot is 139 meters from the "center" + maxdist=500 -- 100 meters was not enough, e.g. on the Seawise Giant, where the spot is 139 meters from the "center". end local spot=nil --Wrapper.Airbase#AIRBASE.ParkingSpot @@ -3917,8 +3969,10 @@ function FLIGHTGROUP:GetParkingSpot(element, maxdist, airbase) local distmin=math.huge for _,_parking in pairs(parking) do local parking=_parking --Wrapper.Airbase#AIRBASE.ParkingSpot + + -- Distance to spot. dist=coord:Get2DDistance(parking.Coordinate) - --env.info(string.format("FF parking %d dist=%.1f", parking.TerminalID, dist)) + if dist=1 then - -- General info. - local text=string.format("State=%s: Phase=%s, Phases=%d [Active=%d, Planned=%d, Over=%d]", fsmstate, phaseName, NphaseTot, NphaseAct, NphasePla, NphaseOvr) - self:I(self.lid..text) - - -- Info on phases. - local text="Phases:" - for i,_phase in pairs(self.phases) do - local phase=_phase --#OPERATION.Phase - text=text..string.format("\n[%d] %s: status=%s", i, phase.name, tostring(phase.status)) + -- Current phase. + local currphase=self:GetPhaseActive() + local phaseName="None" + if currphase then + phaseName=currphase.name + end + local NphaseTot=self:CountPhases() + local NphaseAct=self:CountPhases(OPERATION.PhaseStatus.ACTIVE) + local NphasePla=self:CountPhases(OPERATION.PhaseStatus.PLANNED) + local NphaseOvr=self:CountPhases(OPERATION.PhaseStatus.OVER) + + -- General info. + local text=string.format("State=%s: Phase=%s, Phases=%d [Active=%d, Planned=%d, Over=%d]", fsmstate, phaseName, NphaseTot, NphaseAct, NphasePla, NphaseOvr) + self:I(self.lid..text) + + end + + -- Debug output. + if self.verbose>=2 then + + -- Info on phases. + local text="Phases:" + for i,_phase in pairs(self.phases) do + local phase=_phase --#OPERATION.Phase + text=text..string.format("\n[%d] %s: status=%s", i, phase.name, tostring(phase.status)) + end + if text=="Phases:" then text=text.." None" end + self:I(self.lid..text) + end - if text=="Phases:" then text=text.." None" end - self:I(self.lid..text) -- Next status update. self:__StatusUpdate(-30) diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index 0ab4896eb..91f5f80e1 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -210,6 +210,7 @@ OPSGROUP = { -- @field #string skill Skill level. -- @field #string playerName Name of player if this is a client. -- @field #number Nhit Number of times the element was hit. +-- @field #boolean engineOn If `true`, engines were started. -- -- @field Core.Zone#ZONE_POLYGON_BASE zoneBoundingbox Bounding box zone of the element unit. -- @field Core.Zone#ZONE_POLYGON_BASE zoneLoad Loading zone. @@ -5831,8 +5832,6 @@ end -- @param #number Duration Duration how long the group will be waiting in seconds. Default `nil` (=forever). function OPSGROUP:onbeforeWait(From, Event, To, Duration) - env.info("FF before wait") - local allowed=true local Tsuspend=nil @@ -11712,32 +11711,15 @@ end -- @return #string Callsign name, e.g. Uzi11, or "Ghostrider11". function OPSGROUP:GetCallsignName() - local element=self:GetElementAlive() + local element=self:GetElementAlive() - if element then - self:T2(self.lid..string.format("Callsign %s", tostring(element.callsign))) - return element.callsign - end - - --[[ - - local numberSquad=self.callsign.NumberSquad or self.callsignDefault.NumberSquad - local numberGroup=self.callsign.NumberGroup or self.callsignDefault.NumberGroup - - local callsign="Unknown 1" - - if numberSquad and numberGroup then - - local nameSquad=UTILS.GetCallsignName(numberSquad) - - callsign=string.format("%s %d", nameSquad, numberGroup) - - else - + if element then + self:T2(self.lid..string.format("Callsign %s", tostring(element.callsign))) + local name=element.callsign or "Ghostrider11" + name=name:gsub("-", "") + return name end - ]] - return "Ghostrider11" end diff --git a/Moose Development/Moose/Sound/SRS.lua b/Moose Development/Moose/Sound/SRS.lua index cafb49c7c..502a78ffc 100644 --- a/Moose Development/Moose/Sound/SRS.lua +++ b/Moose Development/Moose/Sound/SRS.lua @@ -529,37 +529,41 @@ function MSRS:PlayText(Text, Delay) -- Execute command. self:_ExecCommand(command) - --[[ - - -- Check that length of command is max 255 chars or os.execute() will not work! - if string.len(command)>255 then - - -- Create a tmp file. - local filename = os.getenv('TMP') .. "\\MSRS-"..STTS.uuid()..".bat" - - local script = io.open(filename, "w+") - script:write(command.." && exit") - script:close() - - -- Play command. - command=string.format("\"%s\"", filename) - - -- Play file in 0.05 seconds - timer.scheduleFunction(os.execute, command, timer.getTime()+0.05) - - -- Remove file in 1 second. - timer.scheduleFunction(os.remove, filename, timer.getTime()+1) - else - - -- Debug output. - self:I(string.format("MSRS Text command=%s", command)) + end - -- Execute SRS command. - local x=os.execute(command) + return self +end + +--- Play text message via STTS with explicitly specified options. +-- @param #MSRS self +-- @param #string Text Text message. +-- @param #number Delay Delay in seconds, before the message is played. +-- @return #MSRS self +function MSRS:PlayTextExt(Text, Delay, Frequencies, Modulations, Gender, Culture, Voice, Volume, Label) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, MSRS.PlayTextExt, self, Text, 0, Frequencies, Modulations, Gender, Culture, Voice, Volume, Label) + else + + -- Ensure table. + if Frequencies and type(Frequencies)~="table" then + Frequencies={Frequencies} + end + + -- Ensure table. + if Modulations and type(Modulations)~="table" then + Modulations={Modulations} + end + + -- Get command line. + local command=self:_GetCommand(Frequencies, Modulations, nil, Gender, Voice, Culture, Volume, nil, nil, Label) + + -- Append text. + command=command..string.format(" --text=\"%s\"", tostring(Text)) - end + -- Execute command. + self:_ExecCommand(command) - ]] end return self diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 4a877dc9e..d4e2d81f4 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -523,8 +523,9 @@ AIRBASE.SouthAtlantic={ -- @field #string AirbaseName Name of the airbase. -- @field #number MarkerID Numerical ID of marker placed at parking spot. -- @field Wrapper.Marker#MARKER Marker The marker on the F10 map. --- @field #string ClientSpot Client unit sitting at this spot or *nil*. --- @field #string Status Status of spot e.g. AIRBASE.SpotStatus.FREE. +-- @field #string ClientSpot If `true`, this is a parking spot of a client aircraft. +-- @field #string ClientName Client unit name of this spot. +-- @field #string Status Status of spot e.g. `AIRBASE.SpotStatus.FREE`. -- @field #string OccupiedBy Name of the aircraft occupying the spot or "unknown". Can be *nil* if spot is not occupied. -- @field #string ReservedBy Name of the aircraft for which this spot is reserved. Can be *nil* if spot is not reserved. @@ -1081,11 +1082,11 @@ function AIRBASE:_InitParkingSpots() local Coord=COORDINATE:New(unit.x, unit.alt, unit.y) local dist=Coord:Get2DDistance(coord) if dist<2 then - return true + return true, clientname end end end - return false + return false, nil end -- Put coordinates of parking spots into table. @@ -1101,7 +1102,7 @@ function AIRBASE:_InitParkingSpots() park.TerminalID0=spot.Term_Index_0 park.TerminalType=spot.Term_Type park.TOAC=spot.TO_AC - park.ClientSpot=isClient(park.Coordinate) + park.ClientSpot, park.ClientName=isClient(park.Coordinate) self.NparkingTotal=self.NparkingTotal+1 @@ -2103,8 +2104,9 @@ end --- Get name of a given runway, e.g. "31L". -- @param #AIRBASE self -- @param #AIRBASE.Runway Runway The runway. Default is the active runway. +-- @param #boolean LongLeftRight If `true`, return "Left" or "Right" instead of "L" or "R". -- @return #string Name of the runway or "XX" if it could not be found. -function AIRBASE:GetRunwayName(Runway) +function AIRBASE:GetRunwayName(Runway, LongLeftRight) Runway=Runway or self:GetActiveRunway() @@ -2112,9 +2114,17 @@ function AIRBASE:GetRunwayName(Runway) if Runway then name=Runway.name if Runway.isLeft==true then - name=name.."L" + if LongLeftRight then + name=name.." Left" + else + name=name.."L" + end elseif Runway.isLeft==false then - name=name.."R" + if LongLeftRight then + name=name.." Right" + else + name=name.."R" + end end end From 8dd850d6853289619c76fc0284408376a86ee3f7 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 29 Jun 2022 22:25:08 +0200 Subject: [PATCH 16/20] OPS Operation --- Moose Development/Moose/Ops/Auftrag.lua | 2 +- Moose Development/Moose/Ops/Operation.lua | 886 +++++++++++++++------- Moose Development/Moose/Ops/OpsGroup.lua | 42 +- 3 files changed, 654 insertions(+), 276 deletions(-) diff --git a/Moose Development/Moose/Ops/Auftrag.lua b/Moose Development/Moose/Ops/Auftrag.lua index 6a70170ba..e6a5536d5 100644 --- a/Moose Development/Moose/Ops/Auftrag.lua +++ b/Moose Development/Moose/Ops/Auftrag.lua @@ -4474,7 +4474,7 @@ function AUFTRAG:onafterCancel(From, Event, To) else -- Debug info. - self:T(self.lid..string.format("No legion, commander or chief. Attached flights will cancel the mission on their own. Will wait for mission DONE before evaluation!")) + self:T(self.lid..string.format("No legion, commander or chief. Attached groups will cancel the mission on their own. Will wait for mission DONE before evaluation!")) -- Loop over all groups. for _,_groupdata in pairs(self.groupdata or {}) do diff --git a/Moose Development/Moose/Ops/Operation.lua b/Moose Development/Moose/Ops/Operation.lua index ff6b33438..a785fa7ed 100644 --- a/Moose Development/Moose/Ops/Operation.lua +++ b/Moose Development/Moose/Ops/Operation.lua @@ -27,33 +27,55 @@ -- @field #number verbose Verbosity level. -- @field #string lid Class id string for output to DCS log file. -- @field #string name Name of the operation. +-- @field Core.Condition#CONDITION conditionStart Start condition. +-- @field Core.Condition#CONDITION conditionStop Stop condition. +-- @field #table branches Branches. +-- @field #OPERATION.Branch branchMaster Master branch. +-- @field #OPERATION.Branch branchActive Active branch. +-- @field #number counterPhase Running number counting the phases. +-- @field #number counterBranch Running number counting the branches. +-- @field #OPERATION.Phase phase Currently active phase (if any). +-- @field #OPERATION.Phase phaseLast The phase that was active before the current one. -- @field #table cohorts Dedicated cohorts. -- @field #table legions Dedicated legions. --- @field #table phases Phases. --- @field #number counterPhase Running number counting the phases. --- @field #OPERATION.Phase phase Currently active phase (if any). -- @field #table targets Targets. -- @field #table missions Missions. --- -- @extends Core.Fsm#FSM ---- *A warrior's mission is to foster the success of others.* -- Morihei Ueshiba +--- *Before this time tomorrow I shall have gained a peerage, or Westminster Abbey.* -- Horatio Nelson -- -- === -- -- # The OPERATION Concept -- +-- This class allows you to create complex operations, which consist of multiple phases. Conditions can be specified, when a phase is over. If a phase is over, the next phase is started. +-- FSM events can be used to customize code that is executed at each phase. Phases can also switched manually, of course. +-- +-- In the simplest case, adding phases leads to a linear chain. However, you can also create branches to contruct a more tree like structure of phases. You can switch between branches +-- manually or add "edges" with conditions when to switch branches. We are diving a bit into graph theory here. So don't feel embarrassed at all, if you stick to linear chains. +-- +-- # Constructor +-- +-- A new operation can be created with the @{#OPERATION.New}(*Name*) function, where the parameter `Name` is a free to choose string. +-- +-- ## Adding Phases +-- +-- You can add phases with the @{#OPERATION.AddPhase}(*Name*, *Branch*) function. The first parameter `Name` is the name of the phase. The second parameter `Branch` is the branch to which the phase is +-- added. If this is omitted (nil), the phase is added to the default, *i.e.* "master branch". More about adding branches later. +-- +-- -- -- -- @field #OPERATION OPERATION = { ClassName = "OPERATION", verbose = 0, - lid = nil, - cohorts = {}, - legions = {}, - phases = {}, + branches = {}, counterPhase = 0, + counterBranch = 0, + counterEdge = 0, + cohorts = {}, + legions = {}, targets = {}, missions = {}, } @@ -67,6 +89,23 @@ _OPERATIONID=0 -- @field #string name Name of the phase. -- @field Core.Condition#CONDITION conditionOver Conditions when the phase is over. -- @field #string status Phase status. +-- @field #OPERATION.Branch branch The branch this phase belongs to. + +--- Operation branch. +-- @type OPERATION.Branch +-- @field #number uid Unique ID of the branch. +-- @field #string name Name of the branch. +-- @field #table phases Phases of this branch. +-- @field #table edges Edges of this branch. + +--- Operation edge. +-- @type OPERATION.Edge +-- @field #number uid Unique ID of the edge. +-- @field #OPERATION.Branch branchFrom The from branch. +-- @field #OPERATION.Phase phaseFrom The from phase after which to switch. +-- @field #OPERATION.Branch branchTo The branch to switch to. +-- @field #OPERATION.Phase phaseTo The phase to switch to. +-- @field Core.Condition#CONDITION conditionSwitch Conditions when to switch the branch. --- Operation phase. -- @type OPERATION.PhaseStatus @@ -87,7 +126,9 @@ OPERATION.version="0.1.0" -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: A lot +-- TODO: Braches? +-- TODO: Over conditions. +-- DONE: Phases. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor @@ -116,6 +157,12 @@ function OPERATION:New(Name) -- FMS start state is PLANNED. self:SetStartState("Planned") + + -- Master branch. + self.branchMaster=self:AddBranch("Master") + + -- Set master as active branch. + self.branchActive=self.branchMaster -- Add FSM transitions. -- From State --> Event --> To State @@ -127,8 +174,11 @@ function OPERATION:New(Name) self:AddTransition("Paused", "Unpause", "Running") self:AddTransition("*", "PhaseOver", "*") + self:AddTransition("*", "PhaseNext", "*") self:AddTransition("*", "PhaseChange", "*") + self:AddTransition("*", "BranchSwitch", "*") + self:AddTransition("*", "Over", "Over") self:AddTransition("*", "Stop", "Stopped") @@ -147,6 +197,7 @@ function OPERATION:New(Name) -- @param #OPERATION self -- @param #number delay Delay in seconds. + --- Triggers the FSM event "Stop". -- @function [parent=#OPERATION] Stop -- @param #OPERATION self @@ -156,6 +207,7 @@ function OPERATION:New(Name) -- @param #OPERATION self -- @param #number delay Delay in seconds. + --- Triggers the FSM event "StatusUpdate". -- @function [parent=#OPERATION] StatusUpdate -- @param #OPERATION self @@ -186,6 +238,23 @@ function OPERATION:New(Name) -- @param #OPERATION.Phase Phase The new phase. + --- Triggers the FSM event "PhaseNext". + -- @function [parent=#OPERATION] PhaseNext + -- @param #OPERATION self + + --- Triggers the FSM event "PhaseNext" after a delay. + -- @function [parent=#OPERATION] __PhaseNext + -- @param #OPERATION self + -- @param #number delay Delay in seconds. + + --- On after "PhaseNext" event. + -- @function [parent=#OPERATION] OnAfterPhaseNext + -- @param #OPERATION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- Triggers the FSM event "PhaseOver". -- @function [parent=#OPERATION] PhaseOver -- @param #OPERATION self @@ -206,6 +275,26 @@ function OPERATION:New(Name) -- @param #OPERATION.Phase Phase The phase that is over. + --- Triggers the FSM event "BranchSwitch". + -- @function [parent=#OPERATION] BranchSwitch + -- @param #OPERATION self + -- @param #OPERATION.Branch Branch The branch that is now active. + + --- Triggers the FSM event "BranchSwitch" after a delay. + -- @function [parent=#OPERATION] __BranchSwitch + -- @param #OPERATION self + -- @param #number delay Delay in seconds. + -- @param #OPERATION.Branch Branch The branch that is now active. + + --- On after "BranchSwitch" event. + -- @function [parent=#OPERATION] OnAfterBranchSwitch + -- @param #OPERATION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #OPERATION.Branch Branch The branch that is now active. + + --- Triggers the FSM event "Over". -- @function [parent=#OPERATION] Over -- @param #OPERATION self @@ -241,27 +330,385 @@ function OPERATION:SetVerbosity(VerbosityLevel) return self end ---- Create a new generic OPERATION object. +--- Set start and stop time of the operation. +-- @param #OPERATION self +-- @param #string ClockStart Time the mission is started, e.g. "05:00" for 5 am. If specified as a #number, it will be relative (in seconds) to the current mission time. Default is 5 seconds after mission was added. +-- @param #string ClockStop (Optional) Time the mission is stopped, e.g. "13:00" for 1 pm. If mission could not be started at that time, it will be removed from the queue. If specified as a #number it will be relative (in seconds) to the current mission time. +-- @return #OPERATION self +function OPERATION:SetTime(ClockStart, ClockStop) + + -- Current mission time. + local Tnow=timer.getAbsTime() + + -- Set start time. Default in 5 sec. + local Tstart=Tnow+5 + if ClockStart and type(ClockStart)=="number" then + Tstart=Tnow+ClockStart + elseif ClockStart and type(ClockStart)=="string" then + Tstart=UTILS.ClockToSeconds(ClockStart) + end + + -- Set stop time. Default nil. + local Tstop=nil + if ClockStop and type(ClockStop)=="number" then + Tstop=Tnow+ClockStop + elseif ClockStop and type(ClockStop)=="string" then + Tstop=UTILS.ClockToSeconds(ClockStop) + end + + self.Tstart=Tstart + self.Tstop=Tstop + + if Tstop then + self.duration=self.Tstop-self.Tstart + end + + return self +end + +--- Add a new phase to the operation. This is added add the end of all previously added phases (if any). -- @param #OPERATION self -- @param #string Name Name of the phase. Default "Phase-01" where the last number is a running number. +-- @param #OPERATION.Branch Branch The branch to which this phase is added. Default is the master branch. -- @return #OPERATION.Phase Phase table object. -function OPERATION:AddPhase(Name) +function OPERATION:AddPhase(Name, Branch) - -- Increase phase counter. - self.counterPhase=self.counterPhase+1 + -- Branch. + Branch=Branch or self.branchMaster - local phase={} --#OPERATION.Phase - phase.uid=self.counterPhase - phase.name=Name or string.format("Phase-%02d", self.counterPhase) - phase.conditionOver=CONDITION:New(Name.." Over") - phase.status=OPERATION.PhaseStatus.PLANNED + -- Create a new phase. + local phase=self:_CreatePhase(Name) + + -- Branch of phase + phase.branch=Branch + + + -- Debug output. + self:T(self.lid..string.format("Adding phase %s to branch %s", phase.name, Branch.name)) -- Add phase. - table.insert(self.phases, phase) + table.insert(Branch.phases, phase) return phase end +---Insert a new phase after an already defined phase of the operation. +-- @param #OPERATION self +-- @param #OPERATION.Phase PhaseAfter The phase after which the new phase is inserted. +-- @param #string Name Name of the phase. Default "Phase-01" where the last number is a running number. +-- @return #OPERATION.Phase Phase table object. +function OPERATION:InsertPhaseAfter(PhaseAfter, Name) + + for i=1,#self.phases do + local phase=self.phases[i] --#OPERATION.Phase + if PhaseAfter.uid==phase.uid then + + -- Create a new phase. + local phase=self:_CreatePhase(Name) + + + end + end + + return nil +end + + +--- Get a phase by its name. +-- @param #OPERATION self +-- @param #string Name Name of the phase. Default "Phase-01" where the last number is a running number. +-- @return #OPERATION.Phase Phase table object or nil if phase could not be found. +function OPERATION:GetPhaseByName(Name) + + for _,_branch in pairs(self.branches) do + local branch=_branch --#OPERATION.Branch + for _,_phase in pairs(branch.phases or {}) do + local phase=_phase --#OPERATION.Phase + if phase.name==Name then + return phase + end + end + end + + return nil +end + +--- Set status of a phase. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase. +-- @param #string Status New status, *e.g.* `OPERATION.PhaseStatus.OVER`. +-- @return #OPERATION self +function OPERATION:SetPhaseStatus(Phase, Status) + if Phase then + self:T(self.lid..string.format("Phase %s status: %s-->%s"), Phase.status, Status) + Phase.status=Status + end + return self +end + +--- Get status of a phase. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase. +-- @return #string Phase status, *e.g.* `OPERATION.PhaseStatus.OVER`. +function OPERATION:GetPhaseStatus(Phase) + return Phase.status +end + +--- Set codition when the given phase is over. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase. +-- @param Core.Condition#CONDITION Condition Condition when the phase is over. +-- @return #OPERATION self +function OPERATION:SetPhaseConditonOver(Phase, Condition) + if Phase then + self:T(self.lid..string.format("Setting phase %s conditon over %s"), Phase.name, Condition and Condition.name or "None") + Phase.conditionOver=Condition + end + return self +end + +--- Add codition function when the given phase is over. Must return a `#boolean`. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase. +-- @param #function Function Function that needs to be `true`before the phase is over. +-- @param ... Condition function arguments if any. +-- @return #OPERATION self +function OPERATION:AddPhaseConditonOverAll(Phase, Function, ...) + if Phase then + Phase.conditionOver:AddFunctionAll(Function, ...) + end + return self +end + +--- Add condition function when the given phase is over. Must return a `#boolean`. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase. +-- @param #function Function Function that needs to be `true` before the phase is over. +-- @param ... Condition function arguments if any. +-- @return #OPERATION self +function OPERATION:AddPhaseConditonOverAny(Phase, Function, ...) + if Phase then + Phase.conditionOver:AddFunctionAny(Function, ...) + end + return self +end + + +--- Get codition when the given phase is over. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase. +-- @return Core.Condition#CONDITION Condition when the phase is over (if any). +function OPERATION:GetPhaseConditonOver(Phase, Condition) + return Phase.conditionOver +end + +--- Get currrently active phase. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase. +-- @param #string Status New status, e.g. `OPERATION.PhaseStatus.OVER`. +-- @return #OPERATION self +function OPERATION:SetPhaseStatus(Phase, Status) + if Phase then + self:T(self.lid..string.format("Phase \"%s\" status: %s-->%s", Phase.name, Phase.status, Status)) + Phase.status=Status + end + return self +end + +--- Get currrently active phase. +-- @param #OPERATION self +-- @return #OPERATION.Phase Current phase or `nil` if no current phase is active. +function OPERATION:GetPhaseActive() + return self.phase +end + +--- Get name of a phase. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase of which the name is returned. Default is the currently active phase. +-- @return #string The name of the phase or "None" if no phase is given or active. +function OPERATION:GetPhaseName(Phase) + + Phase=Phase or self.phase + + if Phase then + return Phase.name + end + + return "None" +end + +--- Check if a phase is the currently active one. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase to check. +-- @return #boolean If `true`, this phase is currently active. +function OPERATION:IsPhaseActive(Phase) + local phase=self:GetPhaseActive() + if phase and phase.uid==Phase.uid then + return true + else + return false + end + return nil +end + +--- Get index of phase. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase. +-- @return #number The index. +-- @return #OPERATION.Branch The branch. +function OPERATION:GetPhaseIndex(Phase) + + local branch=Phase.branch + + for i,_phase in pairs(branch.phases) do + local phase=_phase --#OPERATION.Phase + if phase.uid==Phase.uid then + return i, branch + end + end + + return nil +end + +--- Get next phase. +-- @param #OPERATION self +-- @param #OPERATION.Branch Branch (Optional) The branch from which the next phase is retrieved. Default is the currently active branch. +-- @param #string PhaseStatus (Optional) Only return a phase, which is in this status. For example, `OPERATION.PhaseStatus.PLANNED` to make sure, the next phase is planned. +-- @return #OPERATION.Phase Next phase or `nil` if no next phase exists. +function OPERATION:GetPhaseNext(Branch, PhaseStatus) + + -- Branch. + Branch=Branch or self:GetBranchActive() + + -- The phases of the branch. + local phases=Branch.phases or {} + + local phase=nil + if self.phase and self.phase.branch.uid==Branch.uid then + phase=self.phase + end + + -- Number of phases. + local N=#phases + + -- Debug message. + self:T(self.lid..string.format("Getting next phase! Branch=%s, Phases=%d, Status=%s", Branch.name, N, tostring(PhaseStatus))) + + if N>0 then + + -- Check if there there is an active phase already. + if phase==nil and PhaseStatus==nil then + return phases[1] + end + + local n=1 + + if phase then + n=self:GetPhaseIndex(phase)+1 + end + + for i=n,N do + local phase=phases[i] --#OPERATION.Phase + + if PhaseStatus==nil or PhaseStatus==phase.status then + return phase + end + + end + + end + + return nil +end + +--- Count phases. +-- @param #OPERATION self +-- @param #string Status (Optional) Only count phases in a certain status, e.g. `OPERATION.PhaseStatus.PLANNED`. +-- @param #OPERATION.Branch (Optional) Branch. +-- @return #number Number of phases +function OPERATION:CountPhases(Status, Branch) + + Branch=Branch or self.branchActive + + local N=0 + for _,_phase in pairs(Branch.phases) do + local phase=_phase --#OPERATION.Phase + if Status==nil or Status==phase.status then + N=N+1 + end + end + + return N +end + + +--- Add a new branch to the operation. +-- @param #OPERATION self +-- @return #OPERATION.Branch Branch table object. +function OPERATION:AddBranch(Name) + + -- Create a new branch. + local branch=self:_CreateBranch(Name) + + -- Add phase. + table.insert(self.branches, branch) + + return branch +end + +--- Get the currently active branch. +-- @param #OPERATION self +-- @return #OPERATION.Branch The active branch. If no branch is active, the master branch is returned. +function OPERATION:GetBranchActive() + return self.branchActive or self.branchMaster +end + +--- Get name of the branch. +-- @param #OPERATION self +-- @param #OPERATION.Branch Branch The branch of which the name is requested. Default is the currently active or master branch. +function OPERATION:GetBranchName(Branch) + Branch=Branch or self:GetBranchActive() + if Branch then + return Branch.name + end + return "None" +end + +--- Add an edge between two branches. +-- @param #OPERATION self +-- @param #OPERATION.Branch BranchTo The branch *to* which to switch. +-- @param #OPERATION.Phase PhaseAfter The phase of the *from* branch *after* which to switch. +-- @param #OPERATION.Phase PhaseNext The phase of the *to* branch *to* which to switch. +-- @param Core.Condition#CONDITION ConditionSwitch (Optional) Condition(s) when to switch the branches. +-- @return #OPERATION.Branch Branch table object. +function OPERATION:AddEdge(BranchTo, PhaseAfter, PhaseNext, ConditionSwitch) + + local edge={} --#OPERATION.Edge + + edge.branchFrom=PhaseAfter and PhaseAfter.branch or self.branchMaster + edge.phaseFrom=PhaseAfter + edge.branchTo=BranchTo + edge.phaseTo=PhaseNext + edge.conditionSwitch=ConditionSwitch or CONDITION:New("Edge") + + table.insert(edge.branchFrom.edges, edge) + + return edge +end + +--- Add condition function to an edge when branches are switched. The function must return a `#boolean`. +-- @param #OPERATION self +-- @param #OPERATION.Edge Edge The edge connecting the two branches. +-- @param #function Function Function that needs to be `true` for switching between the branches. +-- @param ... Condition function arguments if any. +-- @return #OPERATION self +function OPERATION:AddEdgeConditonSwitchAll(Edge, Function, ...) + if Edge then + Edge.conditionSwitch:AddFunctionAll(Function, ...) + end + return self +end + --- Add mission to operation. -- @param #OPERATION self -- @param Ops.Auftrag#AUFTRAG Mission The mission to add. @@ -291,21 +738,22 @@ function OPERATION:AddTarget(Target, Phase) end - ---- Get a phase by its name. +--- Count targets alive. -- @param #OPERATION self --- @param #string Name Name of the phase. Default "Phase-01" where the last number is a running number. --- @return #OPERATION.Phase Phase table object or nil if phase could not be found. -function OPERATION:GetPhaseByName(Name) +-- @param #OPERATION.Phase Phase (Optional) Only count targets set for this phase. +-- @return #number Number of phases +function OPERATION:CountTargets(Phase) - for _,_phase in pairs(self.phases or {}) do - local phase=_phase --#OPERATION.Phase - if phase.name==Name then - return phase + local N=0 + for _,_target in pairs(self.targets) do + local target=_target --Ops.Target#TARGET + + if target:IsAlive() and (Phase==nil or target.phase==Phase) then + N=N+1 end end - return nil + return N end --- Assign cohort to operation. @@ -319,7 +767,7 @@ function OPERATION:AssignCohort(Cohort) end ---- Assign legion to operation. All cohorts of this legion will be assigned and are only available +--- Assign legion to operation. All cohorts of this legion will be assigned and are only available. -- @param #OPERATION self -- @param Ops.Legion#LEGION Legion The legion to be assigned. -- @return #OPERATION self @@ -392,214 +840,6 @@ function OPERATION:IsAssignedCohortOrLegion(Object) return isAssigned end ---- Set start and stop time of the operation. --- @param #OPERATION self --- @param #string ClockStart Time the mission is started, e.g. "05:00" for 5 am. If specified as a #number, it will be relative (in seconds) to the current mission time. Default is 5 seconds after mission was added. --- @param #string ClockStop (Optional) Time the mission is stopped, e.g. "13:00" for 1 pm. If mission could not be started at that time, it will be removed from the queue. If specified as a #number it will be relative (in seconds) to the current mission time. --- @return #OPERATION self -function OPERATION:SetTime(ClockStart, ClockStop) - - -- Current mission time. - local Tnow=timer.getAbsTime() - - -- Set start time. Default in 5 sec. - local Tstart=Tnow+5 - if ClockStart and type(ClockStart)=="number" then - Tstart=Tnow+ClockStart - elseif ClockStart and type(ClockStart)=="string" then - Tstart=UTILS.ClockToSeconds(ClockStart) - end - - -- Set stop time. Default nil. - local Tstop=nil - if ClockStop and type(ClockStop)=="number" then - Tstop=Tnow+ClockStop - elseif ClockStop and type(ClockStop)=="string" then - Tstop=UTILS.ClockToSeconds(ClockStop) - end - - self.Tstart=Tstart - self.Tstop=Tstop - - if Tstop then - self.duration=self.Tstop-self.Tstart - end - - return self -end - ---- Set status of a phase. --- @param #OPERATION self --- @param #OPERATION.Phase Phase The phase. --- @param #string Status New status, *e.g.* `OPERATION.PhaseStatus.OVER`. --- @return #OPERATION self -function OPERATION:SetPhaseStatus(Phase, Status) - if Phase then - self:T(self.lid..string.format("Phase %s status: %s-->%s"), Phase.status, Status) - Phase.status=Status - end - return self -end - ---- Get status of a phase. --- @param #OPERATION self --- @param #OPERATION.Phase Phase The phase. --- @return #string Phase status, *e.g.* `OPERATION.PhaseStatus.OVER`. -function OPERATION:GetPhaseStatus(Phase) - return Phase.status -end - ---- Set codition when the given phase is over. --- @param #OPERATION self --- @param #OPERATION.Phase Phase The phase. --- @param Core.Condition#CONDITION Condition Condition when the phase is over. --- @return #OPERATION self -function OPERATION:SetPhaseConditonOver(Phase, Condition) - if Phase then - self:T(self.lid..string.format("Setting phase %s conditon over %s"), Phase.name, Condition and Condition.name or "None") - Phase.conditionOver=Condition - end - return self -end - ---- Add codition function when the given phase is over. Must return a `#boolean`. --- @param #OPERATION self --- @param #OPERATION.Phase Phase The phase. --- @param #function Function Function that needs to be `true`before the phase is over. --- @param ... Condition function arguments if any. --- @return #OPERATION self -function OPERATION:AddPhaseConditonOverAll(Phase, Function, ...) - if Phase then - Phase.conditionOver:AddFunctionAll(Function, ...) - end - return self -end - ---- Add codition function when the given phase is over. Must return a `#boolean`. --- @param #OPERATION self --- @param #OPERATION.Phase Phase The phase. --- @param #function Function Function that needs to be `true`before the phase is over. --- @param ... Condition function arguments if any. --- @return #OPERATION self -function OPERATION:AddPhaseConditonOverAny(Phase, Function, ...) - if Phase then - Phase.conditionOver:AddFunctionAny(Function, ...) - end - return self -end - - ---- Get codition when the given phase is over. --- @param #OPERATION self --- @param #OPERATION.Phase Phase The phase. --- @return Core.Condition#CONDITION Condition when the phase is over (if any). -function OPERATION:GetPhaseConditonOver(Phase, Condition) - return Phase.conditionOver -end - ---- Get currrently active phase. --- @param #OPERATION self --- @param #OPERATION.Phase Phase The phase. --- @param #string Status New status, e.g. `OPERATION.PhaseStatus.OVER`. --- @return #OPERATION self -function OPERATION:SetPhaseStatus(Phase, Status) - if Phase then - self:T(self.lid..string.format("Phase \"%s\" status: %s-->%s", Phase.name, Phase.status, Status)) - Phase.status=Status - end - return self -end - ---- Get currrently active phase. --- @param #OPERATION self --- @return #OPERATION.Phase Current phase or `nil` if no current phase is active. -function OPERATION:GetPhaseActive() - return self.phase -end - ---- Get name of a phase. --- @param #OPERATION self --- @param #OPERATION.Phase Phase The phase of which the name is returned. --- @return #string The name of the phase. -function OPERATION:GetPhaseName(Phase) - - Phase=Phase or self.phase - - if Phase then - return Phase.name - else - return "None" - end - -end - ---- Check if a phase is the currently active one. --- @param #OPERATION self --- @param #OPERATION.Phase Phase The phase to check. --- @return #boolean If `true`, this phase is currently active. -function OPERATION:IsPhaseActive(Phase) - local phase=self:GetPhaseActive() - if phase and phase.uid==Phase.uid then - return true - else - return false - end - return nil -end - ---- Get next phase. --- @param #OPERATION self --- @return #OPERATION.Phase Next phase or `nil` if no next phase exists. -function OPERATION:GetPhaseNext() - - for _,_phase in pairs(self.phases or {}) do - local phase=_phase --#OPERATION.Phase - - if phase.status==OPERATION.PhaseStatus.PLANNED then - -- Return first phase that is not over. - return phase - end - - end - - return nil -end - ---- Count phases. --- @param #OPERATION self --- @param #string Status (Optional) Only count phases in a certain status, e.g. `OPERATION.PhaseStatus.PLANNED`. --- @return #number Number of phases -function OPERATION:CountPhases(Status) - - local N=0 - for _,_phase in pairs(self.phases) do - local phase=_phase --#OPERATION.Phase - if Status==nil or Status==phase.status then - N=N+1 - end - end - - return N -end - ---- Count targets alive. --- @param #OPERATION self --- @param #OPERATION.Phase Phase (Optional) Only count targets set for this phase. --- @return #number Number of phases -function OPERATION:CountTargets(Phase) - - local N=0 - for _,_target in pairs(self.targets) do - local target=_target --Ops.Target#TARGET - - if target:IsAlive() and (Phase==nil or target.phase==Phase) then - N=N+1 - end - end - - return N -end - --- Check if operation is in FSM state "Planned". -- @param #OPERATION self -- @return #boolean If `true`, operation is "Planned". @@ -624,6 +864,14 @@ function OPERATION:IsPaused() return is end +--- Check if operation is in FSM state "Over". +-- @param #OPERATION self +-- @return #boolean If `true`, operation is "Over". +function OPERATION:IsOver() + local is=self:is("Over") + return is +end + --- Check if operation is in FSM state "Stopped". -- @param #OPERATION self -- @return #boolean If `true`, operation is "Stopped". @@ -632,6 +880,7 @@ function OPERATION:IsStopped() return is end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Status Update ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -662,6 +911,22 @@ function OPERATION:onafterStatusUpdate(From, Event, To) -- Current FSM state. local fsmstate=self:GetState() + if self:IsPlanned() then + if self.Tstart and Tnow>self.Tstart then + self:Start() + end + end + if (self.Tstop and Tnow>self.Tstop) and not (self:IsOver() or self:IsStopped()) then + self:Over() + end + + if (not self:IsRunning()) and (self.conditionStart and self.conditionStart:Evaluate()) then + self:Start() + end + if self:IsRunning() and (self.conditionStop and self.conditionStop:Evaluate()) then + self:Over() + end + -- Check phases. if self:IsRunning() then self:_CheckPhases() @@ -671,18 +936,15 @@ function OPERATION:onafterStatusUpdate(From, Event, To) if self.verbose>=1 then -- Current phase. - local currphase=self:GetPhaseActive() - local phaseName="None" - if currphase then - phaseName=currphase.name - end + local phaseName=self:GetPhaseName() + local branchName=self:GetBranchName() local NphaseTot=self:CountPhases() local NphaseAct=self:CountPhases(OPERATION.PhaseStatus.ACTIVE) local NphasePla=self:CountPhases(OPERATION.PhaseStatus.PLANNED) local NphaseOvr=self:CountPhases(OPERATION.PhaseStatus.OVER) -- General info. - local text=string.format("State=%s: Phase=%s, Phases=%d [Active=%d, Planned=%d, Over=%d]", fsmstate, phaseName, NphaseTot, NphaseAct, NphasePla, NphaseOvr) + local text=string.format("State=%s: Phase=%s [%s], Phases=%d [Active=%d, Planned=%d, Over=%d]", fsmstate, phaseName, branchName, NphaseTot, NphaseAct, NphasePla, NphaseOvr) self:I(self.lid..text) end @@ -692,7 +954,7 @@ function OPERATION:onafterStatusUpdate(From, Event, To) -- Info on phases. local text="Phases:" - for i,_phase in pairs(self.phases) do + for i,_phase in pairs(self.branchActive.phases) do local phase=_phase --#OPERATION.Phase text=text..string.format("\n[%d] %s: status=%s", i, phase.name, tostring(phase.status)) end @@ -709,6 +971,32 @@ end -- FSM Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- On after "PhaseNext" event. +-- @param #OPERATION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPERATION.Phase Phase The new phase. +function OPERATION:onafterPhaseNext(From, Event, To) + + -- Get next phase. + local Phase=self:GetPhaseNext() + + if Phase then + + -- Change phase to next one. + self:PhaseChange(Phase) + + else + + -- No further phases defined ==> Operation is over. + self:Over() + + end + +end + + --- On after "PhaseChange" event. -- @param #OPERATION self -- @param #string From From state. @@ -735,6 +1023,22 @@ function OPERATION:onafterPhaseChange(From, Event, To, Phase) end +--- On after "BranchSwitch" event. +-- @param #OPERATION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPERATION.Branch Branch The new branch. +function OPERATION:onafterBranchSwitch(From, Event, To, Branch) + + -- Debug info. + self:T(self.lid..string.format("Switching to branch %s", Branch.name)) + + -- Set active branch. + self.branchActive=Branch + +end + --- On after "Over" event. -- @param #OPERATION self -- @param #string From From state. @@ -750,15 +1054,17 @@ function OPERATION:onafterOver(From, Event, To) self.phase=nil -- Set all phases to OVER. - for _,_phase in pairs(self.phases) do - local phase=_phase --#OPERATION.Phase - self:SetPhaseStatus(phase, OPERATION.PhaseStatus.OVER) - end - + for _,_branch in pairs(self.branches) do + local branch=_branch --#OPERATION.Branch + for _,_phase in pairs(branch.phases) do + local phase=_phase --#OPERATION.Phase + self:SetPhaseStatus(phase, OPERATION.PhaseStatus.OVER) + end + end end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Misc Functions +-- Misc (private) Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Check phases. @@ -779,25 +1085,79 @@ function OPERATION:_CheckPhases() -- If no current phase or current phase is over, get next phase. if phase==nil or phase.status==OPERATION.PhaseStatus.OVER then - -- Get next phase. - local Phase=self:GetPhaseNext() - - if Phase then - - -- Change phase to next one. - self:PhaseChange(Phase) + for _,_edge in pairs(self.branchActive.edges) do + local edge=_edge --#OPERATION.Edge + + if (edge.phaseFrom==nil) or (phase and edge.phaseFrom.uid==phase.uid) then + + -- Evaluate switch condition. + local switch=edge.conditionSwitch:Evaluate() + + if switch then + + -- Switch to new branch. + self:BranchSwitch(edge.branchTo) + + -- If we want to switch to a specific phase of the branch. + if edge.phaseTo then + + -- Change phase. + self:PhaseChange(edge.phaseTo) - else - - -- No further phases defined ==> Operation is over. - self:Over() + -- Done here! + return + end + + -- Break the loop. + break + end + end end + -- Next phase. + self:PhaseNext() + end end +--- Create a new phase object. +-- @param #OPERATION self +-- @param #string Name Name of the phase. Default "Phase-01" where the last number is a running number. +-- @return #OPERATION.Phase Phase table object. +function OPERATION:_CreatePhase(Name) + + -- Increase phase counter. + self.counterPhase=self.counterPhase+1 + + local phase={} --#OPERATION.Phase + phase.uid=self.counterPhase + phase.name=Name or string.format("Phase-%02d", self.counterPhase) + phase.conditionOver=CONDITION:New(Name.." Over") + phase.status=OPERATION.PhaseStatus.PLANNED + + return phase +end + +--- Create a new branch object. +-- @param #OPERATION self +-- @param #string Name Name of the phase. Default "Phase-01" where the last number is a running number. +-- @return #OPERATION.Branch Branch table object. +function OPERATION:_CreateBranch(Name) + + -- Increase phase counter. + self.counterBranch=self.counterBranch+1 + + local branch={} --#OPERATION.Branch + branch.uid=self.counterBranch + branch.name=Name or string.format("Branch-%02d", self.counterBranch) + branch.phases={} + branch.edges={} + + return branch +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index 91f5f80e1..f3f4932bb 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -4711,9 +4711,13 @@ end -- @return #OPSGROUP self function OPSGROUP:RemoveMission(Mission) - for i,_mission in pairs(self.missionqueue) do - local mission=_mission --Ops.Auftrag#AUFTRAG + --for i,_mission in pairs(self.missionqueue) do + for i=#self.missionqueue,1,-1 do + + -- Mission. + local mission=self.missionqueue[i] --Ops.Auftrag#AUFTRAG + -- Check mission ID. if mission.auftragsnummer==Mission.auftragsnummer then -- Remove mission waypoint task. @@ -4724,9 +4728,10 @@ function OPSGROUP:RemoveMission(Mission) end -- Take care of a paused mission. - for j,mid in pairs(self.pausedmissions) do + for j=#self.pausedmissions,1,-1 do + local mid=self.pausedmissions[j] if Mission.auftragsnummer==mid then - table.remove(self.pausedmission, j) + table.remove(self.pausedmissions, j) end end @@ -5161,6 +5166,7 @@ function OPSGROUP:onafterMissionCancel(From, Event, To, Mission) --- -- Some missions dont have a task set, which could be cancelled. + --[[ if Mission.type==AUFTRAG.Type.ALERT5 or Mission.type==AUFTRAG.Type.ONGUARD or Mission.type==AUFTRAG.Type.ARMOREDGUARD or @@ -5173,19 +5179,31 @@ function OPSGROUP:onafterMissionCancel(From, Event, To, Mission) return end + ]] -- Get mission waypoint task. local Task=Mission:GetGroupWaypointTask(self) + + if Task then - -- Debug info. - self:T(self.lid..string.format("Cancel current mission %s. Task=%s", tostring(Mission.name), tostring(Task and Task.description or "WTF"))) + -- Debug info. + self:T(self.lid..string.format("Cancel current mission %s. Task=%s", tostring(Mission.name), tostring(Task and Task.description or "WTF"))) + + -- Cancelling the mission is actually cancelling the current task. + -- Note that two things can happen. + -- 1.) Group is still on the way to the waypoint (status should be STARTED). In this case there would not be a current task! + -- 2.) Group already passed the mission waypoint (status should be EXECUTING). + + self:TaskCancel(Task) + + else + + -- Some missions dont have a task set, which could be cancelled. - -- Cancelling the mission is actually cancelling the current task. - -- Note that two things can happen. - -- 1.) Group is still on the way to the waypoint (status should be STARTED). In this case there would not be a current task! - -- 2.) Group already passed the mission waypoint (status should be EXECUTING). - - self:TaskCancel(Task) + -- Trigger mission don task. + self:MissionDone(Mission) + + end else From 0dc26216c2ad01401a994feec90d0f9cb62cc58e Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 1 Jul 2022 23:00:25 +0200 Subject: [PATCH 17/20] OPS --- Moose Development/Moose/Ops/Auftrag.lua | 8 ++++++-- Moose Development/Moose/Ops/Operation.lua | 2 +- Moose Development/Moose/Wrapper/Airbase.lua | 4 +++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Moose Development/Moose/Ops/Auftrag.lua b/Moose Development/Moose/Ops/Auftrag.lua index e6a5536d5..c89e9c3a7 100644 --- a/Moose Development/Moose/Ops/Auftrag.lua +++ b/Moose Development/Moose/Ops/Auftrag.lua @@ -2454,9 +2454,13 @@ end --- Set that mission assets are teleported to the mission execution waypoint. -- @param #AUFTRAG self +-- @param #boolean Switch If `true` or `nil`, teleporting is on. If `false`, teleporting is off. -- @return #AUFTRAG self -function AUFTRAG:SetTeleport() - self.teleport=true +function AUFTRAG:SetTeleport(Switch) + if Switch==nil then + Switch=true + end + self.teleport=Switch return self end diff --git a/Moose Development/Moose/Ops/Operation.lua b/Moose Development/Moose/Ops/Operation.lua index a785fa7ed..158af4fd4 100644 --- a/Moose Development/Moose/Ops/Operation.lua +++ b/Moose Development/Moose/Ops/Operation.lua @@ -1013,7 +1013,7 @@ function OPERATION:onafterPhaseChange(From, Event, To, Phase) end -- Debug message. - self:T(self.lid..string.format("Phase change: %s --> %s", oldphase, Phase.name)) + self:I(self.lid..string.format("Phase change: %s --> %s", oldphase, Phase.name)) -- Set currently active phase. self.phase=Phase diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index d4e2d81f4..53113da33 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -637,7 +637,9 @@ function AIRBASE:Register(AirbaseName) self:_InitRunways() -- Set the active runways based on wind direction. - self:SetActiveRunway() + if self.isAirdrome then + self:SetActiveRunway() + end -- Init parking spots. self:_InitParkingSpots() From 469cc3d508bec31cc91b96e83895647a4d0cf4d6 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 11 Jul 2022 23:22:58 +0200 Subject: [PATCH 18/20] OPS CHIEF: SetResponseOnTarget TRANSPORT: improved transfer of loaded cargo --- Moose Development/Moose/Ops/Brigade.lua | 10 +- Moose Development/Moose/Ops/Chief.lua | 179 +++++++++++++++++-- Moose Development/Moose/Ops/Cohort.lua | 2 +- Moose Development/Moose/Ops/Fleet.lua | 9 + Moose Development/Moose/Ops/FlightGroup.lua | 14 +- Moose Development/Moose/Ops/Legion.lua | 16 +- Moose Development/Moose/Ops/OpsGroup.lua | 51 ++++-- Moose Development/Moose/Ops/OpsTransport.lua | 2 +- 8 files changed, 236 insertions(+), 47 deletions(-) diff --git a/Moose Development/Moose/Ops/Brigade.lua b/Moose Development/Moose/Ops/Brigade.lua index b8e5d1c49..3ce1e6be1 100644 --- a/Moose Development/Moose/Ops/Brigade.lua +++ b/Moose Development/Moose/Ops/Brigade.lua @@ -92,7 +92,15 @@ function BRIGADE:New(WarehouseName, BrigadeName) -- Defaults self:SetRetreatZones() - + + -- Turn ship into NAVYGROUP. + if self:IsShip() then + local wh=self.warehouse --Wrapper.Unit#UNIT + local group=wh:GetGroup() + self.warehouseOpsGroup=NAVYGROUP:New(group) --Ops.NavyGroup#NAVYGROUP + self.warehouseOpsElement=self.warehouseOpsGroup:GetElementByName(wh:GetName()) + end + -- Add FSM transitions. -- From State --> Event --> To State self:AddTransition("*", "ArmyOnMission", "*") -- An ARMYGROUP was send on a Mission (AUFTRAG). diff --git a/Moose Development/Moose/Ops/Chief.lua b/Moose Development/Moose/Ops/Chief.lua index 9471147ec..e602a1831 100644 --- a/Moose Development/Moose/Ops/Chief.lua +++ b/Moose Development/Moose/Ops/Chief.lua @@ -256,6 +256,17 @@ CHIEF.Strategy = { -- @field #string MissionType Mission Type. -- @field #number Performance Performance: a number between 0 and 100, where 100 is best performance. +--- Asset numbers for detected targets. +-- @type CHIEF.AssetNumber +-- @field #number nAssetMin Min number of assets. +-- @field #number nAssetMax Max number of assets. +-- @field #number threatlevel Threat level. +-- @field #string targetCategory Target category. +-- @field #string missionType Mission type. +-- @field #number nUnits Number of enemy units. +-- @field #string defcon Defense condition. +-- @field #string strategy Strategy. + --- Strategic zone. -- @type CHIEF.StrategicZone -- @field Ops.OpsZone#OPSZONE opszone OPS zone. @@ -773,6 +784,153 @@ function CHIEF:DeleteFromResource(Resource, MissionType) return self end +--- Set number of assets requested for detected targets. +-- @param #CHIEF self +-- @param #number NassetsMin Min number of assets. Should be at least 1. Default 1. +-- @param #number NassetsMax Max number of assets. Default is same as `NassetsMin`. +-- @param #number ThreatLevel Only apply this setting if the target threat level is greater or equal this number. Default 0. +-- @param #string TargetCategory Only apply this setting if the target is of this category, e.g. `TARGET.Category.AIRCRAFT`. +-- @param #string MissionType Only apply this setting for this mission type, e.g. `AUFTRAG.Type.INTERCEPT`. +-- @param #string Nunits Only apply this setting if the number of enemy units is greater or equal this number. +-- @param #string Defcon Only apply this setting if this defense condition is in place. +-- @param #string Strategy Only apply this setting if this strategy is in currently. place. +-- @return #CHIEF self +function CHIEF:SetResponseOnTarget(NassetsMin, NassetsMax, ThreatLevel, TargetCategory, MissionType, Nunits, Defcon, Strategy) + + local bla={} --#CHIEF.AssetNumber + + bla.nAssetMin=NassetsMin or 1 + bla.nAssetMax=NassetsMax or bla.nAssetMin + bla.threatlevel=ThreatLevel or 0 + bla.targetCategory=TargetCategory + bla.missionType=MissionType + bla.nUnits=Nunits or 1 + bla.defcon=Defcon + bla.strategy=Strategy + + self.assetNumbers=self.assetNumbers or {} + + -- Add to table. + table.insert(self.assetNumbers, bla) + +end + +--- Add mission type and number of required assets to resource. +-- @param #CHIEF self +-- @param Ops.Target#TARGET Target The target. +-- @param #string MissionType Mission type. +-- @return #number Number of min assets. +-- @return #number Number of max assets. +function CHIEF:_GetAssetsForTarget(Target, MissionType) + + -- Threat level. + local threatlevel=Target:GetThreatLevelMax() + + -- Number of units. + local nUnits=Target.N0 + + -- Target category. + local targetcategory=Target:GetCategory() + + -- Debug info. + self:T(self.lid..string.format("Getting number of assets for target with TL=%d, Category=%s, nUnits=%s, MissionType=%s", threatlevel, targetcategory, nUnits, tostring(MissionType))) + + -- Candidates. + local candidates={} + + local threatlevelMatch=nil + for _,_assetnumber in pairs(self.assetNumbers or {}) do + local assetnumber=_assetnumber --#CHIEF.AssetNumber + + if (threatlevelMatch==nil and threatlevel>=assetnumber.threatlevel) or (threatlevelMatch~=nil and threatlevelMatch==threatlevel) then + + if threatlevelMatch==nil then + threatlevelMatch=threatlevel + end + + -- Number of other parameters matching. + local nMatch=0 + + -- Assume cand. + local cand=true + + if assetnumber.targetCategory~=nil then + if assetnumber.targetCategory==targetcategory then + nMatch=nMatch+1 + else + cand=false + end + end + + if MissionType and assetnumber.missionType~=nil then + if assetnumber.missionType==MissionType then + nMatch=nMatch+1 + else + cand=false + end + end + + if assetnumber.nUnits~=nil then + if assetnumber.nUnits>=nUnits then + nMatch=nMatch+1 + else + cand=false + end + end + + if assetnumber.defcon~=nil then + if assetnumber.defcon==self.Defcon then + nMatch=nMatch+1 + else + cand=false + end + end + + if assetnumber.strategy~=nil then + if assetnumber.strategy==self.strategy then + nMatch=nMatch+1 + else + cand=false + end + end + + -- Add to candidates. + if cand then + table.insert(candidates, {assetnumber=assetnumber, nMatch=nMatch}) + end + + end + + end + + if #candidates>0 then + + -- Return greater match. + local function _sort(a,b) + return a.nMatch>b.nMatch + end + + -- Sort table by matches. + table.sort(candidates, _sort) + + -- Pick the candidate with most matches. + local candidate=candidates[1] + + -- Asset number. + local an=candidate.assetnumber --#CHIEF.AssetNumber + + -- Debug message. + self:T(self.lid..string.format("Picking candidate with %d matches: NassetsMin=%d, NassetsMax=%d, ThreatLevel=%d, TargetCategory=%s, MissionType=%s, Defcon=%s, Strategy=%s", + candidate.nMatch, an.nAssetMin, an.nAssetMax, an.threatlevel, tostring(an.targetCategory), tostring(an.missionType), tostring(an.defcon), tostring(an.strategy))) + + -- Return number of assetes. + return an.nAssetMin, an.nAssetMax + else + return 1, 1 + end + +end + --- Get defence condition. -- @param #CHIEF self -- @param #string Current Defence condition. See @{#CHIEF.DEFCON}, e.g. `CHIEF.DEFCON.RED`. @@ -2084,24 +2242,6 @@ function CHIEF:CheckTargetQueue() local Legions=nil if #MissionPerformances>0 then - - --TODO: Number of required assets. How many do we want? Should depend on: - -- * number of enemy units - -- * target threatlevel - -- * how many assets are still in stock - -- * is it inside of our border - -- * add damping factor - - local NassetsMin=1 - local NassetsMax=1 - - if threatlevel>=8 and target.N0 >=10 then - NassetsMax=3 - elseif threatlevel>=5 then - NassetsMax=2 - else - NassetsMax=1 - end for _,_mp in pairs(MissionPerformances) do local mp=_mp --#CHIEF.MissionPerformance @@ -2112,6 +2252,9 @@ function CHIEF:CheckTargetQueue() --env.info(string.format("FF chief %s nolimit=%s", mp.MissionType, tostring(NoLimit))) if notlimited then + + -- Get min/max number of assets. + local NassetsMin, NassetsMax=self:_GetAssetsForTarget(target, mp.MissionType) -- Debug info. self:T2(self.lid..string.format("Recruiting assets for mission type %s [performance=%d] of target %s", mp.MissionType, mp.Performance, target:GetName())) diff --git a/Moose Development/Moose/Ops/Cohort.lua b/Moose Development/Moose/Ops/Cohort.lua index 8a130a982..4c059b639 100644 --- a/Moose Development/Moose/Ops/Cohort.lua +++ b/Moose Development/Moose/Ops/Cohort.lua @@ -1084,7 +1084,7 @@ function COHORT:RecruitAssets(MissionType, Npayloads) table.insert(assets, asset) end - elseif self.legion:IsAssetOnMission(asset, AUFTRAG.Type.ALERT5) and AUFTRAG.CheckMissionCapability(MissionType, asset.payload.capabilities) then + elseif self.legion:IsAssetOnMission(asset, AUFTRAG.Type.ALERT5) and AUFTRAG.CheckMissionCapability(MissionType, asset.payload.capabilities) and MissionType~=AUFTRAG.Type.ALERT5 then -- Check if the payload of this asset is compatible with the mission. self:T(self.lid..string.format("Adding asset on ALERT 5 mission for %s mission", MissionType)) diff --git a/Moose Development/Moose/Ops/Fleet.lua b/Moose Development/Moose/Ops/Fleet.lua index 515e5cf14..24bffb79d 100644 --- a/Moose Development/Moose/Ops/Fleet.lua +++ b/Moose Development/Moose/Ops/Fleet.lua @@ -108,6 +108,15 @@ function FLEET:New(WarehouseName, FleetName) -- Defaults self:SetRetreatZones() + + -- Turn ship into NAVYGROUP. + if self:IsShip() then + local wh=self.warehouse --Wrapper.Unit#UNIT + local group=wh:GetGroup() + self.warehouseOpsGroup=NAVYGROUP:New(group) --Ops.NavyGroup#NAVYGROUP + self.warehouseOpsElement=self.warehouseOpsGroup:GetElementByName(wh:GetName()) + end + -- Add FSM transitions. -- From State --> Event --> To State diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index 1d5b27eb3..a1182ed7d 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -4322,12 +4322,14 @@ function FLIGHTGROUP:_UpdateMenu(delay) if player and player.status~=OPSGROUP.ElementStatus.DEAD then -- Debug text. - local text=string.format("Updating MENU: State=%s, ATC=%s [%s]", self:GetState(), - self.flightcontrol and self.flightcontrol.airbasename or "None", self.flightcontrol and self.flightcontrol:GetFlightStatus(self) or "Unknown") - - -- Message to group. - MESSAGE:New(text, 5):ToGroup(self.group) - self:I(self.lid..text) + if self.verbose>=2 then + local text=string.format("Updating MENU: State=%s, ATC=%s [%s]", self:GetState(), + self.flightcontrol and self.flightcontrol.airbasename or "None", self.flightcontrol and self.flightcontrol:GetFlightStatus(self) or "Unknown") + + -- Message to group. + MESSAGE:New(text, 5):ToGroup(self.group) + self:I(self.lid..text) + end -- Get current position of player. local position=self:GetCoordinate(nil, player.name) diff --git a/Moose Development/Moose/Ops/Legion.lua b/Moose Development/Moose/Ops/Legion.lua index 0ce4ed242..e5c0056e7 100644 --- a/Moose Development/Moose/Ops/Legion.lua +++ b/Moose Development/Moose/Ops/Legion.lua @@ -1191,6 +1191,12 @@ function LEGION:onafterOpsOnMission(From, Event, To, OpsGroup, Mission) self:NavyOnMission(OpsGroup, Mission) end + -- Load group as cargo because it cannot swim! We pause the mission. + if self:IsBrigade() and self:IsShip() then + OpsGroup:PauseMission() + self.warehouseOpsGroup:Load(OpsGroup, self.warehouseOpsElement) + end + -- Trigger event for chief. if self.chief then self.chief:OpsOnMission(OpsGroup, Mission) @@ -2689,15 +2695,15 @@ function LEGION:AssignAssetsForTransport(Legions, CargoAssets, NcarriersMin, Nca -- Set pickup zone to spawn zone or airbase if the legion has one that is operational. local pickupzone=legion.spawnzone - if legion.airbase and legion:IsRunwayOperational() then - --pickupzone=ZONE_AIRBASE:New(legion.airbasename, 4000) - end -- Add TZC from legion spawn zone to deploy zone. local tpz=Transport:AddTransportZoneCombo(nil, pickupzone, Transport:GetDeployZone()) - tpz.PickupAirbase=legion:IsRunwayOperational() and legion.airbase or nil - Transport:SetEmbarkZone(legion.spawnzone, tpz) + -- Set pickup airbase if the legion has an airbase. Could also be the ship itself. + tpz.PickupAirbase=legion:IsRunwayOperational() and legion.airbase or nil + + -- Set embark zone to spawn zone. + Transport:SetEmbarkZone(legion.spawnzone, tpz) -- Add cargo assets to transport. for _,_asset in pairs(CargoAssets) do diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index f3f4932bb..56f8eab4b 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -462,7 +462,7 @@ OPSGROUP.CarrierStatus={ -- @type OPSGROUP.CargoStatus -- @field #string AWAITING Group is awaiting carrier. -- @field #string NOTCARGO This group is no cargo yet. --- @field #string ASSIGNED Cargo is assigned to a carrier. +-- @field #string ASSIGNED Cargo is assigned to a carrier. (Not used!) -- @field #string BOARDING Cargo is boarding a carrier. -- @field #string LOADED Cargo is loaded into a carrier. OPSGROUP.CargoStatus={ @@ -5397,6 +5397,12 @@ function OPSGROUP:RouteToMission(mission, delay) self:T(self.lid..string.format("Route To Mission: I am DEAD or STOPPED! Ooops...")) return end + + -- Check if this group is cargo. + if self:IsCargo() then + self:T(self.lid..string.format("Route To Mission: I am CARGO! You cannot route me...")) + return + end -- OPSTRANSPORT: Just add the ops transport to the queue. if mission.type==AUFTRAG.Type.OPSTRANSPORT then @@ -8661,12 +8667,12 @@ function OPSGROUP:onafterLoading(From, Event, To) -- Check if cargo is not already cargo. local isNotCargo=cargo.opsgroup:IsNotCargo(true) - -- Check if cargo is holding. - local isHolding=cargo.opsgroup:IsHolding() + -- Check if cargo is holding or loaded + local isHolding=cargo.opsgroup:IsHolding() or cargo.opsgroup:IsLoaded() -- Check if cargo is in embark/pickup zone. -- Added InUtero here, if embark zone is moving (ship) and cargo has been spawned late activated and its position is not updated. Not sure if that breaks something else! - local inZone=cargo.opsgroup:IsInZone(self.cargoTZC.EmbarkZone) --or cargo.opsgroup:IsInUtero() + local inZone=cargo.opsgroup:IsInZone(self.cargoTZC.EmbarkZone) or cargo.opsgroup:IsInUtero() -- Check if cargo is currently on a mission. local isOnMission=cargo.opsgroup:IsOnMission() @@ -8675,9 +8681,13 @@ function OPSGROUP:onafterLoading(From, Event, To) if isOnMission then local mission=cargo.opsgroup:GetMissionCurrent() if mission and mission.opstransport and mission.opstransport.uid==self.cargoTransport.uid then - isOnMission=not cargo.opsgroup:IsHolding() + isOnMission=not isHolding end - end + end + + -- Debug message. + self:T(self.lid..string.format("Loading: canCargo=%s, isCarrier=%s, isNotCargo=%s, isHolding=%s, isOnMission=%s", + tostring(canCargo), tostring(isCarrier), tostring(isNotCargo), tostring(isHolding), tostring(isOnMission))) -- TODO: Need a better :IsBusy() function or :IsReadyForMission() :IsReadyForBoarding() :IsReadyForTransport() if canCargo and inZone and isNotCargo and isHolding and (not (cargo.delivered or cargo.opsgroup:IsDead() or isCarrier or isOnMission)) then @@ -8701,10 +8711,7 @@ function OPSGROUP:onafterLoading(From, Event, To) local carrier=self:FindCarrierForCargo(cargo.opsgroup) if carrier then - - -- Set cargo status. - cargo.opsgroup:_NewCargoStatus(OPSGROUP.CargoStatus.ASSIGNED) - + -- Order cargo group to board the carrier. cargo.opsgroup:Board(self, carrier) @@ -9312,7 +9319,10 @@ function OPSGROUP:onafterUnloaded(From, Event, To, OpsGroupCargo) OpsGroupCargo:Returned() end - if self:_CountPausedMissions()>0 then + -- Check if there is a paused mission. + local paused=OpsGroupCargo:_CountPausedMissions()>0 + + if paused then OpsGroupCargo:UnpauseMission() end @@ -9586,9 +9596,6 @@ end -- @param #OPSGROUP.Element Carrier The OPSGROUP element function OPSGROUP:onafterBoard(From, Event, To, CarrierGroup, Carrier) - -- Set cargo status. - self:_NewCargoStatus(OPSGROUP.CargoStatus.BOARDING) - -- Army or Navy group. local CarrierIsArmyOrNavy=CarrierGroup:IsArmygroup() or CarrierGroup:IsNavygroup() local CargoIsArmyOrNavy=self:IsArmygroup() or self:IsNavygroup() @@ -9605,7 +9612,21 @@ function OPSGROUP:onafterBoard(From, Event, To, CarrierGroup, Carrier) board=false end - if board then + if self:IsLoaded() then + + -- Debug info. + self:T(self.lid..string.format("Group is loaded currently ==> Moving directly to new carrier - No Unload(), Disembart() events triggered!")) + + -- Remove my carrier. + self:_RemoveMyCarrier() + + -- Trigger Load event. + CarrierGroup:Load(self) + + elseif board then + + -- Set cargo status. + self:_NewCargoStatus(OPSGROUP.CargoStatus.BOARDING) -- Debug info. self:T(self.lid..string.format("Boarding group=%s [%s], carrier=%s", CarrierGroup:GetName(), CarrierGroup:GetState(), tostring(Carrier.name))) diff --git a/Moose Development/Moose/Ops/OpsTransport.lua b/Moose Development/Moose/Ops/OpsTransport.lua index 03f31488c..633c3c81f 100644 --- a/Moose Development/Moose/Ops/OpsTransport.lua +++ b/Moose Development/Moose/Ops/OpsTransport.lua @@ -2171,7 +2171,7 @@ function OPSTRANSPORT:_GetTransportZoneCombo(Carrier) return nil end ---- Get an OPSGROUP from a given OPSGROUP or GROUP object. If the object is a GROUUP, an OPSGROUP is created automatically. +--- Get an OPSGROUP from a given OPSGROUP or GROUP object. If the object is a GROUP, an OPSGROUP is created automatically. -- @param #OPSTRANSPORT self -- @param Core.Base#BASE Object The object, which can be a GROUP or OPSGROUP. -- @return Ops.OpsGroup#OPSGROUP Ops Group. From 888734b7d169a2b11d123413e55f1bdcc4a98f5d Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 18 Jul 2022 22:59:49 +0200 Subject: [PATCH 19/20] OPS - Fixed bug in parking spot distance for ships. - Added enroute anti-ship task for anti-ship missions. --- Moose Development/Moose/Ops/Auftrag.lua | 6 +- Moose Development/Moose/Ops/FlightGroup.lua | 61 ++++++++++++++++--- Moose Development/Moose/Ops/OpsGroup.lua | 3 +- Moose Development/Moose/Wrapper/Airbase.lua | 1 + .../Moose/Wrapper/Controllable.lua | 21 +++++++ 5 files changed, 81 insertions(+), 11 deletions(-) diff --git a/Moose Development/Moose/Ops/Auftrag.lua b/Moose Development/Moose/Ops/Auftrag.lua index c89e9c3a7..69b9a7fd6 100644 --- a/Moose Development/Moose/Ops/Auftrag.lua +++ b/Moose Development/Moose/Ops/Auftrag.lua @@ -5282,6 +5282,10 @@ function AUFTRAG:GetDCSMissionTask() -- ANTISHIP Mission -- ---------------------- + -- Add enroute anti-ship task. + local DCStask=CONTROLLABLE.EnRouteTaskAntiShip(nil) + table.insert(self.enrouteTasks, DCStask) + self:_GetDCSAttackTask(self.engageTarget, DCStasks) elseif self.type==AUFTRAG.Type.AWACS then @@ -5620,8 +5624,6 @@ function AUFTRAG:GetDCSMissionTask() end - --table.insert(DCStasks, DCStask) - elseif self.type==AUFTRAG.Type.BARRAGE then --------------------- diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index a1182ed7d..6bc993d13 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -875,16 +875,14 @@ function FLIGHTGROUP:Status() if element.parking then -- Get distance to assigned parking spot. - local dist=element.unit:GetCoord():Get2DDistance(element.parking.Coordinate) + local dist=self:_GetDistToParking(element.parking, element.unit:GetCoord()) - --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. - -- At least for fighters, the initial distance seems to be around 1.8 meters. + -- Debug info. + self:T(self.lid..string.format("Distance to parking spot %d = %.1f meters", element.parking.TerminalID, dist)) + + -- If distance >10 meters, we consider the unit as taxiing. At least for fighters, the initial distance seems to be around 1.8 meters. if dist>12 and element.engineOn then - --if element.status==OPSGROUP.ElementStatus.ENGINEON then - self:ElementTaxiing(element) - --end + self:ElementTaxiing(element) end else @@ -4604,6 +4602,53 @@ function FLIGHTGROUP:_GetPlayerData() return nil end +--- Get distance to parking spot. Takes extra care of ships. +-- @param #FLIGHTGROUP self +-- @param Wrapper.Airbase#AIRBASE.ParkingSpot Spot Parking Spot. +-- @param Core.Point#COORDINATE Coordinate Reference coordinate. +-- @return #number Distance to parking spot in meters. +function FLIGHTGROUP:_GetDistToParking(Spot, Coordinate) + + local dist=99999 + + if Spot then + + -- Get the airbase this spot belongs to. + local airbase=AIRBASE:FindByName(Spot.AirbaseName) + + + if airbase:IsShip() then --or airbase:IsHelipad() then + + -- Vec2 of airbase. + local a=airbase:GetVec2() + + -- Vec2 of parking spot. + local b=Spot.Coordinate:GetVec2() + + -- Vec2 of ref coordinate. + local c=Coordinate:GetVec2() + + -- Vector from ref coord to airbase. This still needs to be rotated. + local t=UTILS.Vec2Substract(c,a) + + -- Get the heading of the unit. + local unit=UNIT:FindByName(Spot.AirbaseName) + local hdg=unit:GetHeading() + + -- Rotate the vector so that it corresponds to facing "North". + t=UTILS.Vec2Rotate2D(t, -hdg) + + -- Distance from spot to ref coordinate. + dist=UTILS.VecDist2D(b,t) + else + -- Normal case. + dist=Coordinate:Get2DDistance(Spot.Coordinate) + end + + end + + return dist +end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index 56f8eab4b..840911bfd 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -3595,7 +3595,7 @@ function OPSGROUP:PushTask(DCSTask) text=text..string.format("\n[%d] %s", i, tostring(task.id)) end end - self:T(self.lid..text) + self:T(self.lid..text) end return self @@ -3756,6 +3756,7 @@ function OPSGROUP:AddTaskEnroute(task) end if not gotit then + self:T(self.lid..string.format("Adding enroute task")) table.insert(self.taskenroute, task) end diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 53113da33..c21fb28f1 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -1105,6 +1105,7 @@ function AIRBASE:_InitParkingSpots() park.TerminalType=spot.Term_Type park.TOAC=spot.TO_AC park.ClientSpot, park.ClientName=isClient(park.Coordinate) + park.AirbaseName=self.AirbaseName self.NparkingTotal=self.NparkingTotal+1 diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index b27f74903..9cf3fc341 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -1557,6 +1557,27 @@ function CONTROLLABLE:EnRouteTaskEngageTargetsInZone( Vec2, Radius, TargetTypes, return DCSTask end +--- (AIR) Enroute anti-ship task. +-- @param #CONTROLLABLE self +-- @param DCS#AttributeNameArray TargetTypes Array of target categories allowed to engage. Default `{"Ships"}`. +-- @param #number Priority (Optional) All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. Default 0. +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:EnRouteTaskAntiShip(TargetTypes, Priority) + + local DCSTask = { + id = 'EngageTargets', + key = "AntiShip", + --auto = false, + --enabled = true, + params = { + targetTypes = TargetTypes or {"Ships"}, + priority = Priority or 0 + } + } + + return DCSTask +end + --- (AIR) Engaging a controllable. The task does not assign the target controllable to the unit/controllable to attack now; it just allows the unit/controllable to engage the target controllable as well as other assigned targets. -- @param #CONTROLLABLE self From 4bb9073ce15782b4a379031cd4d9cbfa171faad0 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 20 Jul 2022 22:20:26 +0200 Subject: [PATCH 20/20] OPS - Increased selection score for GCICAP/INTERCEPT --- Moose Development/Moose/Ops/Chief.lua | 29 +++++++++++++++++++++++++- Moose Development/Moose/Ops/Legion.lua | 4 ++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Ops/Chief.lua b/Moose Development/Moose/Ops/Chief.lua index e602a1831..53f54a891 100644 --- a/Moose Development/Moose/Ops/Chief.lua +++ b/Moose Development/Moose/Ops/Chief.lua @@ -120,6 +120,33 @@ -- -- Fleets can be added via the @{#CHIEF.AddFleet}() function. -- +-- ## Response on Target +-- +-- When the chief detects a valid target, he will launch a certain number of selected assets. Only whole groups from SQUADRONs, PLATOONs or FLOTILLAs can be selected. +-- In other words, it is not possible to specify the abount of individual *units*. +-- +-- By default, one group is selected for any detected target. This can, however, be customized with the @{CHIEF.SetResponseOnTarget}() function. The number of min and max +-- asset groups can be specified depending on threatlevel, category, mission type, number of units, defcon and strategy. +-- +-- For example: +-- +-- -- One group for aircraft targets of threat level 0 or higher. +-- myChief:SetResponseOnTarget(1, 1, 0, TARGET.Category.AIRCRAFT) +-- -- At least one and up to two groups for aircraft targets of threat level 8 or higher. This will overrule the previous response! +-- myChief:SetResponseOnTarget(1, 2, 8, TARGET.Category.AIRCRAFT) +-- +-- -- At least one and up to three groups for ground targets of threat level 0 or higher if current strategy is aggressive. +-- myChief:SetResponseOnTarget(1, 1, 0, TARGET.Category.GROUND, nil ,nil, nil, CHIEF.Strategy.DEFENSIVE) +-- +-- -- One group for BAI missions if current defcon is green. +-- myChief:SetResponseOnTarget(1, 1, 0, nil, AUFTRAG.Type.BAI, nil, CHIEF.DEFCON.GREEN) +-- +-- -- At least one and up to four groups for BAI missions if current defcon is red. +-- myChief:SetResponseOnTarget(1, 2, 0, nil, AUFTRAG.Type.BAI, nil, CHIEF.DEFCON.YELLOW) +-- +-- -- At least one and up to four groups for BAI missions if current defcon is red. +-- myChief:SetResponseOnTarget(1, 3, 0, nil, AUFTRAG.Type.BAI, nil, CHIEF.DEFCON.RED) +-- -- -- # Strategic (Capture) Zones -- @@ -287,7 +314,7 @@ CHIEF.Strategy = { --- CHIEF class version. -- @field #string version -CHIEF.version="0.3.1" +CHIEF.version="0.4.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list diff --git a/Moose Development/Moose/Ops/Legion.lua b/Moose Development/Moose/Ops/Legion.lua index e5c0056e7..01e35dc3b 100644 --- a/Moose Development/Moose/Ops/Legion.lua +++ b/Moose Development/Moose/Ops/Legion.lua @@ -2806,8 +2806,8 @@ function LEGION.CalculateAssetMissionScore(asset, MissionType, TargetVec2, Inclu -- Prefer assets that are on ALERT5 for this mission type. score=score+25 elseif currmission.type==AUFTRAG.Type.GCICAP and MissionType==AUFTRAG.Type.INTERCEPT then - -- Prefer assets that are on GCICAP to perform INTERCEPTS - score=score+25 + -- Prefer assets that are on GCICAP to perform INTERCEPTS. We set this even higher than alert5 because they are already in the air. + score=score+35 elseif (currmission.type==AUFTRAG.Type.ONGUARD or currmission.type==AUFTRAG.Type.PATROLZONE) and (MissionType==AUFTRAG.Type.ARTY or MissionType==AUFTRAG.Type.GROUNDATTACK) then score=score+25 elseif currmission.type==AUFTRAG.Type.NOTHING then