diff --git a/Moose Development/Moose/Core/Condition.lua b/Moose Development/Moose/Core/Condition.lua new file mode 100644 index 000000000..7c25f1948 --- /dev/null +++ b/Moose Development/Moose/Core/Condition.lua @@ -0,0 +1,295 @@ +--- **Core** - Define any or all conditions to be evaluated. +-- +-- **Main Features:** +-- +-- * Add arbitrary numbers of conditon functions +-- * Evaluate *any* or *all* conditions +-- +-- === +-- +-- ## 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_Conditon.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.1.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- 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) + + -- 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 + 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: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 + +--- 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/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index 07af8817b..5997fb856 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -1027,7 +1027,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]) @@ -1533,6 +1533,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 655e4eca2..ee276c71b 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -6331,7 +6331,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. @@ -6413,6 +6413,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/Range.lua b/Moose Development/Moose/Functional/Range.lua index e7aa20889..e97f15874 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/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 42e8d61cb..7b51d8da1 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", @@ -1936,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 @@ -2581,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%). @@ -5364,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. @@ -6311,10 +6322,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. @@ -7874,14 +7886,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 @@ -8351,9 +8365,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") @@ -8390,6 +8405,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/Modules.lua b/Moose Development/Moose/Modules.lua index 38e80ead8..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,8 @@ __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' ) __Moose.Include( 'Scripts/Moose/AI/AI_Air.lua' ) diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index d95425b37..6b9fa6bd1 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 @@ -1137,15 +1135,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 @@ -1391,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 +2020,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 +2144,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 +2162,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 +2180,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 +2239,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 +2366,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/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" ) diff --git a/Moose Development/Moose/Ops/ArmyGroup.lua b/Moose Development/Moose/Ops/ArmyGroup.lua index 35ba2606f..e8a659885 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 @@ -63,7 +68,7 @@ ARMYGROUP = { --- Army Group version. -- @field #string version -ARMYGROUP.version="0.7.3" +ARMYGROUP.version="0.7.9" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -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 @@ -129,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 --- @@ -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 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -880,6 +920,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 +989,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 +1009,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))) @@ -1219,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 @@ -1241,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 @@ -1283,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)) @@ -1323,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. @@ -1580,7 +1651,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() @@ -1743,6 +1814,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 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1987,6 +2073,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 a1e46b10b..69b9a7fd6 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. @@ -111,6 +113,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 +173,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 @@ -411,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", @@ -452,6 +460,7 @@ AUFTRAG.Type={ AIRDEFENSE="Air Defence", EWR="Early Warning Radar", RECOVERYTANKER="Recovery Tanker", + REARMING="Rearming", NOTHING="Nothing", } @@ -473,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", @@ -492,6 +502,7 @@ AUFTRAG.SpecialTask={ AIRDEFENSE="Air Defense", EWR="Early Warning Radar", RECOVERYTANKER="Recovery Tanker", + REARMING="Rearming", NOTHING="Nothing", } @@ -687,6 +698,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) @@ -1763,7 +1777,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 @@ -1999,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. @@ -2196,7 +2234,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 @@ -2414,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 @@ -2522,6 +2566,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 @@ -2970,6 +3029,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. @@ -3288,9 +3377,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. @@ -4091,7 +4204,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 @@ -4103,7 +4216,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 @@ -4111,7 +4224,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 @@ -4119,14 +4232,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 @@ -4249,7 +4362,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 @@ -4267,6 +4381,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 @@ -4361,7 +4478,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 @@ -4625,6 +4742,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() @@ -4897,12 +5017,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 @@ -4967,6 +5093,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 @@ -5140,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 @@ -5478,8 +5624,6 @@ function AUFTRAG:GetDCSMissionTask() end - --table.insert(DCStasks, DCStask) - elseif self.type==AUFTRAG.Type.BARRAGE then --------------------- @@ -5600,6 +5744,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/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 ce5d5a65c..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 -- @@ -256,6 +283,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. @@ -276,7 +314,7 @@ CHIEF.Strategy = { --- CHIEF class version. -- @field #string version -CHIEF.version="0.3.1" +CHIEF.version="0.4.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -773,6 +811,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`. @@ -955,6 +1140,7 @@ end function CHIEF:AddTarget(Target) if not self:IsTarget(Target) then + Target.chief=self table.insert(self.targetqueue, Target) end @@ -1536,7 +1722,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) @@ -2083,24 +2269,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 @@ -2111,12 +2279,15 @@ 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())) -- 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 @@ -2127,10 +2298,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. @@ -2650,45 +2819,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={} - 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 - - -- 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 @@ -2698,23 +2828,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 fe90c4e2f..4c059b639 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. @@ -240,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. @@ -257,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. @@ -274,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. @@ -291,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. @@ -785,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. @@ -987,6 +989,30 @@ 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_OPSGROUP 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: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 +1047,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 +1060,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 @@ -1058,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)) @@ -1519,7 +1545,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 5049e05e9..022fc2678 100644 --- a/Moose Development/Moose/Ops/Commander.lua +++ b/Moose Development/Moose/Ops/Commander.lua @@ -24,6 +24,8 @@ -- @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 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`. @@ -125,6 +127,8 @@ COMMANDER = { legions = {}, missionqueue = {}, transportqueue = {}, + targetqueue = {}, + opsqueue = {}, rearmingZones = {}, refuellingZones = {}, capZones = {}, @@ -514,6 +518,69 @@ 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 + +--- 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. +-- @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. @@ -786,9 +853,15 @@ 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 + + -- Check Operations queue. + self:CheckOpsQueue() + + -- Check target queue and add missions. + self:CheckTargetQueue() -- Check mission queue and assign one PLANNED mission. self:CheckMissionQueue() @@ -977,6 +1050,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 @@ -1148,6 +1236,160 @@ 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#OPERATION + + 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 + + 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() + + -- Number of missions. + local Ntargets=#self.targetqueue + + -- Treat special cases. + if Ntargets==0 then + 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 + + -- 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 + + -- Set mission parameters. + mission:SetRequiredAssets(resource.Nmin, resource.Nmax) + mission:SetRequiredAttribute(resource.Attributes) + mission:SetRequiredProperty(resource.Properties) + + -- Set operation (if any). + mission.operation=target.operation + + -- Set resource mission. + 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() @@ -1203,10 +1445,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) @@ -1262,6 +1501,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. @@ -1274,29 +1628,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() @@ -1325,30 +1660,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) @@ -1359,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 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1478,24 +1814,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/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/FlightControl.lua b/Moose Development/Moose/Ops/FlightControl.lua new file mode 100644 index 000000000..39e23ebc5 --- /dev/null +++ b/Moose Development/Moose/Ops/FlightControl.lua @@ -0,0 +1,4578 @@ +--- **OPS** - Air Traffic Control for AI and human players. +-- +-- **Main Features:** +-- +-- * Manage aircraft departure and arrival +-- * Handles AI and human players +-- * Limit number of AI groups taxiing, taking off and landing simultaniously +-- * Immersive voice overs via SRS text-to-speech +-- * Define holding patterns for airdromes +-- +-- === +-- +-- ## 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 + + +--- FLIGHTCONTROL class. +-- @type FLIGHTCONTROL +-- @field #string ClassName Name of the class. +-- @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. +-- @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 flights All flights table. +-- @field #table clients Table with all clients spawning at this airbase. +-- @field Ops.ATIS#ATIS atis ATIS object. +-- @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. +-- @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 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. +-- @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. +-- @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. +-- **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. +-- +-- 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 +-- +-- * 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. +-- * 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. +-- * 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 +-- +-- 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. +-- +-- 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. 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 = { + ClassName = "FLIGHTCONTROL", + verbose = 0, + lid = nil, + theatre = nil, + airbasename = nil, + airbase = nil, + airbasetype = nil, + zoneAirbase = nil, + parking = {}, + runways = {}, + flights = {}, + clients = {}, + atis = nil, + Nlanding = nil, + dTlanding = nil, + Nparkingspots = nil, + holdingpatterns = {}, + hpcounter = 0, +} + +--- Holding point. Contains holding stacks. +-- @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 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. +-- @field #number markArrival Marker ID of the arrival zone. +-- @field #number markArrow Marker ID of the direction. + +--- 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 pattern. +-- @field Core.Point#COORDINATE pos1 Second position of racetrack holding pattern. +-- @field #number heading Heading. + + +--- 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 UNKNOWN Flight is unknown. +-- @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={ + UNKNOWN="Unknown", + PARKING="Parking", + READYTX="Ready To Taxi", + TAXIOUT="Taxi To Runway", + READYTO="Ready For Takeoff", + TAKEOFF="Takeoff", + INBOUND="Inbound", + HOLDING="Holding", + LANDING="Landing", + TAXIINB="Taxi To Parking", + ARRIVED="Arrived", +} + +--- FlightControl class version. +-- @field #string version +FLIGHTCONTROL.version="0.7.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list + +-- TODO: Switch to enable/disable AI messages. +-- TODO: Talk me down 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. +-- DONE: Basic ATC voice overs. +-- DONE: Add SRS TTS. +-- DONE: Add parking guard. +-- 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)) + + -- Add backup holding pattern. + self:_AddHoldingPatternBackup() + + -- Set alias. + self.alias=self.airbasename.." Tower" + + -- Defaults: + self:SetLimitLanding(2, 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:SetSRSTower() + + -- SRS for Pilot. + self.msrsPilot=MSRS:New(PathToSRS, Frequency, Modulation) + self:SetSRSPilot() + + -- Wait at least 10 seconds after last radio message before calling the next status update. + self.dTmessage=10 + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- 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("*", "RunwayDestroyed", "*") -- Runway of the airbase was destroyed. + self:AddTransition("*", "RunwayRepaired", "*") -- Runway of the airbase was repaired. + + 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 "StatusUpdate". + -- @function [parent=#FLIGHTCONTROL] StatusUpdate + -- @param #FLIGHTCONTROL self + + --- Triggers the FSM event "StatusUpdate" after a delay. + -- @function [parent=#FLIGHTCONTROL] __StatusUpdate + -- @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 + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- 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. +-- @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 + + 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 + +--- 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. +-- +-- 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. +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:SetLimitLanding(Nlanding, Ntakeoff) + + self.NlandingTot=Nlanding or 2 + + self.NlandingTakeoff=Ntakeoff or 0 + + 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 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 + +--- Add a holding pattern. +-- 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. 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, Prio) + + -- 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.HoldingPattern + 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 + 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 + + -- Add to table. + table.insert(self.holdingpatterns, hp) + + -- Sort holding patterns wrt to prio. + local function _sort(a,b) + return a.prio%d sec ago. Status update allowed", dT, self.dTmessage)) + end + end + + return true +end + +--- Update status. +-- @param #FLIGHTCONTROL self +function FLIGHTCONTROL:onafterStatusUpdate() + + -- Debug message. + self:T2(self.lid.."Status update") + + -- 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() + + -- Check parking spots. + --self:_CheckParking() + + -- Check waiting and landing queue. + self:_CheckQueues() + + -- Get runway. + local rwyLanding=self:GetActiveRunwayText() + local rwyTakeoff=self:GetActiveRunwayText(true) + + -- Count flights. + 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. + if self.verbose>=1 then + 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 + + if Nflights==Nqueues then + --Check! + else + self:E(string.format("WARNING: Number of total flights %d!=%d number of flights in all queues!", Nflights, Nqueues)) + end + + if self.verbose>=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:__StatusUpdate(-30) +end + +--- Stop FLIGHTCONTROL FSM. +-- @param #FLIGHTCONTROL self +function FLIGHTCONTROL:onafterStop() + + -- 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) + self:UnHandleEvent(EVENTS.Kill) +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: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 + + -- 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 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 +function FLIGHTCONTROL:OnEventLand(EventData) + self:F3({EvendData=EventData}) + + self:T2(self.lid..string.format("LAND: unit = %s", tostring(EventData.IniUnitName))) + self:T3(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:T3(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: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. +-- @param #FLIGHTCONTROL self +-- @param Core.Event#EVENTDATA EventData +function FLIGHTCONTROL:OnEventEngineShutdown(EventData) + self:F3({EvendData=EventData}) + + 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 + +--- Event handler for event kill. +-- @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 + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- 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 +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check takeoff and landing queues. +-- @param #FLIGHTCONTROL self +function FLIGHTCONTROL:_CheckQueues() + + -- Print queue. + if self.verbose>=2 then + self:_PrintQueue(self.flights, "All flights") + end + + -- Get next flight in line: either holding or parking. + local flight, isholding, parking=self:_GetNextFlight() + + + -- Check if somebody wants something. + if flight then + + if isholding then + + -------------------- + -- Holding flight -- + -------------------- + + -- No other flight is taking off and number of landing flights is below threshold. + if self:_CheckFlightLanding(flight) then + + -- Get interval to last flight that got landing clearance. + local dTlanding=99999 + if self.Tlanding then + dTlanding=timer.getAbsTime()-self.Tlanding + end + + if parking and dTlanding>=self.dTlanding then + + -- Get callsign. + local callsign=self:_GetCallsignName(flight) + + -- Runway. + local runway=self:GetActiveRunwayText() + + -- Message. + local text=string.format("%s, %s, you are cleared to land, runway %s", callsign, self.alias, runway) + + -- Transmit message. + self:TransmissionTower(text, flight) + + -- 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 + + -- We set this flight to landing. With this he is allowed to leave the pattern. + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.LANDING) + + end + + -- Set time last flight got landing clearance. + self.Tlanding=timer.getAbsTime() + + end + else + self:T3(self.lid..string.format("FYI: Landing clearance for flight %s denied", flight.groupname)) + end + + else + + -------------------- + -- Takeoff flight -- + -------------------- + + -- No other flight is taking off or landing. + if self:_CheckFlightTakeoff(flight) then + + -- Get callsign. + local callsign=self:_GetCallsignName(flight) + + -- Runway. + local runway=self:GetActiveRunwayText(true) + + -- Message. + 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", 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 + --- + + -- Message. + local text="Wilco, " + + -- Start uncontrolled aircraft. + if flight:IsUncontrolled() then + + -- Message. + text=text..string.format("starting engines, ") + + -- Start uncontrolled aircraft. + flight:StartUncontrolled() + end + + -- Message. + text=text..string.format("runway %s, %s", runway, callsign) + + -- Transmit message. + self:TransmissionPilot(text, flight, 10) + + -- 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, 15) + else + self:RemoveParkingGuard(spot, 10) + end + end + end + + end + + end + + else + -- Debug message. + self:T3(self.lid..string.format("FYI: Take off for flight %s denied", flight.groupname)) + end + end + else + -- Debug message. + self:T2(self.lid..string.format("FYI: No flight in queue for takeoff or landing")) + 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:_CheckFlightTakeoff(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 nlanding>self.NtaxiLanding then + 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 + + local ninbound=0 + if self.NtaxiInbound then + ninbound=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAXIINB, nil, true) + end + + 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: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 + --- + -- 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>self.NtaxiLanding then + -- Traffic landing. No permission to + 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:T(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<=self.NlandingTakeoff and nlanding land + return flightholding, true, parking + else + -- Not enough parking ==> take off + return flightparking, false, nil + end + end + + local text=string.format("Flight holding for %d sec, flight parking for %d sec", flightholding:GetHoldingTime(), flightparking:GetParkingTime()) + self:T(self.lid..text) + + -- 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.Tholding and flightparking.Tparking and flightholding.TholdingTholdingMin then + return fg + end + end + + -- Sort flights by low fuel. + local function _sortByFuel(a, b) + local flightA=a --Ops.FlightGroup#FLIGHTGROUP + local flightB=b --Ops.FlightGroup#FLIGHTGROUP + local fuelA=flightA.group:GetFuelMin() + local fuelB=flightB.group:GetFuelMin() + return fuelATholdingMin then + return fg + end + + return nil +end + + +--- Get next flight waiting for taxi and takeoff clearance. +-- @param #FLIGHTCONTROL self +-- @return Ops.FlightGroup#FLIGHTGROUP Marshal flight next in line and ready to enter the pattern. Or nil if no flight is ready. +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, OnlyAI) + + -- First check human players. + if #QreadyTO>0 then + -- First come, first serve. + return QreadyTO[1] + end + + -- Get flights ready to taxi. + local QreadyTX=self:GetFlights(FLIGHTCONTROL.FlightStatus.READYTX, OPSGROUP.GroupStatus.PARKING, OnlyAI) + + -- First check human players. + if #QreadyTX>0 then + -- First come, first serve. + 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. + if Nparking==0 then + return nil + end + + -- Sort flights parking time. + local function _sortByTparking(a, b) + local flightA=a --Ops.FlightGroup#FLIGHTGROUP + local flightB=b --Ops.FlightGroup#FLIGHTGROUP + return flightA.Tparking=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 [%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 + + -- Get the first AI flight. + for i,_flight in pairs(Qparking) do + local flight=_flight --Ops.FlightGroup#FLIGHTGROUP + if flight.isAI and flight.isReadyTO then + return flight + end + end + + return nil +end + +--- Print queue. +-- @param #FLIGHTCONTROL self +-- @param #table queue Queue to print. +-- @param #string name Queue name. +-- @return #string Queue text. +function FLIGHTCONTROL:_PrintQueue(queue, name) + + local text=string.format("%s Queue N=%d:", name, #queue) + if #queue==0 then + -- Queue is empty. + text=text.." empty." + else + + local time=timer.getAbsTime() + + -- Loop over all flights in queue. + for i,_flight in ipairs(queue) do + local flight=_flight --Ops.FlightGroup#FLIGHTGROUP + + -- Gather info. + local fuel=flight.group:GetFuelMin()*100 + local ai=tostring(flight.isAI) + local actype=tostring(flight.actype) + + -- Holding and parking time. + local holding=flight.Tholding and UTILS.SecondsToClock(time-flight.Tholding, true) or "X" + local parking=flight.Tparking and UTILS.SecondsToClock(time-flight.Tparking, true) or "X" + + local holding=flight:GetHoldingTime() + if holding>=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 + + -- Number of elements. + local nunits=flight:CountElements() + + -- Status. + local state=flight:GetState() + local status=self:GetFlightStatus(flight) + + -- Main info. + 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 + 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 + +--- 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())) + + -- 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 + +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) + + -- 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. +-- @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 +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get the active runway based on current wind direction. +-- @param #FLIGHTCONTROL self +-- @return Wrapper.Airbase#AIRBASE.Runway Active runway. +function FLIGHTCONTROL:GetActiveRunway() + local rwy=self.airbase: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(Takeoff) + + local runway + if Takeoff then + runway=self:GetActiveRunwayTakeoff() + else + runway=self:GetActiveRunwayLanding() + end + + local name=self.airbase:GetRunwayName(runway, true) + + return name or "XX" +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.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()) + + -- 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:E(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. +-- @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) + + -- Update spot status. + self:_UpdateSpotStatus(spot, AIRBASE.SpotStatus.FREE, spot.OccupiedBy or spot.ReservedBy) + + -- Not occupied or reserved. + spot.OccupiedBy=nil + spot.ReservedBy=nil + + -- Remove parking guard. + self:RemoveParkingGuard(spot) + + -- Update marker. + 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) + + -- Get spot. + local spot=self:GetParkingSpotByID(spot.TerminalID) + + -- Update spot status. + self:_UpdateSpotStatus(spot, AIRBASE.SpotStatus.RESERVED, unitname) + + -- Reserved. + spot.ReservedBy=unitname or "unknown" + + -- Update marker. + 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) + + -- Get spot. + local spot=self:GetParkingSpotByID(spot.TerminalID) + + -- Update spot status. + self:_UpdateSpotStatus(spot, AIRBASE.SpotStatus.OCCUPIED, unitname) + + -- Occupied. + spot.OccupiedBy=unitname or "unknown" + + -- Update marker. + self:UpdateParkingMarker(spot) + +end + +--- Update parking markers. +-- @param #FLIGHTCONTROL self +-- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot The parking spot data table. +function FLIGHTCONTROL:UpdateParkingMarker(spot) + + if self.markerParking then + + -- Get spot. + local spot=self:GetParkingSpotByID(spot.TerminalID) + + -- 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", 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. +-- @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 +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 Status (Optional) Only consider spots that have this status. +-- @return #FLIGHTCONTROL.ParkingSpot Closest parking spot. +function FLIGHTCONTROL:GetClosestParkingSpot(Coordinate, TerminalType, Status) + + local distmin=math.huge + local spotmin=nil + + for TerminalID, Spot in pairs(self.parking) do + local spot=Spot --Wrapper.Airbase#AIRBASE.ParkingSpot + + --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, 15, true) + + else + self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Player Menu: Inbound +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Player calls inbound. +-- @param #FLIGHTCONTROL self +-- @param #string groupname Name of the flight group. +function FLIGHTCONTROL:_PlayerRequestInbound(groupname) + + -- Get flight group. + local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP + + if flight then + + if flight:IsAirborne() then + + -- Call sign. + local callsign=self:_GetCallsignName(flight) + + -- Get player element. + local player=flight:GetPlayerElement() + + -- Pilot calls inbound for landing. + local text=string.format("%s, %s, inbound for landing", self.alias, callsign) + + -- Radio message. + self:TransmissionPilot(text, flight) + + -- Current player coord. + local flightcoord=flight:GetCoordinate(nil, player.name) + + -- Distance from player to airbase. + local dist=flightcoord:Get2DDistance(self:GetCoordinate()) + + if distself.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 +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Player requests taxi. +-- @param #FLIGHTCONTROL self +-- @param #string groupname Name of the flight group. +function FLIGHTCONTROL:_PlayerRequestTaxi(groupname) + + -- Get flight. + local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP + + if flight then + + -- Get callsign. + local callsign=self:_GetCallsignName(flight) + + -- Pilot request for taxi. + local text=string.format("%s, %s, request taxi to runway.", self.alias, callsign) + self:TransmissionPilot(text, flight) + + if flight:IsParking() then + + -- Tell pilot to wait until cleared. + local text=string.format("%s, %s, hold position until further notice.", callsign, self.alias) + self:TransmissionTower(text, flight, 10) + + -- Set flight status to "Ready to Taxi". + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.READYTX) + + 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) + end + + else + self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) + end + +end + +--- Player aborts taxi. +-- @param #FLIGHTCONTROL self +-- @param #string groupname Name of the flight group. +function FLIGHTCONTROL:_PlayerAbortTaxi(groupname) + + -- Get flight. + local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP + + if flight then + + -- Get callsign. + local callsign=self:_GetCallsignName(flight) + + -- Pilot request for taxi. + local text=string.format("%s, %s, cancel my taxi request.", self.alias, callsign) + self:TransmissionPilot(text, flight) + + if flight:IsParking() then + + -- Tell pilot remain parking. + local text=string.format("%s, %s, roger, remain on your parking position.", callsign, self.alias) + self:TransmissionTower(text, flight, 10) + + -- Set flight status to "Parking". + 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 + + -- Tell pilot to return to parking. + local text=string.format("%s, %s, roger, return to your parking position.", callsign, self.alias) + self:TransmissionTower(text, flight, 10) + + -- Set flight status to "Taxi Inbound". + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.TAXIINB) + + else + self:TextMessageToFlight(string.format("Negative, you must be PARKING or TAXIING to abort TAXI!"), flight) + end + + else + self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Player Menu: Takeoff +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Player requests takeoff. +-- @param #FLIGHTCONTROL self +-- @param #string groupname Name of the flight group. +function FLIGHTCONTROL:_PlayerRequestTakeoff(groupname) + + local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP + + if flight then + + if flight:IsTaxiing() then + + -- Get callsign. + local callsign=self:_GetCallsignName(flight) + + -- 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 + 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 + 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 + 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 + ]] + + -- 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 + text=text..string.format("negative, we got %d flight inbound before it's your turn. Hold position until futher notice.", Nlanding) + else + text=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) + + else + self:TextMessageToFlight(string.format("Negative, you must request TAXI before you can request TAKEOFF!"), flight) + end + + else + self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) + 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 + + -- 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=self:_GetCallsignName(flight) + + -- Pilot request for taxi. + local text=string.format("%s, %s, abort takeoff.", self.alias, callsign) + self:TransmissionPilot(text, flight) + + -- 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) + else + env.info(self.lid.."ERROR") + end + + -- Message from tower. + self:TransmissionTower(text, flight, 10) + + else + self:TextMessageToFlight("Negative, You are NOT in the takeoff queue", flight) + end + + else + self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Player Menu: Parking +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Player reserves a parking spot. +-- @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 + + -- Get callsign. + local callsign=self:_GetCallsignName(flight) + + -- Get player element. + local player=flight:GetPlayerElement() + + -- 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 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.", callsign, spot.TerminalID) + + -- Transmit message. + self:TransmissionTower(text, flight) + + -- 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) + + -- Update menu ==> Cancel Parking. + flight:_UpdateMenu(0.2) + + 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 + +--- 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. +function FLIGHTCONTROL:_PlayerArrived(groupname) + + -- Get flight group. + local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP + + if flight then + + -- Player element. + local player=flight:GetPlayerElement() + + -- Get current coordinate. + local coord=flight:GetCoordinate(nil, player.name) + + -- 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. + local callsign=self:_GetCallsignName(flight) + + -- Distance to parking spot. + local dist=coord:Get2DDistance(spot.Coordinate) + + 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) + -- Message text. + 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) + + else + + -- Message text. + local text=string.format("%s, %s, arrived at parking position", self.alias, callsign) + + -- Transmit message. + self:TransmissionPilot(text, flight) + + 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) + + end + + else + -- TODO: No spot + end + + else + self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) + 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: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()) + + -- If it does not exist yet, create one. + if not flight then + flight=FLIGHTGROUP:New(group:GetName()) + end + + -- Set flightcontrol. + 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) + + -- 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) + + return true + end + end + + -- Debug message. + self:E(self.lid..string.format("WARNING: Could NOT remove flight group %s", Flight.groupname)) +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 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) + + -- 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 --Ops.OpsGroup#OPSGROUP.Element + + 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:T(self.lid..string.format("Removing DEAD flight %s", tostring(flight.groupname))) + self:_RemoveFlight(flight) + end + end + + -- Check speeding. + if self.speedLimitTaxi then + + for _,_flight in pairs(self.flights) do + 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 + + -- 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 + + end + + end + end + end + + end + +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:T(self.lid..string.format("Landing AI flight %s.", flight.groupname)) + + + -- Respawn? + 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("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) + + else + env.info("FF error could not get element to assign parking!") + end + end + + -- Debug message. + self:TextMessageToFlight(string.format("Respawning group %s", flight.groupname), flight) + + --Respawn the group. + flight:Respawn(Template) + + else + + -- Give signal to land. + flight:ClearToLand() + + end + +end + +--- Get holding stack. +-- @param #FLIGHTCONTROL self +-- @param Ops.FlightGroup#FLIGHTGROUP flight Flight group. +-- @return #FLIGHTCONTROL.HoldingStack Holding point. +function FLIGHTCONTROL:_GetHoldingStack(flight) + + -- Debug message. + self:T(self.lid..string.format("Getting holding point for flight %s", flight:GetName())) + + for i,_hp in pairs(self.holdingpatterns) do + local holdingpattern=_hp --#FLIGHTCONTROL.HoldingPattern + + self:T(self.lid..string.format("Checking holding point %s", holdingpattern.name)) + + 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)) + if not stack.flightgroup then + return stack + end + end + + end + + 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 +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- 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) + + -- Spoken text. + local text=self:_GetTextForSpeech(Text) + + -- Tower radio call. + self.msrsTower:PlayText(text, Delay) + + -- "Subtitle". + if Flight and not Flight.isAI then + 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. + self.Tlastmessage=timer.getAbsTime() + (Delay or 0) + + -- 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) + + -- Get player data. + local playerData=Flight:_GetPlayerData() + + -- Check if player enabled his "voice". + if playerData==nil or playerData.myvoice then + + -- Spoken text. + local text=self:_GetTextForSpeech(Text) + + 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 + self:TextMessageToFlight(Text, Flight, 5, false, Delay) + end + + end + + -- Set time stamp. + self.Tlastmessage=timer.getAbsTime() + (Delay or 0) + + -- 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) + + if not spot.ParkingGuard then + + -- 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 + + 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 + +--- 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. +-- @return #string Callsign or "Ghostrider 1-1". +function FLIGHTCONTROL:_GetCallsignName(flight) + + local callsign=flight:GetCallsignName() + + --local name=string.match(callsign, "%a+") + --local number=string.match(callsign, "%d+") + + return callsign +end + + +--- 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 Spoken text. +function FLIGHTCONTROL:_GetTextForSpeech(text) + + --- Function to space out text. + local function space(text) + + local res="" + + for i=1, #text do + local char=text:sub(i,i) + res=res..char.." " + end + + return res + end + + -- Space out numbers. + local t=text:gsub("(%d+)", space) + + --TODO: 9 to niner. + + return t +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 + +--- 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 50f9116fa..6bc993d13 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -54,6 +54,8 @@ -- @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. +-- @field #boolean isReadyTO Flight is ready for takeoff. This is for FLIGHTCONTROL. -- -- @extends Ops.OpsGroup#OPSGROUP @@ -141,6 +143,8 @@ FLIGHTGROUP = { menu = nil, isHelo = nil, RTBRecallCount = 0, + playerSettings = {}, + playerWarnings = {}, } @@ -181,9 +185,32 @@ FLIGHTGROUP.RadioMessage = { TAXIING={normal="Taxiing", enhanced="Taxiing"}, } +--- Skill level. +-- @type FLIGHTGROUP.PlayerSkill +-- @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 = { + STUDENT = "Student", + AVIATOR = "Aviator", + GRADUATE = "Graduate", + INSTRUCTOR = "Instructor", +} + +--- 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.3" +FLIGHTGROUP.version="0.8.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -295,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() @@ -357,6 +385,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. @@ -365,7 +407,7 @@ function FLIGHTGROUP:SetFlightControl(flightcontrol) -- Check if there is already a FC. if self.flightcontrol then - if self.flightcontrol.airbasename==flightcontrol.airbasename then + if self.flightcontrol:IsControlling(self) then -- Flight control is already controlling this flight! return else @@ -379,11 +421,8 @@ function FLIGHTGROUP:SetFlightControl(flightcontrol) self.flightcontrol=flightcontrol -- Add flight to all flights. - table.insert(flightcontrol.flights, self) - - -- Update flight's F10 menu. - if self.isAI==false then - self:_UpdateMenu(0.5) + if not flightcontrol:IsFlight(self) then + table.insert(flightcontrol.flights, self) end return self @@ -534,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 flight is parking. +--- 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 @@ -562,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 @@ -609,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 @@ -710,8 +773,20 @@ 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) + + -- Not holding any more. + self.Tholding=nil + + -- Clear holding stack. + if self.stack then + self.stack.flightgroup=nil + self.stack=nil + end + end end @@ -795,18 +870,19 @@ 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) - - -- 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 element.status==OPSGROUP.ElementStatus.ENGINEON then - self:ElementTaxiing(element) - end + local dist=self:_GetDistToParking(element.parking, element.unit:GetCoord()) + + -- 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 + self:ElementTaxiing(element) end else @@ -1081,6 +1157,14 @@ 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) + + -- 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 :( -- when player starts cold, ? @@ -1092,6 +1176,7 @@ function FLIGHTGROUP:OnEventEngineStartup(EventData) self:ElementEngineOn(element) end end + ]] end end @@ -1169,6 +1254,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 @@ -1257,6 +1345,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) @@ -1307,8 +1399,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 @@ -1482,12 +1576,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 @@ -1529,6 +1624,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 @@ -1549,6 +1649,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) @@ -1580,12 +1686,20 @@ function FLIGHTGROUP:onafterSpawned(From, Event, To) self:__UpdateRoute(-0.5) 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(0.5) + end + else + self:_UpdateMenu(0.5) + end + end end @@ -1621,7 +1735,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 @@ -1629,12 +1743,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:T3(self.lid.."INFO: No flight control in onAfterParking!") end end @@ -1649,10 +1761,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 @@ -1661,9 +1770,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 @@ -1721,20 +1827,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 @@ -1743,7 +1836,7 @@ function FLIGHTGROUP:onafterCruise(From, Event, To) -- CLIENT --- - self:_UpdateMenu(0.1) + --self:_UpdateMenu(0.1) end @@ -1757,8 +1850,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 @@ -1771,7 +1879,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 @@ -2173,10 +2281,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 @@ -2193,15 +2314,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)) @@ -2279,6 +2391,9 @@ end -- @param #number SpeedHold Holding speed in knots. function FLIGHTGROUP:onbeforeRTB(From, Event, To, airbase, SpeedTo, SpeedHold) + -- Debug info. + self:T(self.lid..string.format("RTB: before event=%s: %s --> %s to %s", Event, From, To, airbase and airbase:GetName() or "None")) + if self:IsAlive() then local allowed=true @@ -2398,6 +2513,7 @@ function FLIGHTGROUP:onafterRTB(From, Event, To, airbase, SpeedTo, SpeedHold, Sp end + -- Land at airbase. self:_LandAtAirbase(airbase, SpeedTo, SpeedHold, SpeedLand) end @@ -2516,16 +2632,29 @@ 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 fc and self.isAI then + + -- Get holding stack from flight control. + local stack=fc:_GetHoldingStack(self) + + if stack then + + stack.flightgroup=self + self.stack=stack + + -- Race track points. + p0=stack.pos0 + p1=stack.pos1 + + -- Debug marks. + 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 + end -- Set flightcontrol for this flight. @@ -2533,6 +2662,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. @@ -2547,7 +2692,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) @@ -2582,8 +2727,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) @@ -2782,17 +2931,37 @@ 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) -- 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 self.isAI then + + -- Callsign. + local callsign=self:GetCallsignName() - if not self.isAI then - self:_UpdateMenu() + -- 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 @@ -3045,6 +3214,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. @@ -3126,6 +3309,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={} @@ -3133,8 +3317,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. @@ -3154,8 +3338,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. @@ -3583,6 +3768,34 @@ 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 + +--- 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 @@ -3597,6 +3810,13 @@ function FLIGHTGROUP:_SetElementParkingAt(Element, Spot) -- Debug info. self:T(self.lid..string.format("Element %s is parking on spot %d", Element.name, Spot.TerminalID)) + + -- Get flightcontrol. + local fc=_DATABASE:GetFlightControl(Spot.AirbaseName) + + if fc and not self.flightcontrol then + self:SetFlightControl(fc) + end if self.flightcontrol then @@ -3730,16 +3950,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 @@ -3747,8 +3967,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 dist0 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 airbase=AIRBASE:FindByName(airbasename) - - local coord=airbase:GetCoordinate() - - local dist=coord:Get2DDistance(position) - - local fcitem={airbasename=airbasename, dist=dist} - - table.insert(fc, fcitem) - end - - -- Sort table wrt distance to airbases. - local function _sort(a,b) - return a.dist=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) + + -- 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 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 dist0 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 +2308,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 +2360,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:T(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). @@ -2664,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 @@ -2775,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 diff --git a/Moose Development/Moose/Ops/NavyGroup.lua b/Moose Development/Moose/Ops/NavyGroup.lua index f0d5268e0..3c52c0258 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 @@ -780,7 +780,11 @@ function NAVYGROUP:Status(From, Event, To) 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 @@ -987,8 +991,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/Operation.lua b/Moose Development/Moose/Ops/Operation.lua new file mode 100644 index 000000000..158af4fd4 --- /dev/null +++ b/Moose Development/Moose/Ops/Operation.lua @@ -0,0 +1,1163 @@ +--- **Ops** - Operation with multiple phases. +-- +-- ## Main Features: +-- +-- * Define operation phases +-- * Define conditions when phases are over +-- * 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 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 targets Targets. +-- @field #table missions Missions. +-- @extends Core.Fsm#FSM + +--- *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, + branches = {}, + counterPhase = 0, + counterBranch = 0, + counterEdge = 0, + cohorts = {}, + legions = {}, + targets = {}, + missions = {}, +} + +--- 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 #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 +-- @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 +OPERATION.version="0.1.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Braches? +-- TODO: Over conditions. +-- DONE: Phases. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- 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 + + -- 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") + + -- Master branch. + self.branchMaster=self:AddBranch("Master") + + -- Set master as active branch. + self.branchActive=self.branchMaster + + -- 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("*", "PhaseOver", "*") + self:AddTransition("*", "PhaseNext", "*") + self:AddTransition("*", "PhaseChange", "*") + + self:AddTransition("*", "BranchSwitch", "*") + + self:AddTransition("*", "Over", "Over") + + self:AddTransition("*", "Stop", "Stopped") + + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". + -- @function [parent=#OPERATION] Start + -- @param #OPERATION self + + --- 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 + + --- 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 "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 + -- @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. + + + --- 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 + -- @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 "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 + + --- 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) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User API Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set verbosity level. +-- @param #OPERATION self +-- @param #number VerbosityLevel Level of output (higher=more). Default 0. +-- @return #OPERATION self +function OPERATION:SetVerbosity(VerbosityLevel) + self.verbose=VerbosityLevel or 0 + return self +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 + +--- 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, Branch) + + -- Branch. + Branch=Branch or self.branchMaster + + -- 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(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. +-- @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 + + +--- 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 + +--- Assign cohort to operation. +-- @param #OPERATION self +-- @param Ops.Cohort#COHORT Cohort The cohort +-- @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 + +--- 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 + +--- 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 + + -- 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 + + 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 + +--- 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 "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". +function OPERATION:IsStopped() + local is=self:is("Stopped") + return is +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) + + -- Debug message. + self:T(self.lid..string.format("Starting Operation!")) + +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() + + 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() + end + + -- Debug output. + if self.verbose>=1 then + + -- Current phase. + 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 [%s], Phases=%d [Active=%d, Planned=%d, Over=%d]", fsmstate, phaseName, branchName, 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.branchActive.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 + + -- Next status update. + self:__StatusUpdate(-30) +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. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPERATION.Phase Phase The new 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:I(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 "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. +-- @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!")) + + -- No active phase. + self.phase=nil + + -- Set all phases to OVER. + 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 (private) Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check phases. +-- @param #OPERATION self +function OPERATION:_CheckPhases() + + -- Currently active phase. + local phase=self:GetPhaseActive() + + -- 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.status==OPERATION.PhaseStatus.OVER then + + 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) + + -- 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 ebbfdf6ef..840911bfd 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 = {}, } @@ -205,6 +208,9 @@ 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 #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. @@ -257,17 +263,39 @@ OPSGROUP = { -- @field #string ARRIVED Element 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.ElementStatus={ - INUTERO="inutero", - SPAWNED="spawned", - PARKING="parking", - ENGINEON="engineon", - TAXIING="taxiing", - TAKEOFF="takeoff", - AIRBORNE="airborne", - LANDING="landing", - LANDED="landed", - ARRIVED="arrived", - DEAD="dead", + INUTERO="InUtero", + SPAWNED="Spawned", + PARKING="Parking", + ENGINEON="Engine On", + TAXIING="Taxiing", + TAKEOFF="Takeoff", + AIRBORNE="Airborne", + LANDING="Landing", + LANDED="Landed", + ARRIVED="Arrived", + 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. @@ -308,6 +336,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 +369,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. @@ -432,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={ @@ -469,21 +499,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 @@ -621,8 +651,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. @@ -681,6 +712,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. @@ -707,6 +739,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. @@ -1150,6 +1183,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. @@ -3186,7 +3374,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) @@ -3236,10 +3453,37 @@ function OPSGROUP:OnEventRemoveUnit(EventData) end +--- Event function handling when a unit is removed from the game. +-- @param #OPSGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function OPSGROUP:OnEventPlayerLeaveUnit(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 + self:T2(self.lid..string.format("EVENT: Player left Unit %s!", EventData.IniUnitName)) + + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + -- Get element. + local element=self:GetElementByName(unitname) + + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + self:T(self.lid..string.format("EVENT: Player left Element %s ==> 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 @@ -3351,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 @@ -3512,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 @@ -3946,6 +4191,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 @@ -4267,8 +4523,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 @@ -4430,6 +4688,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) @@ -4451,9 +4712,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. @@ -4464,8 +4729,11 @@ 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=#self.pausedmissions,1,-1 do + local mid=self.pausedmissions[j] + if Mission.auftragsnummer==mid then + table.remove(self.pausedmissions, j) + end end -- Remove mission from queue. @@ -4723,7 +4991,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 @@ -4749,6 +5022,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 @@ -4833,7 +5111,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 @@ -4845,19 +5123,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 @@ -4879,31 +5166,45 @@ 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 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 @@ -4997,6 +5298,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() @@ -5089,6 +5398,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 @@ -5105,7 +5420,7 @@ function OPSGROUP:RouteToMission(mission, delay) 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 @@ -5135,12 +5450,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) @@ -5154,13 +5466,13 @@ 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 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 --- @@ -5237,7 +5549,7 @@ function OPSGROUP:RouteToMission(mission, delay) waypointcoord=CarrierCoordinate:Translate(10000, heading-180):SetAltitude(2000) - waypointcoord:MarkToAll("Recoverytanker",ReadOnly,Text) + waypointcoord:MarkToAll("Recoverytanker") else --- @@ -5291,65 +5603,39 @@ 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 @@ -6597,6 +6898,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. @@ -6762,6 +7094,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(0) + elseif self:IsNavygroup() then + Template.route.points[1]=Coordinate:WaypointNaval(0) end -- Template units. @@ -6833,6 +7169,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 @@ -7068,14 +7405,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 @@ -7091,6 +7425,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 @@ -7133,6 +7471,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 @@ -7153,13 +7493,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 @@ -8328,12 +8668,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() @@ -8342,9 +8682,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 @@ -8368,10 +8712,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) @@ -8979,7 +9320,10 @@ function OPSGROUP:onafterUnloaded(From, Event, To, OpsGroupCargo) OpsGroupCargo:Returned() end - if OpsGroupCargo.missionpaused then + -- Check if there is a paused mission. + local paused=OpsGroupCargo:_CountPausedMissions()>0 + + if paused then OpsGroupCargo:UnpauseMission() end @@ -9253,9 +9597,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() @@ -9272,7 +9613,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))) @@ -9558,10 +9913,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 @@ -10790,6 +11149,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 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -11237,6 +11693,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 @@ -11288,27 +11746,21 @@ 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 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 - + local element=self:GetElementAlive() + + 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 callsign + + return "Ghostrider11" end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -12255,7 +12707,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. @@ -12263,6 +12716,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 @@ -12273,7 +12727,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/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. 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/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 diff --git a/Moose Development/Moose/Ops/Target.lua b/Moose Development/Moose/Ops/Target.lua index e06e7df43..8129059c4 100644 --- a/Moose Development/Moose/Ops/Target.lua +++ b/Moose Development/Moose/Ops/Target.lua @@ -36,6 +36,9 @@ -- @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. +-- @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 @@ -64,7 +67,8 @@ TARGET = { Ndead = 0, elements = {}, casualties = {}, - threatlevel0 = 0 + threatlevel0 = 0, + conditionStart = {}, } @@ -113,6 +117,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 +321,142 @@ 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. +-- @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. 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/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index d9308e8b5..650aaea3a 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -1155,7 +1155,7 @@ function UTILS.VecHdg(a) end --- Calculate "heading" of a 2D vector in the X-Y plane. --- @param DCS#Vec2 a Vector in "D with x, y components. +-- @param DCS#Vec2 a Vector in 2D with x, y components. -- @return #number Heading in degrees in [0,360). function UTILS.Vec2Hdg(a) local h=math.deg(math.atan2(a.y, a.x)) diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index edde1fdc2..c21fb28f1 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -25,9 +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: @@ -69,7 +71,6 @@ AIRBASE = { [Airbase.Category.HELIPAD] = "Helipad", [Airbase.Category.SHIP] = "Ship", }, - activerwyno=nil, } --- Enumeration to identify the airbases in the Caucasus region. @@ -522,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. @@ -573,11 +575,17 @@ AIRBASE.SpotStatus = { --- Runway data. -- @type AIRBASE.Runway --- @field #number heading Heading of the runway in degrees. +-- @field #string name Runway name. -- @field #string idx Runway ID: heading 070° ==> 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 @@ -624,6 +632,14 @@ function AIRBASE:Register(AirbaseName) else self:E("ERROR: Unknown airbase category!") end + + -- Init Runways. + self:_InitRunways() + + -- Set the active runways based on wind direction. + if self.isAirdrome then + self:SetActiveRunway() + end -- Init parking spots. self:_InitParkingSpots() @@ -841,6 +857,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 @@ -1022,6 +1074,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, clientname + end + end + end + return false, nil + end + -- Put coordinates of parking spots into table. for _,spot in pairs(parkingdata) do @@ -1035,6 +1104,8 @@ function AIRBASE:_InitParkingSpots() park.TerminalID0=spot.Term_Index_0 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 @@ -1094,7 +1165,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) @@ -1132,7 +1202,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) @@ -1484,6 +1553,248 @@ end -- Runway ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Get runways. +-- @param #AIRBASE self +-- @return #table Runway data. +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. +-- @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. @@ -1651,26 +1962,100 @@ 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) - - -- Return user forced active runway if it was set. - if self.activerwyno then - return runways[self.activerwyno] + 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() -- Get wind vector. local Vwind=self:GetCoordinate():GetWindWithTurbulenceVec3() @@ -1691,33 +2076,64 @@ 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) - - -- Debug. - --env.info(string.format("runway=%03d° dot=%.3f", runway.heading, dot)) - - -- New min? - if dotmin==nil or dot