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