From eed74d72cd0964e283c1ff95bf0e0cac8b72eb39 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 18 Aug 2022 01:33:09 +0200 Subject: [PATCH] SOCKEt - Added new SOCKET class - Fixed bug in RANGE that self.PlayerSetti**n**gs was misspelled. --- MOOSE_INCLUDE/Moose_Include_Static/Moose.lua | 80365 ++++++++++++---- Moose Development/Moose/Functional/Range.lua | 62 +- Moose Development/Moose/Modules.lua | 1 + Moose Development/Moose/Ops/Airboss.lua | 4 +- Moose Development/Moose/Utilities/Socket.lua | 117 + .../Moose/Wrapper/Positionable.lua | 3 +- 6 files changed, 64048 insertions(+), 16504 deletions(-) create mode 100644 Moose Development/Moose/Utilities/Socket.lua diff --git a/MOOSE_INCLUDE/Moose_Include_Static/Moose.lua b/MOOSE_INCLUDE/Moose_Include_Static/Moose.lua index b0c1854b8..6094c2afc 100644 --- a/MOOSE_INCLUDE/Moose_Include_Static/Moose.lua +++ b/MOOSE_INCLUDE/Moose_Include_Static/Moose.lua @@ -14,7 +14,7 @@ env.info( '*** MOOSE STATIC INCLUDE START *** ' ) -- -- DCS itself provides a lot of enumerators for various things. See [Enumerators](https://wiki.hoggitworld.com/view/Category:Enumerators) on Hoggit. -- --- Other Moose classe also have enumerators. For example, the AIRBASE class has enumerators for airbase names. +-- Other Moose classes also have enumerators. For example, the AIRBASE class has enumerators for airbase names. -- -- @module ENUMS -- @image MOOSE.JPG @@ -129,6 +129,8 @@ ENUMS.WeaponFlag={ AnyMissile = 268402688, -- AnyASM + AnyAAM --- Guns Cannons = 805306368, -- GUN_POD + BuiltInCannon + --- Torpedo + Torpedo = 4294967296, --- -- Even More Genral Auto = 3221225470, -- Any Weapon (AnyBomb + AnyRocket + AnyMissile + Cannons) @@ -139,6 +141,93 @@ ENUMS.WeaponFlag={ AnyGuided = 268402702, -- Any Guided Weapon } +--- Weapon types by category. See the [Weapon Flag](https://wiki.hoggitworld.com/view/DCS_enum_weapon_flag) enumerator on hoggit wiki. +-- @type ENUMS.WeaponType +-- @field #table Bomb Bombs. +-- @field #table Rocket Rocket. +-- @field #table Gun Guns. +-- @field #table Missile Missiles. +-- @field #table AAM Air-to-Air missiles. +-- @field #table Torpedo Torpedos. +-- @field #table Any Combinations. +ENUMS.WeaponType={} +ENUMS.WeaponType.Bomb={ + -- Bombs + LGB = 2, + TvGB = 4, + SNSGB = 8, + HEBomb = 16, + Penetrator = 32, + NapalmBomb = 64, + FAEBomb = 128, + ClusterBomb = 256, + Dispencer = 512, + CandleBomb = 1024, + ParachuteBomb = 2147483648, + -- Combinations + GuidedBomb = 14, -- (LGB + TvGB + SNSGB) + AnyUnguidedBomb = 2147485680, -- (HeBomb + Penetrator + NapalmBomb + FAEBomb + ClusterBomb + Dispencer + CandleBomb + ParachuteBomb) + AnyBomb = 2147485694, -- (GuidedBomb + AnyUnguidedBomb) +} +ENUMS.WeaponType.Rocket={ + -- Rockets + LightRocket = 2048, + MarkerRocket = 4096, + CandleRocket = 8192, + HeavyRocket = 16384, + -- Combinations + AnyRocket = 30720, -- LightRocket + MarkerRocket + CandleRocket + HeavyRocket +} +ENUMS.WeaponType.Gun={ + -- Guns + GunPod = 268435456, + BuiltInCannon = 536870912, + -- Combinations + Cannons = 805306368, -- GUN_POD + BuiltInCannon +} +ENUMS.WeaponType.Missile={ + -- Missiles + AntiRadarMissile = 32768, + AntiShipMissile = 65536, + AntiTankMissile = 131072, + FireAndForgetASM = 262144, + LaserASM = 524288, + TeleASM = 1048576, + CruiseMissile = 2097152, + AntiRadarMissile2 = 1073741824, + -- Combinations + GuidedASM = 1572864, -- (LaserASM + TeleASM) + TacticalASM = 1835008, -- (GuidedASM + FireAndForgetASM) + AnyASM = 4161536, -- (AntiRadarMissile + AntiShipMissile + AntiTankMissile + FireAndForgetASM + GuidedASM + CruiseMissile) + AnyASM2 = 1077903360, -- 4161536+1073741824, + AnyAutonomousMissile = 36012032, -- IR_AAM + AntiRadarMissile + AntiShipMissile + FireAndForgetASM + CruiseMissile + AnyMissile = 268402688, -- AnyASM + AnyAAM +} +ENUMS.WeaponType.AAM={ + -- Air-To-Air Missiles + SRAM = 4194304, + MRAAM = 8388608, + LRAAM = 16777216, + IR_AAM = 33554432, + SAR_AAM = 67108864, + AR_AAM = 134217728, + -- Combinations + AnyAAM = 264241152, -- IR_AAM + SAR_AAM + AR_AAM + SRAAM + MRAAM + LRAAM +} +ENUMS.WeaponType.Torpedo={ + -- Torpedo + Torpedo = 4294967296, +} +ENUMS.WeaponType.Any={ + -- General combinations + Weapon = 3221225470, -- Any Weapon (AnyBomb + AnyRocket + AnyMissile + Cannons) + AG = 2956984318, -- Any Air-To-Ground Weapon + AA = 264241152, -- Any Air-To-Air Weapon + Unguided = 2952822768, -- Any Unguided Weapon + Guided = 268402702, -- Any Guided Weapon +} + + --- Mission tasks. -- @type ENUMS.MissionTask -- @field #string NOTHING No special task. Group can perform the minimal tasks: Orbit, Refuelling, Follow and Aerobatics. @@ -362,7 +451,118 @@ ENUMS.Phonetic = X = 'Xray', Y = 'Yankee', Z = 'Zulu', -}--- Various routines +} + +--- Reporting Names (NATO). See the [Wikipedia](https://en.wikipedia.org/wiki/List_of_NATO_reporting_names_for_fighter_aircraft). +-- DCS known aircraft types +-- +-- @type ENUMS.ReportingName +ENUMS.ReportingName = +{ + NATO = { + -- Fighters + Dragon = "JF-17", -- China, correctly Fierce Dragon, Thunder for PAC + Fagot = "MiG-15", + Farmer = "MiG-19", -- Shenyang J-6 and Mikoyan-Gurevich MiG-19 + Felon = "Su-57", + Fencer = "Su-24", + Fishbed = "MiG-21", + Fitter = "Su-17", -- Sukhoi Su-7 and Su-17/Su-20/Su-22 + Flogger = "MiG-23", --and MiG-27 + Flogger_D = "MiG-27", --and MiG-23 + Flagon = "Su-15", + Foxbat = "MiG-25", + Fulcrum = "MiG-29", + Foxhound = "MiG-31", + Flanker = "Su-27", -- Sukhoi Su-27/Su-30/Su-33/Su-35/Su-37 and Shenyang J-11/J-15/J-16 + Flanker_C = "Su-30", + Flanker_E = "Su-35", + Flanker_F = "Su-37", + Flanker_L = "J-11A", + Firebird = "J-10", + Sea_Flanker = "Su-33", + Fullback = "Su-34", -- also Su-32 + Frogfoot = "Su-25", + Tomcat = "F-14", -- Iran + Mirage = "Mirage", -- various non-NATO + Codling = "Yak-40", + Maya = "L-39", + -- Fighters US/NATO + Warthog = "A-10", + --Mosquito = "A-20", + Skyhawk = "A-4E", + Viggen = "AJS37", + Harrier = "AV-8B", + Spirit = "B-2", + Aviojet = "C-101", + Nighthawk = "F-117A", + Eagle = "F-15C", + Mudhen = "F-15E", + Viper = "F-16", + Phantom = "F-4E", + Tiger = "F-5", -- was thinkg to name this MiG-25 ;) + Sabre = "F-86", + Hornet = "A-18", -- avoiding the slash + Hawk = "Hawk", + Albatros = "L-39", + Goshawk = "T-45", + Starfighter = "F-104", + Tornado = "Tornado", + -- Transport / Bomber / Others + Atlas = "A400", + Lancer = "B1-B", + Stratofortress = "B-52H", + Hercules = "C-130", + Super_Hercules = "Hercules", + Globemaster = "C-17", + Greyhound = "C-2A", + Galaxy = "C-5", + Hawkeye = "E-2D", + Sentry = "E-3A", + Stratotanker = "KC-135", + Extender = "KC-10", + Orion = "P-3C", + Viking = "S-3B", + Osprey = "V-22", + -- Bomber Rus + Badger = "H6-J", + Bear_J = "Tu-142", -- also Tu-95 + Bear = "Tu-95", -- also Tu-142 + Blinder = "Tu-22", + Blackjack = "Tu-160", + -- AIC / Transport / Other + Clank = "An-30", + Curl = "An-26", + Candid = "IL-76", + Midas = "IL-78", + Mainstay = "A-50", + Mainring = "KJ-2000", -- A-50 China + Yak = "Yak-52", + -- Helos + Helix = "Ka-27", + Shark = "Ka-50", + Hind = "Mi-24", + Halo = "Mi-26", + Hip = "Mi-8", + Havoc = "Mi-28", + Gazelle = "SA342", + -- Helos US + Huey = "UH-1H", + Cobra = "AH-1", + Apache = "AH-64", + Chinook = "CH-47", + Sea_Stallion = "CH-53", + Kiowa = "OH-58", + Seahawk = "SH-60", + Blackhawk = "UH-60", + Sea_King = "S-61", + -- Drones + UCAV = "WingLoong", + Reaper = "MQ-9", + Predator = "MQ-1A", + } +} +--- Various routines -- @module routines -- @image MOOSE.JPG @@ -1114,7 +1314,7 @@ routines.groupRandomDistSelf = function(gpData, dist, form, heading, speed) local pos = routines.getLeadPos(gpData) local fakeZone = {} fakeZone.radius = dist or math.random(300, 1000) - fakeZone.point = {x = pos.x, y, pos.y, z = pos.z} + fakeZone.point = {x = pos.x, y = pos.y, z = pos.z} routines.groupToRandomZone(gpData, fakeZone, form, heading, speed) return @@ -2864,7 +3064,7 @@ end env.info(( 'Init: Scripts Loaded v1.1' )) ---- This module contains derived utilities taken from the MIST framework, which are excellent tools to be reused in an OO environment. +--- This module contains derived utilities taken from the MIST framework, as well as a lot of added helpers from the MOOSE community. -- -- ### Authors: -- @@ -2873,6 +3073,7 @@ env.info(( 'Init: Scripts Loaded v1.1' )) -- ### Contributions: -- -- * FlightControl : Rework to OO framework. +-- * And many more -- -- @module Utils -- @image MOOSE.JPG @@ -2917,6 +3118,7 @@ BIGSMOKEPRESET = { -- @field #string TheChannel The Channel map. -- @field #string Syria Syria map. -- @field #string MarianaIslands Mariana Islands map. +-- @field #string Falklands South Atlantic map. DCSMAP = { Caucasus="Caucasus", NTTR="Nevada", @@ -2924,7 +3126,8 @@ DCSMAP = { PersianGulf="PersianGulf", TheChannel="TheChannel", Syria="Syria", - MarianaIslands="MarianaIslands" + MarianaIslands="MarianaIslands", + Falklands="Falklands", } @@ -2996,6 +3199,62 @@ CALLSIGN={ Dublin=9, Perth=10, }, + F16={ + Viper=9, + Venom=10, + Lobo=11, + Cowboy=12, + Python=13, + Rattler=14, + Panther=15, + Wolf=16, + Weasel=17, + Wild=18, + Ninja=19, + Jedi=20, + }, + F18={ + Hornet=9, + Squid=10, + Ragin=11, + Roman=12, + Sting=13, + Jury=14, + Jokey=15, + Ram=16, + Hawk=17, + Devil=18, + Check=19, + Snake=20, + }, + F15E={ + Dude=9, + Thud=10, + Gunny=11, + Trek=12, + Sniper=13, + Sled=14, + Best=15, + Jazz=16, + Rage=17, + Tahoe=18, + }, + B1B={ + Bone=9, + Dark=10, + Vader=11 + }, + B52={ + Buff=9, + Dump=10, + Kenworth=11, + }, + TransportAircraft={ + Heavy=9, + Trash=10, + Cargo=11, + Ascot=12, + }, } --#CALLSIGN --- Utilities static class. @@ -3275,13 +3534,17 @@ end -- @param #number knots Speed in knots. -- @return #number Speed in m/s. UTILS.KnotsToMps = function( knots ) - return knots / 1.94384 --* 1852 / 3600 + if type(knots) == "number" then + return knots / 1.94384 --* 1852 / 3600 + else + return 0 + end end ---- Convert temperature from Celsius to Farenheit. +--- Convert temperature from Celsius to Fahrenheit. -- @param #number Celcius Temperature in degrees Celsius. --- @return #number Temperature in degrees Farenheit. -UTILS.CelciusToFarenheit = function( Celcius ) +-- @return #number Temperature in degrees Fahrenheit. +UTILS.CelsiusToFahrenheit = function( Celcius ) return Celcius * 9/5 + 32 end @@ -3614,7 +3877,7 @@ function UTILS.BeaufortScale(speed) return bn,bd end ---- Split string at seperators. C.f. http://stackoverflow.com/questions/1426954/split-string-in-lua +--- Split string at seperators. C.f. [split-string-in-lua](http://stackoverflow.com/questions/1426954/split-string-in-lua). -- @param #string str Sting to split. -- @param #string sep Speparator for split. -- @return #table Split text. @@ -3836,6 +4099,15 @@ function UTILS.VecDot(a, b) return a.x*b.x + a.y*b.y + a.z*b.z end +--- Calculate the [dot product](https://en.wikipedia.org/wiki/Dot_product) of two 2D vectors. The result is a number. +-- @param DCS#Vec2 a Vector in 2D with x, y components. +-- @param DCS#Vec2 b Vector in 2D with x, y components. +-- @return #number Scalar product of the two vectors a*b. +function UTILS.Vec2Dot(a, b) + return a.x*b.x + a.y*b.y +end + + --- Calculate the [euclidean norm](https://en.wikipedia.org/wiki/Euclidean_distance) (length) of a 3D vector. -- @param DCS#Vec3 a Vector in 3D with x, y, z components. -- @return #number Norm of the vector. @@ -3843,6 +4115,13 @@ function UTILS.VecNorm(a) return math.sqrt(UTILS.VecDot(a, a)) end +--- Calculate the [euclidean norm](https://en.wikipedia.org/wiki/Euclidean_distance) (length) of a 2D vector. +-- @param DCS#Vec2 a Vector in 2D with x, y components. +-- @return #number Norm of the vector. +function UTILS.Vec2Norm(a) + return math.sqrt(UTILS.Vec2Dot(a, a)) +end + --- Calculate the distance between two 2D vectors. -- @param DCS#Vec2 a Vector in 3D with x, y components. -- @param DCS#Vec2 b Vector in 3D with x, y components. @@ -3886,6 +4165,14 @@ function UTILS.VecSubstract(a, b) return {x=a.x-b.x, y=a.y-b.y, z=a.z-b.z} end +--- Calculate the difference between two 2D vectors by substracting the x,y components from each other. +-- @param DCS#Vec2 a Vector in 2D with x, y components. +-- @param DCS#Vec2 b Vector in 2D with x, y components. +-- @return DCS#Vec2 Vector c=a-b with c(i)=a(i)-b(i), i=x,y. +function UTILS.Vec2Substract(a, b) + return {x=a.x-b.x, y=a.y-b.y} +end + --- Calculate the total vector of two 3D vectors by adding the x,y,z components of each other. -- @param DCS#Vec3 a Vector in 3D with x, y, z components. -- @param DCS#Vec3 b Vector in 3D with x, y, z components. @@ -3894,6 +4181,14 @@ function UTILS.VecAdd(a, b) return {x=a.x+b.x, y=a.y+b.y, z=a.z+b.z} end +--- Calculate the total vector of two 2D vectors by adding the x,y components of each other. +-- @param DCS#Vec2 a Vector in 2D with x, y components. +-- @param DCS#Vec2 b Vector in 2D with x, y components. +-- @return DCS#Vec2 Vector c=a+b with c(i)=a(i)+b(i), i=x,y. +function UTILS.Vec2Add(a, b) + return {x=a.x+b.x, y=a.y+b.y} +end + --- Calculate the angle between two 3D vectors. -- @param DCS#Vec3 a Vector in 3D with x, y, z components. -- @param DCS#Vec3 b Vector in 3D with x, y, z components. @@ -3925,6 +4220,17 @@ function UTILS.VecHdg(a) return h end +--- Calculate "heading" of a 2D vector in the X-Y plane. +-- @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)) + if h<0 then + h=h+360 + end + return h +end + --- Calculate the difference between two "heading", i.e. angles in [0,360) deg. -- @param #number h1 Heading one. -- @param #number h2 Heading two. @@ -3961,6 +4267,22 @@ function UTILS.VecTranslate(a, distance, angle) return {x=TX, y=a.y, z=TY} end +--- Translate 2D vector in the 2D (x,z) plane. +-- @param DCS#Vec2 a Vector in 2D with x, y components. +-- @param #number distance The distance to translate. +-- @param #number angle Rotation angle in degrees. +-- @return DCS#Vec2 Translated vector. +function UTILS.Vec2Translate(a, distance, angle) + + local SX = a.x + local SY = a.y + local Radians=math.rad(angle or 0) + local TX=distance*math.cos(Radians)+SX + local TY=distance*math.sin(Radians)+SY + + return {x=TX, y=TY} +end + --- Rotate 3D vector in the 2D (x,z) plane. y-component (usually altitude) unchanged. -- @param DCS#Vec3 a Vector in 3D with x, y, z components. -- @param #number angle Rotation angle in degrees. @@ -3981,6 +4303,25 @@ function UTILS.Rotate2D(a, angle) return A end +--- Rotate 2D vector in the 2D (x,z) plane. +-- @param DCS#Vec2 a Vector in 2D with x, y components. +-- @param #number angle Rotation angle in degrees. +-- @return DCS#Vec2 Vector rotated in the (x,y) plane. +function UTILS.Vec2Rotate2D(a, angle) + + local phi=math.rad(angle) + + local x=a.x + local y=a.y + + local X=x*math.cos(phi)-y*math.sin(phi) + local Y=x*math.sin(phi)+y*math.cos(phi) + + local A={x=X, y=Y} + + return A +end + --- Converts a TACAN Channel/Mode couple into a frequency in Hz. -- @param #number TACANChannel The TACAN channel, i.e. the 10 in "10X". @@ -4064,26 +4405,6 @@ function UTILS.GetMissionDayOfYear(Time) end ---- Returns the current date. --- @return #string Mission date in yyyy/mm/dd format. --- @return #number The year anno domini. --- @return #number The month. --- @return #number The day. -function UTILS.GetDate() - - -- Mission start date - local date, year, month, day=UTILS.GetDCSMissionDate() - - local time=timer.getAbsTime() - - local clock=UTILS.SecondsToClock(time, false) - - local x=tonumber(UTILS.Split(clock, "+")[2]) - - local day=day+x - -end - --- Returns the magnetic declination of the map. -- Returned values for the current maps are: -- @@ -4094,6 +4415,7 @@ end -- * The Cannel Map -10 (West) -- * Syria +5 (East) -- * Mariana Islands +2 (East) +-- * Falklands +12 (East) - note there's a LOT of deviation across the map, as we're closer to the South Pole -- @param #string map (Optional) Map for which the declination is returned. Default is from env.mission.theatre -- @return #number Declination in degrees. function UTILS.GetMagneticDeclination(map) @@ -4116,6 +4438,8 @@ function UTILS.GetMagneticDeclination(map) declination=5 elseif map==DCSMAP.MarianaIslands then declination=2 + elseif map==DCSMAP.Falklands then + declination=12 else declination=0 end @@ -4194,6 +4518,23 @@ function UTILS.GetModulationName(Modulation) end +--- Get the NATO reporting name of a unit type name +-- @param #number Typename The type name. +-- @return #string The Reporting name or "Bogey". +function UTILS.GetReportingName(Typename) + + local typename = string.lower(Typename) + + for name, value in pairs(ENUMS.ReportingName.NATO) do + local svalue = string.lower(value) + if string.find(typename,svalue,1,true) then + return name + end + end + + return "Bogey" +end + --- Get the callsign name from its enumerator value -- @param #number Callsign The enumerator callsign. -- @return #string The callsign name or "Ghostrider". @@ -4222,7 +4563,49 @@ function UTILS.GetCallsignName(Callsign) return name end end - + + for name, value in pairs(CALLSIGN.B1B) do + if value==Callsign then + return name + end + end + + for name, value in pairs(CALLSIGN.B52) do + if value==Callsign then + return name + end + end + + for name, value in pairs(CALLSIGN.F15E) do + if value==Callsign then + return name + end + end + + for name, value in pairs(CALLSIGN.F16) do + if value==Callsign then + return name + end + end + + for name, value in pairs(CALLSIGN.F18) do + if value==Callsign then + return name + end + end + + for name, value in pairs(CALLSIGN.FARP) do + if value==Callsign then + return name + end + end + + for name, value in pairs(CALLSIGN.TransportAircraft) do + if value==Callsign then + return name + end + end + return "Ghostrider" end @@ -4415,8 +4798,8 @@ function UTILS.GetOSTime() end --- Shuffle a table accoring to Fisher Yeates algorithm ---@param #table t Table to be shuffled ---@return #table +--@param #table t Table to be shuffled. +--@return #table Shuffled table. function UTILS.ShuffleTable(t) if t == nil or type(t) ~= "table" then BASE:I("Error in ShuffleTable: Missing or wrong type of Argument") @@ -4434,60 +4817,99 @@ function UTILS.ShuffleTable(t) return TempTable end +--- Get a random element of a table. +--@param #table t Table. +--@param #boolean replace If `true`, the drawn element is replaced, i.e. not deleted. +--@return #number Table element. +function UTILS.GetRandomTableElement(t, replace) + + if t == nil or type(t) ~= "table" then + BASE:I("Error in ShuffleTable: Missing or wrong type of Argument") + return + end + + math.random() + math.random() + math.random() + + local r=math.random(#t) + + local element=t[r] + + if not replace then + table.remove(t, r) + end + + return element +end + --- (Helicopter) Check if one loading door is open. --@param #string unit_name Unit name to be checked --@return #boolean Outcome - true if a (loading door) is open, false if not, nil if none exists. function UTILS.IsLoadingDoorOpen( unit_name ) - local ret_val = false local unit = Unit.getByName(unit_name) + if unit ~= nil then local type_name = unit:getTypeName() + BASE:T("TypeName = ".. type_name) - if type_name == "Mi-8MT" and unit:getDrawArgumentValue(38) == 1 or unit:getDrawArgumentValue(86) == 1 or unit:getDrawArgumentValue(250) < 0 then + if type_name == "Mi-8MT" and (unit:getDrawArgumentValue(38) == 1 or unit:getDrawArgumentValue(86) == 1 or unit:getDrawArgumentValue(250) < 0) then BASE:T(unit_name .. " Cargo doors are open or cargo door not present") - ret_val = true + return true end - if type_name == "Mi-24P" and unit:getDrawArgumentValue(38) == 1 or unit:getDrawArgumentValue(86) == 1 then + if type_name == "Mi-24P" and (unit:getDrawArgumentValue(38) == 1 or unit:getDrawArgumentValue(86) == 1) then BASE:T(unit_name .. " a side door is open") - ret_val = true + return true end - if type_name == "UH-1H" and unit:getDrawArgumentValue(43) == 1 or unit:getDrawArgumentValue(44) == 1 then + if type_name == "UH-1H" and (unit:getDrawArgumentValue(43) == 1 or unit:getDrawArgumentValue(44) == 1) then BASE:T(unit_name .. " a side door is open ") - ret_val = true + return true + end + + if string.find(type_name, "SA342" ) and (unit:getDrawArgumentValue(34) == 1) then + BASE:T(unit_name .. " front door(s) are open or doors removed") + return true end - if string.find(type_name, "SA342" ) and unit:getDrawArgumentValue(34) == 1 or unit:getDrawArgumentValue(38) == 1 then - BASE:T(unit_name .. " front door(s) are open") - ret_val = true - end - - if string.find(type_name, "Hercules") and unit:getDrawArgumentValue(1215) == 1 and unit:getDrawArgumentValue(1216) == 1 then + if string.find(type_name, "Hercules") and (unit:getDrawArgumentValue(1215) == 1 and unit:getDrawArgumentValue(1216) == 1) then BASE:T(unit_name .. " rear doors are open") - ret_val = true + return true end if string.find(type_name, "Hercules") and (unit:getDrawArgumentValue(1220) == 1 or unit:getDrawArgumentValue(1221) == 1) then BASE:T(unit_name .. " para doors are open") - ret_val = true + return true end - if string.find(type_name, "Hercules") and unit:getDrawArgumentValue(1217) == 1 then + if string.find(type_name, "Hercules") and (unit:getDrawArgumentValue(1217) == 1) then BASE:T(unit_name .. " side door is open") - ret_val = true + return true end if string.find(type_name, "Bell-47") then -- bell aint got no doors so always ready to load injured soldiers BASE:T(unit_name .. " door is open") - ret_val = true + return true + end + + if string.find(type_name, "UH-60L") and (unit:getDrawArgumentValue(401) == 1 or unit:getDrawArgumentValue(402) == 1) then + BASE:T(unit_name .. " cargo door is open") + return true end - if ret_val == false then - BASE:T(unit_name .. " all doors are closed") + if string.find(type_name, "UH-60L" ) and (unit:getDrawArgumentValue(38) == 1 or unit:getDrawArgumentValue(400) == 1 ) then + BASE:T(unit_name .. " front door(s) are open") + return true end - return ret_val + + if type_name == "AH-64D_BLK_II" then + BASE:T(unit_name .. " front door(s) are open") + return true -- no doors on this one ;) + end + + return false end -- nil @@ -4638,7 +5060,475 @@ function UTILS.GenerateLaserCodes() end return jtacGeneratedLaserCodes end ---- **Utils** - Lua Profiler. + +--- Function to save an object to a file +-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. +-- @param #string Filename The name of the file. Existing file will be overwritten. +-- @param #table Data The LUA data structure to save. This will be e.g. a table of text lines with an \\n at the end of each line. +-- @return #boolean outcome True if saving is possible, else false. +function UTILS.SaveToFile(Path,Filename,Data) + -- Thanks to @FunkyFranky + -- Check io module is available. + if not io then + BASE:E("ERROR: io not desanitized. Can't save current file.") + return false + end + + -- Check default path. + if Path==nil and not lfs then + BASE:E("WARNING: lfs not desanitized. File will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") + end + + -- Set path or default. + local path = nil + if lfs then + path=Path or lfs.writedir() + end + + -- Set file name. + local filename=Filename + if path~=nil then + filename=path.."\\"..filename + end + + -- write + local f = assert(io.open(filename, "wb")) + f:write(Data) + f:close() + return true +end + +--- Function to save an object to a file +-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. +-- @param #string Filename The name of the file. +-- @return #boolean outcome True if reading is possible and successful, else false. +-- @return #table data The data read from the filesystem (table of lines of text). Each line is one single #string! +function UTILS.LoadFromFile(Path,Filename) + -- Thanks to @FunkyFranky + -- Check io module is available. + if not io then + BASE:E("ERROR: io not desanitized. Can't save current state.") + return false + end + + -- Check default path. + if Path==nil and not lfs then + BASE:E("WARNING: lfs not desanitized. Loading will look into your DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") + end + + -- Set path or default. + local path = nil + if lfs then + path=Path or lfs.writedir() + end + + -- Set file name. + local filename=Filename + if path~=nil then + filename=path.."\\"..filename + end + + -- Check if file exists. + local exists=UTILS.CheckFileExists(Path,Filename) + if not exists then + BASE:E(string.format("ERROR: File %s does not exist!",filename)) + return false + end + + -- read + local file=assert(io.open(filename, "rb")) + local loadeddata = {} + for line in file:lines() do + loadeddata[#loadeddata+1] = line + end + file:close() + return true, loadeddata +end + +--- Function to check if a file exists. +-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. +-- @param #string Filename The name of the file. +-- @return #boolean outcome True if reading is possible, else false. +function UTILS.CheckFileExists(Path,Filename) + -- Thanks to @FunkyFranky + -- Function that check if a file exists. + local function _fileexists(name) + local f=io.open(name,"r") + if f~=nil then + io.close(f) + return true + else + return false + end + end + + -- Check io module is available. + if not io then + BASE:E("ERROR: io not desanitized. Can't save current state.") + return false + end + + -- Check default path. + if Path==nil and not lfs then + BASE:E("WARNING: lfs not desanitized. Loading will look into your DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") + end + + -- Set path or default. + local path = nil + if lfs then + path=Path or lfs.writedir() + end + + -- Set file name. + local filename=Filename + if path~=nil then + filename=path.."\\"..filename + end + + -- Check if file exists. + local exists=_fileexists(filename) + if not exists then + BASE:E(string.format("ERROR: File %s does not exist!",filename)) + return false + else + return true + end +end + +--- Function to save the state of a list of groups found by name +-- @param #table List Table of strings with groupnames +-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. +-- @param #string Filename The name of the file. +-- @return #boolean outcome True if saving is successful, else false. +-- @usage +-- We will go through the list and find the corresponding group and save the current group size (0 when dead). +-- These groups are supposed to be put on the map in the ME and have *not* moved (e.g. stationary SAM sites). +-- Position is still saved for your usage. +-- The idea is to reduce the number of units when reloading the data again to restart the saved mission. +-- The data will be a simple comma separated list of groupname and size, with one header line. +function UTILS.SaveStationaryListOfGroups(List,Path,Filename) + local filename = Filename or "StateListofGroups" + local data = "--Save Stationary List of Groups: "..Filename .."\n" + for _,_group in pairs (List) do + local group = GROUP:FindByName(_group) -- Wrapper.Group#GROUP + if group and group:IsAlive() then + local units = group:CountAliveUnits() + local position = group:GetVec3() + data = string.format("%s%s,%d,%d,%d,%d\n",data,_group,units,position.x,position.y,position.z) + else + data = string.format("%s%s,0,0,0,0\n",data,_group) + end + end + -- save the data + local outcome = UTILS.SaveToFile(Path,Filename,data) + return outcome +end + +--- Function to save the state of a set of Wrapper.Group#GROUP objects. +-- @param Core.Set#SET_BASE Set of objects to save +-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. +-- @param #string Filename The name of the file. +-- @return #boolean outcome True if saving is successful, else false. +-- @usage +-- We will go through the set and find the corresponding group and save the current group size and current position. +-- The idea is to respawn the groups **spawned during an earlier run of the mission** at the given location and reduce +-- the number of units in the group when reloading the data again to restart the saved mission. Note that *dead* groups +-- cannot be covered with this. +-- **Note** Do NOT use dashes or hashes in group template names (-,#)! +-- The data will be a simple comma separated list of groupname and size, with one header line. +-- The current task/waypoint/etc cannot be restored. +function UTILS.SaveSetOfGroups(Set,Path,Filename) + local filename = Filename or "SetOfGroups" + local data = "--Save SET of groups: "..Filename .."\n" + local List = Set:GetSetObjects() + for _,_group in pairs (List) do + local group = _group -- Wrapper.Group#GROUP + if group and group:IsAlive() then + local name = group:GetName() + local template = string.gsub(name,"-(.+)$","") + if string.find(template,"#") then + template = string.gsub(name,"#(%d+)$","") + end + local units = group:CountAliveUnits() + local position = group:GetVec3() + data = string.format("%s%s,%s,%d,%d,%d,%d\n",data,name,template,units,position.x,position.y,position.z) + end + end + -- save the data + local outcome = UTILS.SaveToFile(Path,Filename,data) + return outcome +end + +--- Function to save the state of a set of Wrapper.Static#STATIC objects. +-- @param Core.Set#SET_BASE Set of objects to save +-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. +-- @param #string Filename The name of the file. +-- @return #boolean outcome True if saving is successful, else false. +-- @usage +-- We will go through the set and find the corresponding static and save the current name and postion when alive. +-- The data will be a simple comma separated list of name and state etc, with one header line. +function UTILS.SaveSetOfStatics(Set,Path,Filename) + local filename = Filename or "SetOfStatics" + local data = "--Save SET of statics: "..Filename .."\n" + local List = Set:GetSetObjects() + for _,_group in pairs (List) do + local group = _group -- Wrapper.Static#STATIC + if group and group:IsAlive() then + local name = group:GetName() + local position = group:GetVec3() + data = string.format("%s%s,%d,%d,%d\n",data,name,position.x,position.y,position.z) + end + end + -- save the data + local outcome = UTILS.SaveToFile(Path,Filename,data) + return outcome +end + +--- Function to save the state of a list of statics found by name +-- @param #table List Table of strings with statics names +-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. +-- @param #string Filename The name of the file. +-- @return #boolean outcome True if saving is successful, else false. +-- @usage +-- We will go through the list and find the corresponding static and save the current alive state as 1 (0 when dead). +-- Position is saved for your usage. **Note** this works on UNIT-name level. +-- The idea is to reduce the number of units when reloading the data again to restart the saved mission. +-- The data will be a simple comma separated list of name and state etc, with one header line. +function UTILS.SaveStationaryListOfStatics(List,Path,Filename) + local filename = Filename or "StateListofStatics" + local data = "--Save Stationary List of Statics: "..Filename .."\n" + for _,_group in pairs (List) do + local group = STATIC:FindByName(_group,false) -- Wrapper.Static#STATIC + if group and group:IsAlive() then + local position = group:GetVec3() + data = string.format("%s%s,1,%d,%d,%d\n",data,_group,position.x,position.y,position.z) + else + data = string.format("%s%s,0,0,0,0\n",data,_group) + end + end + -- save the data + local outcome = UTILS.SaveToFile(Path,Filename,data) + return outcome +end + +--- Load back a stationary list of groups from file. +-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. +-- @param #string Filename The name of the file. +-- @param #boolean Reduce If false, existing loaded groups will not be reduced to fit the saved number. +-- @return #table Table of data objects (tables) containing groupname, coordinate and group object. Returns nil when file cannot be read. +function UTILS.LoadStationaryListOfGroups(Path,Filename,Reduce) + local reduce = true + if Reduce == false then reduce = false end + local filename = Filename or "StateListofGroups" + local datatable = {} + if UTILS.CheckFileExists(Path,filename) then + local outcome,loadeddata = UTILS.LoadFromFile(Path,Filename) + -- remove header + table.remove(loadeddata, 1) + for _id,_entry in pairs (loadeddata) do + local dataset = UTILS.Split(_entry,",") + -- groupname,units,position.x,position.y,position.z + local groupname = dataset[1] + local size = tonumber(dataset[2]) + local posx = tonumber(dataset[3]) + local posy = tonumber(dataset[4]) + local posz = tonumber(dataset[5]) + local coordinate = COORDINATE:NewFromVec3({x=posx, y=posy, z=posz}) + local data = { groupname=groupname, size=size, coordinate=coordinate, group=GROUP:FindByName(groupname) } + if reduce then + local actualgroup = GROUP:FindByName(groupname) + if actualgroup and actualgroup:IsAlive() and actualgroup:CountAliveUnits() > size then + local reduction = actualgroup:CountAliveUnits() - size + BASE:I("Reducing groupsize by ".. reduction .. " units!") + -- reduce existing group + local units = actualgroup:GetUnits() + local units2 = UTILS.ShuffleTable(units) -- randomize table + for i=1,reduction do + units2[i]:Destroy(false) + end + end + end + table.insert(datatable,data) + end + else + return nil + end + return datatable +end + +--- Load back a SET of groups from file. +-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. +-- @param #string Filename The name of the file. +-- @param #boolean Spawn If set to false, do not re-spawn the groups loaded in location and reduce to size. +-- @return Core.Set#SET_GROUP Set of GROUP objects. +-- Returns nil when file cannot be read. Returns a table of data entries if Spawn is false: `{ groupname=groupname, size=size, coordinate=coordinate }` +function UTILS.LoadSetOfGroups(Path,Filename,Spawn) + local spawn = true + if Spawn == false then spawn = false end + BASE:I("Spawn = "..tostring(spawn)) + local filename = Filename or "SetOfGroups" + local setdata = SET_GROUP:New() + local datatable = {} + if UTILS.CheckFileExists(Path,filename) then + local outcome,loadeddata = UTILS.LoadFromFile(Path,Filename) + -- remove header + table.remove(loadeddata, 1) + for _id,_entry in pairs (loadeddata) do + local dataset = UTILS.Split(_entry,",") + -- groupname,template,units,position.x,position.y,position.z + local groupname = dataset[1] + local template = dataset[2] + local size = tonumber(dataset[3]) + local posx = tonumber(dataset[4]) + local posy = tonumber(dataset[5]) + local posz = tonumber(dataset[6]) + local coordinate = COORDINATE:NewFromVec3({x=posx, y=posy, z=posz}) + local group=nil + local data = { groupname=groupname, size=size, coordinate=coordinate } + table.insert(datatable,data) + if spawn then + local group = SPAWN:New(groupname) + :InitDelayOff() + :OnSpawnGroup( + function(spwndgrp) + setdata:AddObject(spwndgrp) + local actualsize = spwndgrp:CountAliveUnits() + if actualsize > size then + local reduction = actualsize-size + -- reduce existing group + local units = spwndgrp:GetUnits() + local units2 = UTILS.ShuffleTable(units) -- randomize table + for i=1,reduction do + units2[i]:Destroy(false) + end + end + end + ) + :SpawnFromCoordinate(coordinate) + end + end + else + return nil + end + if spawn then + return setdata + else + return datatable + end +end + +--- Load back a SET of statics from file. +-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. +-- @param #string Filename The name of the file. +-- @return Core.Set#SET_STATIC Set SET_STATIC containing the static objects. +function UTILS.LoadSetOfStatics(Path,Filename) + local filename = Filename or "SetOfStatics" + local datatable = SET_STATIC:New() + if UTILS.CheckFileExists(Path,filename) then + local outcome,loadeddata = UTILS.LoadFromFile(Path,Filename) + -- remove header + table.remove(loadeddata, 1) + for _id,_entry in pairs (loadeddata) do + local dataset = UTILS.Split(_entry,",") + -- staticname,position.x,position.y,position.z + local staticname = dataset[1] + local posx = tonumber(dataset[2]) + local posy = tonumber(dataset[3]) + local posz = tonumber(dataset[4]) + local coordinate = COORDINATE:NewFromVec3({x=posx, y=posy, z=posz}) + datatable:AddObject(STATIC:FindByName(staticname,false)) + end + else + return nil + end + return datatable +end + +--- Load back a stationary list of statics from file. +-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. +-- @param #string Filename The name of the file. +-- @param #boolean Reduce If false, do not destroy the units with size=0. +-- @return #table Table of data objects (tables) containing staticname, size (0=dead else 1), coordinate and the static object. +-- Returns nil when file cannot be read. +function UTILS.LoadStationaryListOfStatics(Path,Filename,Reduce) + local reduce = true + if Reduce == false then reduce = false end + local filename = Filename or "StateListofStatics" + local datatable = {} + if UTILS.CheckFileExists(Path,filename) then + local outcome,loadeddata = UTILS.LoadFromFile(Path,Filename) + -- remove header + table.remove(loadeddata, 1) + for _id,_entry in pairs (loadeddata) do + local dataset = UTILS.Split(_entry,",") + -- staticname,units(1/0),position.x,position.y,position.z) + local staticname = dataset[1] + local size = tonumber(dataset[2]) + local posx = tonumber(dataset[3]) + local posy = tonumber(dataset[4]) + local posz = tonumber(dataset[5]) + local coordinate = COORDINATE:NewFromVec3({x=posx, y=posy, z=posz}) + local data = { staticname=staticname, size=size, coordinate=coordinate, static=STATIC:FindByName(staticname,false) } + table.insert(datatable,data) + if size==0 and reduce then + local static = STATIC:FindByName(staticname,false) + if static then + static:Destroy(false) + end + end + end + else + return nil + end + return datatable +end + +--- Heading Degrees (0-360) to Cardinal +-- @param #number Heading The heading +-- @return #string Cardinal, e.g. "NORTH" +function UTILS.BearingToCardinal(Heading) + if Heading >= 0 and Heading <= 22 then return "North" + elseif Heading >= 23 and Heading <= 66 then return "North-East" + elseif Heading >= 67 and Heading <= 101 then return "East" + elseif Heading >= 102 and Heading <= 146 then return "South-East" + elseif Heading >= 147 and Heading <= 201 then return "South" + elseif Heading >= 202 and Heading <= 246 then return "South-West" + elseif Heading >= 247 and Heading <= 291 then return "West" + elseif Heading >= 292 and Heading <= 338 then return "North-West" + elseif Heading >= 339 then return "North" + end +end + +--- Create a BRAA NATO call string BRAA between two GROUP objects +-- @param Wrapper.Group#GROUP FromGrp GROUP object +-- @param Wrapper.Group#GROUP ToGrp GROUP object +-- @return #string Formatted BRAA NATO call +function UTILS.ToStringBRAANATO(FromGrp,ToGrp) + local BRAANATO = "Merged." + local GroupNumber = FromGrp:GetSize() + local GroupWords = "Singleton" + if GroupNumber == 2 then GroupWords = "Two-Ship" + elseif GroupNumber >= 3 then GroupWords = "Heavy" + end + local grpLeadUnit = ToGrp:GetUnit(1) + local tgtCoord = grpLeadUnit:GetCoordinate() + local currentCoord = FromGrp:GetCoordinate() + local hdg = UTILS.Round(ToGrp:GetHeading()/100,1)*100 + local bearing = UTILS.Round(currentCoord:HeadingTo(tgtCoord),0) + local rangeMetres = tgtCoord:Get2DDistance(currentCoord) + local rangeNM = UTILS.Round( UTILS.MetersToNM(rangeMetres), 0) + local aspect = tgtCoord:ToStringAspect(currentCoord) + local alt = UTILS.Round(UTILS.MetersToFeet(grpLeadUnit:GetAltitude())/1000,0)--*1000 + local track = UTILS.BearingToCardinal(hdg) + if rangeNM > 3 then + if aspect == "" then + BRAANATO = string.format("%s, BRA, %03d, %d miles, Angels %d, Track %s",GroupWords,bearing, rangeNM, alt, track) + else + BRAANATO = string.format("%s, BRAA, %03d, %d miles, Angels %d, %s, Track %s",GroupWords, bearing, rangeNM, alt, aspect, track) + end + end + return BRAANATO +end--- **Utils** - Lua Profiler. -- -- Find out how many times functions are called and how much real time it costs. -- @@ -5927,6 +6817,9 @@ end -- -- * (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min -- +-- @param #number length can also be passed as #string +-- @param #number speed Defaults to 1.0 +-- @param #boolean isGoogle We're using Google TTS function STTS.getSpeechTime(length,speed,isGoogle) local maxRateRatio = 3 @@ -5953,7 +6846,7 @@ function STTS.getSpeechTime(length,speed,isGoogle) length = string.len(length) end - return math.ceil(length/cps) + return length/cps --math.ceil(length/cps) end --- Text to speech function. @@ -6055,6 +6948,778 @@ function STTS.PlayMP3(pathToMP3, freqs, modulations, volume, name, coalition, po env.info("[DCS-STTS] MP3/OGG Command :\n" .. cmd.."\n") os.execute(cmd) +end--- **UTILS** - ClassicFiFo Stack. +-- +-- === +-- +-- ## Main Features: +-- +-- * Build a simple multi-purpose FiFo (First-In, First-Out) stack for generic data. +-- * [Wikipedia](https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics) +-- +-- === +-- +-- ### Author: **applevangelist** +-- @module Utils.FiFo +-- @image MOOSE.JPG + +-- Date: April 2022 + +do +--- FIFO class. +-- @type FIFO +-- @field #string ClassName Name of the class. +-- @field #string lid Class id string for output to DCS log file. +-- @field #string version Version of FiFo. +-- @field #number counter Counter. +-- @field #number pointer Pointer. +-- @field #table stackbypointer Stack by pointer. +-- @field #table stackbyid Stack by ID. +-- @extends Core.Base#BASE + +--- +-- @type FIFO.IDEntry +-- @field #number pointer +-- @field #table data +-- @field #table uniqueID + +--- +-- @field #FIFO +FIFO = { + ClassName = "FIFO", + lid = "", + version = "0.0.5", + counter = 0, + pointer = 0, + stackbypointer = {}, + stackbyid = {} +} + +--- Instantiate a new FIFO Stack. +-- @param #FIFO self +-- @return #FIFO self +function FIFO:New() + -- Inherit everything from BASE class. + local self=BASE:Inherit(self, BASE:New()) --#FIFO + self.pointer = 0 + self.counter = 0 + self.stackbypointer = {} + self.stackbyid = {} + self.uniquecounter = 0 + -- Set some string id for output to DCS.log file. + self.lid=string.format("%s (%s) | ", "FiFo", self.version) + self:T(self.lid .."Created.") + return self +end + +--- Empty FIFO Stack. +-- @param #FIFO self +-- @return #FIFO self +function FIFO:Clear() + self:T(self.lid.."Clear") + self.pointer = 0 + self.counter = 0 + self.stackbypointer = nil + self.stackbyid = nil + self.stackbypointer = {} + self.stackbyid = {} + self.uniquecounter = 0 + return self +end + +--- FIFO Push Object to Stack. +-- @param #FIFO self +-- @param #table Object +-- @param #string UniqueID (optional) - will default to current pointer + 1. Note - if you intend to use `FIFO:GetIDStackSorted()` keep the UniqueID numerical! +-- @return #FIFO self +function FIFO:Push(Object,UniqueID) + self:T(self.lid.."Push") + self:T({Object,UniqueID}) + self.pointer = self.pointer + 1 + self.counter = self.counter + 1 + local uniID = UniqueID + if not UniqueID then + self.uniquecounter = self.uniquecounter + 1 + uniID = self.uniquecounter + end + self.stackbyid[uniID] = { pointer = self.pointer, data = Object, uniqueID = uniID } + self.stackbypointer[self.pointer] = { pointer = self.pointer, data = Object, uniqueID = uniID } + return self +end + +--- FIFO Pull Object from Stack. +-- @param #FIFO self +-- @return #table Object or nil if stack is empty +function FIFO:Pull() + self:T(self.lid.."Pull") + if self.counter == 0 then return nil end + --local object = self.stackbypointer[self.pointer].data + --self.stackbypointer[self.pointer] = nil + local object = self.stackbypointer[1].data + self.stackbypointer[1] = nil + self.counter = self.counter - 1 + --self.pointer = self.pointer - 1 + self:Flatten() + return object +end + +--- FIFO Pull Object from Stack by Pointer +-- @param #FIFO self +-- @param #number Pointer +-- @return #table Object or nil if stack is empty +function FIFO:PullByPointer(Pointer) + self:T(self.lid.."PullByPointer " .. tostring(Pointer)) + if self.counter == 0 then return nil end + local object = self.stackbypointer[Pointer] -- #FIFO.IDEntry + self.stackbypointer[Pointer] = nil + if object then self.stackbyid[object.uniqueID] = nil end + self.counter = self.counter - 1 + self:Flatten() + if object then + return object.data + else + return nil + end +end + + +--- FIFO Read, not Pull, Object from Stack by Pointer +-- @param #FIFO self +-- @param #number Pointer +-- @return #table Object or nil if stack is empty or pointer does not exist +function FIFO:ReadByPointer(Pointer) + self:T(self.lid.."ReadByPointer " .. tostring(Pointer)) + if self.counter == 0 or not Pointer or not self.stackbypointer[Pointer] then return nil end + local object = self.stackbypointer[Pointer] -- #FIFO.IDEntry + if object then + return object.data + else + return nil + end +end + +--- FIFO Read, not Pull, Object from Stack by UniqueID +-- @param #FIFO self +-- @param #number UniqueID +-- @return #table Object data or nil if stack is empty or ID does not exist +function FIFO:ReadByID(UniqueID) + self:T(self.lid.."ReadByID " .. tostring(UniqueID)) + if self.counter == 0 or not UniqueID or not self.stackbyid[UniqueID] then return nil end + local object = self.stackbyid[UniqueID] -- #FIFO.IDEntry + if object then + return object.data + else + return nil + end +end + +--- FIFO Pull Object from Stack by UniqueID +-- @param #FIFO self +-- @param #tableUniqueID +-- @return #table Object or nil if stack is empty +function FIFO:PullByID(UniqueID) + self:T(self.lid.."PullByID " .. tostring(UniqueID)) + if self.counter == 0 then return nil end + local object = self.stackbyid[UniqueID] -- #FIFO.IDEntry + --self.stackbyid[UniqueID] = nil + if object then + return self:PullByPointer(object.pointer) + else + return nil + end +end + +--- FIFO Housekeeping +-- @param #FIFO self +-- @return #FIFO self +function FIFO:Flatten() + self:T(self.lid.."Flatten") + -- rebuild stacks + local pointerstack = {} + local idstack = {} + local counter = 0 + for _ID,_entry in pairs(self.stackbypointer) do + counter = counter + 1 + pointerstack[counter] = { pointer = counter, data = _entry.data, uniqueID = _entry.uniqueID} + end + for _ID,_entry in pairs(pointerstack) do + idstack[_entry.uniqueID] = { pointer = _entry.pointer , data = _entry.data, uniqueID = _entry.uniqueID} + end + self.stackbypointer = nil + self.stackbypointer = pointerstack + self.stackbyid = nil + self.stackbyid = idstack + self.counter = counter + self.pointer = counter + return self +end + +--- FIFO Check Stack is empty +-- @param #FIFO self +-- @return #boolean empty +function FIFO:IsEmpty() + self:T(self.lid.."IsEmpty") + return self.counter == 0 and true or false +end + +--- FIFO Get stack size +-- @param #FIFO self +-- @return #number size +function FIFO:GetSize() + self:T(self.lid.."GetSize") + return self.counter +end + +--- FIFO Get stack size +-- @param #FIFO self +-- @return #number size +function FIFO:Count() + self:T(self.lid.."Count") + return self.counter +end + +--- FIFO Check Stack is NOT empty +-- @param #FIFO self +-- @return #boolean notempty +function FIFO:IsNotEmpty() + self:T(self.lid.."IsNotEmpty") + return not self:IsEmpty() +end + +--- FIFO Get the data stack by pointer +-- @param #FIFO self +-- @return #table Table of #FIFO.IDEntry entries +function FIFO:GetPointerStack() + self:T(self.lid.."GetPointerStack") + return self.stackbypointer +end + +--- FIFO Check if a certain UniqeID exists +-- @param #FIFO self +-- @return #boolean exists +function FIFO:HasUniqueID(UniqueID) + self:T(self.lid.."HasUniqueID") + if self.stackbyid[UniqueID] ~= nil then + return true + else + return false + end +end + +--- FIFO Get the data stack by UniqueID +-- @param #FIFO self +-- @return #table Table of #FIFO.IDEntry entries +function FIFO:GetIDStack() + self:T(self.lid.."GetIDStack") + return self.stackbyid +end + +--- FIFO Get table of UniqueIDs sorted smallest to largest +-- @param #FIFO self +-- @return #table Table with index [1] to [n] of UniqueID entries +function FIFO:GetIDStackSorted() + self:T(self.lid.."GetIDStackSorted") + + local stack = self:GetIDStack() + local idstack = {} + for _id,_entry in pairs(stack) do + idstack[#idstack+1] = _id + + self:T({"pre",_id}) + end + + local function sortID(a, b) + return a < b + end + + table.sort(idstack) + + return idstack +end + +--- FIFO Get table of data entries +-- @param #FIFO self +-- @return #table Raw table indexed [1] to [n] of object entries - might be empty! +function FIFO:GetDataTable() + self:T(self.lid.."GetDataTable") + local datatable = {} + for _,_entry in pairs(self.stackbypointer) do + datatable[#datatable+1] = _entry.data + end + return datatable +end + +--- FIFO Get sorted table of data entries by UniqueIDs (must be numerical UniqueIDs only!) +-- @param #FIFO self +-- @return #table Table indexed [1] to [n] of sorted object entries - might be empty! +function FIFO:GetSortedDataTable() + self:T(self.lid.."GetSortedDataTable") + local datatable = {} + local idtablesorted = self:GetIDStackSorted() + for _,_entry in pairs(idtablesorted) do + datatable[#datatable+1] = self:ReadByID(_entry) + end + return datatable +end + +--- Iterate the FIFO and call an iterator function for the given FIFO data, providing the object for each element of the stack and optional parameters. +-- @param #FIFO self +-- @param #function IteratorFunction The function that will be called. +-- @param #table Arg (Optional) Further Arguments of the IteratorFunction. +-- @param #function Function (Optional) A function returning a #boolean true/false. Only if true, the IteratorFunction is called. +-- @param #table FunctionArguments (Optional) Function arguments. +-- @return #FIFO self +function FIFO:ForEach( IteratorFunction, Arg, Function, FunctionArguments ) + self:T(self.lid.."ForEach") + + local Set = self:GetPointerStack() or {} + Arg = Arg or {} + + local function CoRoutine() + local Count = 0 + for ObjectID, ObjectData in pairs( Set ) do + local Object = ObjectData.data + self:T( {Object} ) + if Function then + if Function( unpack( FunctionArguments or {} ), Object ) == true then + IteratorFunction( Object, unpack( Arg ) ) + end + else + IteratorFunction( Object, unpack( Arg ) ) + end + Count = Count + 1 + end + return true + end + + local co = CoRoutine + + local function Schedule() + + local status, res = co() + self:T( { status, res } ) + + if status == false then + error( res ) + end + if res == false then + return true -- resume next time the loop + end + + return false + end + + Schedule() + + return self +end + +--- FIFO Print stacks to dcs.log +-- @param #FIFO self +-- @return #FIFO self +function FIFO:Flush() + self:T(self.lid.."FiFo Flush") + self:I("FIFO Flushing Stack by Pointer") + for _id,_data in pairs (self.stackbypointer) do + local data = _data -- #FIFO.IDEntry + self:I(string.format("Pointer: %s | Entry: Number = %s Data = %s UniqueID = %s",tostring(_id),tostring(data.pointer),tostring(data.data),tostring(data.uniqueID))) + end + self:I("FIFO Flushing Stack by ID") + for _id,_data in pairs (self.stackbyid) do + local data = _data -- #FIFO.IDEntry + self:I(string.format("ID: %s | Entry: Number = %s Data = %s UniqueID = %s",tostring(_id),tostring(data.pointer),tostring(data.data),tostring(data.uniqueID))) + end + self:I("Counter = " .. self.counter) + self:I("Pointer = ".. self.pointer) + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- End FIFO +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- LIFO +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +do +--- **UTILS** - LiFo Stack. +-- +-- **Main Features:** +-- +-- * Build a simple multi-purpose LiFo (Last-In, First-Out) stack for generic data. +-- +-- === +-- +-- ### Author: **applevangelist** + +--- LIFO class. +-- @type LIFO +-- @field #string ClassName Name of the class. +-- @field #string lid Class id string for output to DCS log file. +-- @field #string version Version of LiFo +-- @field #number counter +-- @field #number pointer +-- @field #table stackbypointer +-- @field #table stackbyid +-- @extends Core.Base#BASE + +--- +-- @type LIFO.IDEntry +-- @field #number pointer +-- @field #table data +-- @field #table uniqueID + +--- +-- @field #LIFO +LIFO = { + ClassName = "LIFO", + lid = "", + version = "0.0.5", + counter = 0, + pointer = 0, + stackbypointer = {}, + stackbyid = {} +} + +--- Instantiate a new LIFO Stack +-- @param #LIFO self +-- @return #LIFO self +function LIFO:New() + -- Inherit everything from BASE class. + local self=BASE:Inherit(self, BASE:New()) + self.pointer = 0 + self.counter = 0 + self.uniquecounter = 0 + self.stackbypointer = {} + self.stackbyid = {} + -- Set some string id for output to DCS.log file. + self.lid=string.format("%s (%s) | ", "LiFo", self.version) + self:T(self.lid .."Created.") + return self +end + +--- Empty LIFO Stack +-- @param #LIFO self +-- @return #LIFO self +function LIFO:Clear() + self:T(self.lid.."Clear") + self.pointer = 0 + self.counter = 0 + self.stackbypointer = nil + self.stackbyid = nil + self.stackbypointer = {} + self.stackbyid = {} + self.uniquecounter = 0 + return self +end + +--- LIFO Push Object to Stack +-- @param #LIFO self +-- @param #table Object +-- @param #string UniqueID (optional) - will default to current pointer + 1 +-- @return #LIFO self +function LIFO:Push(Object,UniqueID) + self:T(self.lid.."Push") + self:T({Object,UniqueID}) + self.pointer = self.pointer + 1 + self.counter = self.counter + 1 + local uniID = UniqueID + if not UniqueID then + self.uniquecounter = self.uniquecounter + 1 + uniID = self.uniquecounter + end + self.stackbyid[uniID] = { pointer = self.pointer, data = Object, uniqueID = uniID } + self.stackbypointer[self.pointer] = { pointer = self.pointer, data = Object, uniqueID = uniID } + return self +end + +--- LIFO Pull Object from Stack +-- @param #LIFO self +-- @return #table Object or nil if stack is empty +function LIFO:Pull() + self:T(self.lid.."Pull") + if self.counter == 0 then return nil end + local object = self.stackbypointer[self.pointer].data + self.stackbypointer[self.pointer] = nil + --local object = self.stackbypointer[1].data + --self.stackbypointer[1] = nil + self.counter = self.counter - 1 + self.pointer = self.pointer - 1 + self:Flatten() + return object +end + +--- LIFO Pull Object from Stack by Pointer +-- @param #LIFO self +-- @param #number Pointer +-- @return #table Object or nil if stack is empty +function LIFO:PullByPointer(Pointer) + self:T(self.lid.."PullByPointer " .. tostring(Pointer)) + if self.counter == 0 then return nil end + local object = self.stackbypointer[Pointer] -- #FIFO.IDEntry + self.stackbypointer[Pointer] = nil + if object then self.stackbyid[object.uniqueID] = nil end + self.counter = self.counter - 1 + self:Flatten() + if object then + return object.data + else + return nil + end +end + +--- LIFO Read, not Pull, Object from Stack by Pointer +-- @param #LIFO self +-- @param #number Pointer +-- @return #table Object or nil if stack is empty or pointer does not exist +function LIFO:ReadByPointer(Pointer) + self:T(self.lid.."ReadByPointer " .. tostring(Pointer)) + if self.counter == 0 or not Pointer or not self.stackbypointer[Pointer] then return nil end + local object = self.stackbypointer[Pointer] -- #LIFO.IDEntry + if object then + return object.data + else + return nil + end +end + +--- LIFO Read, not Pull, Object from Stack by UniqueID +-- @param #LIFO self +-- @param #number UniqueID +-- @return #table Object or nil if stack is empty or ID does not exist +function LIFO:ReadByID(UniqueID) + self:T(self.lid.."ReadByID " .. tostring(UniqueID)) + if self.counter == 0 or not UniqueID or not self.stackbyid[UniqueID] then return nil end + local object = self.stackbyid[UniqueID] -- #LIFO.IDEntry + if object then + return object.data + else + return nil + end +end + +--- LIFO Pull Object from Stack by UniqueID +-- @param #LIFO self +-- @param #tableUniqueID +-- @return #table Object or nil if stack is empty +function LIFO:PullByID(UniqueID) + self:T(self.lid.."PullByID " .. tostring(UniqueID)) + if self.counter == 0 then return nil end + local object = self.stackbyid[UniqueID] -- #LIFO.IDEntry + --self.stackbyid[UniqueID] = nil + if object then + return self:PullByPointer(object.pointer) + else + return nil + end +end + +--- LIFO Housekeeping +-- @param #LIFO self +-- @return #LIFO self +function LIFO:Flatten() + self:T(self.lid.."Flatten") + -- rebuild stacks + local pointerstack = {} + local idstack = {} + local counter = 0 + for _ID,_entry in pairs(self.stackbypointer) do + counter = counter + 1 + pointerstack[counter] = { pointer = counter, data = _entry.data, uniqueID = _entry.uniqueID} + end + for _ID,_entry in pairs(pointerstack) do + idstack[_entry.uniqueID] = { pointer = _entry.pointer , data = _entry.data, uniqueID = _entry.uniqueID} + end + self.stackbypointer = nil + self.stackbypointer = pointerstack + self.stackbyid = nil + self.stackbyid = idstack + self.counter = counter + self.pointer = counter + return self +end + +--- LIFO Check Stack is empty +-- @param #LIFO self +-- @return #boolean empty +function LIFO:IsEmpty() + self:T(self.lid.."IsEmpty") + return self.counter == 0 and true or false +end + +--- LIFO Get stack size +-- @param #LIFO self +-- @return #number size +function LIFO:GetSize() + self:T(self.lid.."GetSize") + return self.counter +end + +--- LIFO Get stack size +-- @param #LIFO self +-- @return #number size +function LIFO:Count() + self:T(self.lid.."Count") + return self.counter +end + +--- LIFO Check Stack is NOT empty +-- @param #LIFO self +-- @return #boolean notempty +function LIFO:IsNotEmpty() + self:T(self.lid.."IsNotEmpty") + return not self:IsEmpty() +end + +--- LIFO Get the data stack by pointer +-- @param #LIFO self +-- @return #table Table of #LIFO.IDEntry entries +function LIFO:GetPointerStack() + self:T(self.lid.."GetPointerStack") + return self.stackbypointer +end + +--- LIFO Get the data stack by UniqueID +-- @param #LIFO self +-- @return #table Table of #LIFO.IDEntry entries +function LIFO:GetIDStack() + self:T(self.lid.."GetIDStack") + return self.stackbyid +end + +--- LIFO Get table of UniqueIDs sorted smallest to largest +-- @param #LIFO self +-- @return #table Table of #LIFO.IDEntry entries +function LIFO:GetIDStackSorted() + self:T(self.lid.."GetIDStackSorted") + + local stack = self:GetIDStack() + local idstack = {} + for _id,_entry in pairs(stack) do + idstack[#idstack+1] = _id + + self:T({"pre",_id}) + end + + local function sortID(a, b) + return a < b + end + + table.sort(idstack) + + return idstack +end + +--- LIFO Check if a certain UniqeID exists +-- @param #LIFO self +-- @return #boolean exists +function LIFO:HasUniqueID(UniqueID) + self:T(self.lid.."HasUniqueID") + return self.stackbyid[UniqueID] and true or false +end + +--- LIFO Print stacks to dcs.log +-- @param #LIFO self +-- @return #LIFO self +function LIFO:Flush() + self:T(self.lid.."FiFo Flush") + self:I("LIFO Flushing Stack by Pointer") + for _id,_data in pairs (self.stackbypointer) do + local data = _data -- #LIFO.IDEntry + self:I(string.format("Pointer: %s | Entry: Number = %s Data = %s UniqueID = %s",tostring(_id),tostring(data.pointer),tostring(data.data),tostring(data.uniqueID))) + end + self:I("LIFO Flushing Stack by ID") + for _id,_data in pairs (self.stackbyid) do + local data = _data -- #LIFO.IDEntry + self:I(string.format("ID: %s | Entry: Number = %s Data = %s UniqueID = %s",tostring(_id),tostring(data.pointer),tostring(data.data),tostring(data.uniqueID))) + end + self:I("Counter = " .. self.counter) + self:I("Pointer = ".. self.pointer) + return self +end + +--- LIFO Get table of data entries +-- @param #LIFO self +-- @return #table Raw table indexed [1] to [n] of object entries - might be empty! +function LIFO:GetDataTable() + self:T(self.lid.."GetDataTable") + local datatable = {} + for _,_entry in pairs(self.stackbypointer) do + datatable[#datatable+1] = _entry.data + end + return datatable +end + +--- LIFO Get sorted table of data entries by UniqueIDs (must be numerical UniqueIDs only!) +-- @param #LIFO self +-- @return #table Table indexed [1] to [n] of sorted object entries - might be empty! +function LIFO:GetSortedDataTable() + self:T(self.lid.."GetSortedDataTable") + local datatable = {} + local idtablesorted = self:GetIDStackSorted() + for _,_entry in pairs(idtablesorted) do + datatable[#datatable+1] = self:ReadByID(_entry) + end + return datatable +end + +--- Iterate the LIFO and call an iterator function for the given LIFO data, providing the object for each element of the stack and optional parameters. +-- @param #LIFO self +-- @param #function IteratorFunction The function that will be called. +-- @param #table Arg (Optional) Further Arguments of the IteratorFunction. +-- @param #function Function (Optional) A function returning a #boolean true/false. Only if true, the IteratorFunction is called. +-- @param #table FunctionArguments (Optional) Function arguments. +-- @return #LIFO self +function LIFO:ForEach( IteratorFunction, Arg, Function, FunctionArguments ) + self:T(self.lid.."ForEach") + + local Set = self:GetPointerStack() or {} + Arg = Arg or {} + + local function CoRoutine() + local Count = 0 + for ObjectID, ObjectData in pairs( Set ) do + local Object = ObjectData.data + self:T( {Object} ) + if Function then + if Function( unpack( FunctionArguments or {} ), Object ) == true then + IteratorFunction( Object, unpack( Arg ) ) + end + else + IteratorFunction( Object, unpack( Arg ) ) + end + Count = Count + 1 + end + return true + end + + local co = CoRoutine + + local function Schedule() + + local status, res = co() + self:T( { status, res } ) + + if status == false then + error( res ) + end + if res == false then + return true -- resume next time the loop + end + + return false + end + + Schedule() + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- End LIFO +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- end--- **Core** - The base class within the framework. -- -- === @@ -6291,7 +7956,8 @@ FORMATION = { -- @param #BASE self -- @return #BASE function BASE:New() - local self = routines.utils.deepCopy( self ) -- Create a new self instance + --local self = routines.utils.deepCopy( self ) -- Create a new self instance + local self = UTILS.DeepCopy(self) _ClassID = _ClassID + 1 self.ClassID = _ClassID @@ -6529,14 +8195,16 @@ do -- Event Handling return self end - -- Event handling function prototypes + -- Event handling function prototypes - Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. --- Occurs whenever any unit in a mission fires a weapon. But not any machine gun or autocannon based weapon, those are handled by EVENT.ShootingStart. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- @function [parent=#BASE] OnEventShot -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs whenever an object is hit by a weapon. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- initiator : The unit object the fired the weapon -- weapon: Weapon object that hit the target -- target: The Object that was hit. @@ -6545,6 +8213,7 @@ do -- Event Handling -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs when an aircraft takes off from an airbase, farp, or ship. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- initiator : The unit that tookoff -- place: Object from where the AI took-off from. Can be an Airbase Object, FARP, or Ships -- @function [parent=#BASE] OnEventTakeoff @@ -6552,6 +8221,7 @@ do -- Event Handling -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs when an aircraft lands at an airbase, farp or ship + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- initiator : The unit that has landed -- place: Object that the unit landed on. Can be an Airbase Object, FARP, or Ships -- @function [parent=#BASE] OnEventLand @@ -6559,101 +8229,118 @@ do -- Event Handling -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs when any aircraft crashes into the ground and is completely destroyed. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- initiator : The unit that has crashed -- @function [parent=#BASE] OnEventCrash -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs when a pilot ejects from an aircraft + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- initiator : The unit that has ejected -- @function [parent=#BASE] OnEventEjection -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs when an aircraft connects with a tanker and begins taking on fuel. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- initiator : The unit that is receiving fuel. -- @function [parent=#BASE] OnEventRefueling -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs when an object is dead. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- initiator : The unit that is dead. -- @function [parent=#BASE] OnEventDead -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. - --- Occurs when an object is completely destroyed. - -- initiator : The unit that is was destroyed. + --- Occurs when an Event for an object is triggered. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. + -- initiator : The unit that triggered the event. -- @function [parent=#BASE] OnEvent -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs when the pilot of an aircraft is killed. Can occur either if the player is alive and crashes or if a weapon kills the pilot without completely destroying the plane. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- initiator : The unit that the pilot has died in. -- @function [parent=#BASE] OnEventPilotDead -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs when a ground unit captures either an airbase or a farp. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- initiator : The unit that captured the base -- place: The airbase that was captured, can be a FARP or Airbase. When calling place:getCoalition() the faction will already be the new owning faction. -- @function [parent=#BASE] OnEventBaseCaptured -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. - --- Occurs when a mission starts + --- Occurs when a mission starts + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- @function [parent=#BASE] OnEventMissionStart -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs when a mission ends + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- @function [parent=#BASE] OnEventMissionEnd -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs when an aircraft is finished taking fuel. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- initiator : The unit that was receiving fuel. -- @function [parent=#BASE] OnEventRefuelingStop -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs when any object is spawned into the mission. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- initiator : The unit that was spawned -- @function [parent=#BASE] OnEventBirth -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs when any system fails on a human controlled aircraft. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- initiator : The unit that had the failure -- @function [parent=#BASE] OnEventHumanFailure -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs when any aircraft starts its engines. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- initiator : The unit that is starting its engines. -- @function [parent=#BASE] OnEventEngineStartup -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs when any aircraft shuts down its engines. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- initiator : The unit that is stopping its engines. -- @function [parent=#BASE] OnEventEngineShutdown -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. - --- Occurs when any player assumes direct control of a unit. + --- Occurs when any player assumes direct control of a unit. Note - not Mulitplayer safe. Use PlayerEnterAircraft. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- initiator : The unit that is being taken control of. -- @function [parent=#BASE] OnEventPlayerEnterUnit -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs when any player relieves control of a unit to the AI. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- initiator : The unit that the player left. -- @function [parent=#BASE] OnEventPlayerLeaveUnit -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs when any unit begins firing a weapon that has a high rate of fire. Most common with aircraft cannons (GAU-8), autocannons, and machine guns. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- initiator : The unit that is doing the shooting. -- target: The unit that is being targeted. -- @function [parent=#BASE] OnEventShootingStart @@ -6661,24 +8348,28 @@ do -- Event Handling -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs when any unit stops firing its weapon. Event will always correspond with a shooting start event. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- initiator : The unit that was doing the shooting. -- @function [parent=#BASE] OnEventShootingEnd -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs when a new mark was added. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- MarkID: ID of the mark. -- @function [parent=#BASE] OnEventMarkAdded -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs when a mark was removed. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- MarkID: ID of the mark. -- @function [parent=#BASE] OnEventMarkRemoved -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs when a mark text was changed. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- MarkID: ID of the mark. -- @function [parent=#BASE] OnEventMarkChange -- @param #BASE self @@ -6695,11 +8386,13 @@ do -- Event Handling --- Occurs when any modification to the "Score" as seen on the debrief menu would occur. -- There is no information on what values the score was changed to. Event is likely similar to player_comment in this regard. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- @function [parent=#BASE] OnEventScore -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs on the death of a unit. Contains more and different information. Similar to unit_lost it will occur for aircraft before the aircraft crash event occurs. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- -- * initiator: The unit that killed the target -- * target: Target Object @@ -6711,11 +8404,13 @@ do -- Event Handling --- Occurs when any modification to the "Score" as seen on the debrief menu would occur. -- There is no information on what values the score was changed to. Event is likely similar to player_comment in this regard. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- @function [parent=#BASE] OnEventScore -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs when the game thinks an object is destroyed. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- -- * initiator: The unit that is was destroyed. -- @@ -6724,6 +8419,7 @@ do -- Event Handling -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs shortly after the landing animation of an ejected pilot touching the ground and standing up. Event does not occur if the pilot lands in the water and sub combs to Davey Jones Locker. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- -- * initiator: Static object representing the ejected pilot. Place : Aircraft that the pilot ejected from. -- * place: may not return as a valid object if the aircraft has crashed into the ground and no longer exists. @@ -6734,37 +8430,44 @@ do -- Event Handling -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Paratrooper landing. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- @function [parent=#BASE] OnEventParatrooperLanding -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Discard chair after ejection. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- @function [parent=#BASE] OnEventDiscardChairAfterEjection -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Weapon add. Fires when entering a mission per pylon with the name of the weapon (double pylons not counted, infinite wep reload not counted. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- @function [parent=#BASE] OnEventParatrooperLanding -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Trigger zone. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- @function [parent=#BASE] OnEventTriggerZone -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Landing quality mark. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- @function [parent=#BASE] OnEventLandingQualityMark -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. --- BDA. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- @function [parent=#BASE] OnEventBDA -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs when a player enters a slot and takes control of an aircraft. + -- Have a look at the class @{Core.EVENT#EVENT} as these are just the prototypes. -- **NOTE**: This is a workaround of a long standing DCS bug with the PLAYER_ENTER_UNIT event. -- initiator : The unit that is being taken control of. -- @function [parent=#BASE] OnEventPlayerEnterAircraft @@ -6832,13 +8535,14 @@ end -- @param #BASE self -- @param DCS#Time EventTime The time stamp of the event. -- @param DCS#Object Initiator The initiating object of the event. -function BASE:CreateEventDead( EventTime, Initiator ) - self:F( { EventTime, Initiator } ) +function BASE:CreateEventDead( EventTime, Initiator, IniObjectCategory ) + self:F( { EventTime, Initiator, IniObjectCategory } ) local Event = { id = world.event.S_EVENT_DEAD, time = EventTime, initiator = Initiator, + IniObjectCategory = IniObjectCategory, } world.onEvent( Event ) @@ -6890,8 +8594,7 @@ end world.onEvent(Event) end - --- TODO: Complete DCS#Event structure. + --- The main event handling function... This function captures all events generated for the class. -- @param #BASE self -- @param DCS#Event event @@ -6930,20 +8633,22 @@ do -- Scheduling -- @param #number Start Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called. -- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments. -- @param #table ... Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }. - -- @return #number The ScheduleID of the planned schedule. + -- @return #string The Schedule ID of the planned schedule. function BASE:ScheduleOnce( Start, SchedulerFunction, ... ) - self:F2( { Start } ) - self:T3( { ... } ) + -- Object name. local ObjectName = "-" ObjectName = self.ClassName .. self.ClassID + -- Debug info. self:F3( { "ScheduleOnce: ", ObjectName, Start } ) if not self.Scheduler then self.Scheduler = SCHEDULER:New( self ) end + -- FF this was wrong! + --[[ local ScheduleID = _SCHEDULEDISPATCHER:AddSchedule( self, SchedulerFunction, @@ -6953,6 +8658,10 @@ do -- Scheduling nil, nil ) + ]] + + -- NOTE: MasterObject (first parameter) needs to be nil or it will be the first argument passed to the SchedulerFunction! + local ScheduleID = self.Scheduler:Schedule(nil, SchedulerFunction, {...}, Start) self._.Schedules[#self._.Schedules+1] = ScheduleID @@ -6967,7 +8676,7 @@ do -- Scheduling -- @param #number Stop Specifies the amount of seconds when the scheduler will be stopped. -- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments. -- @param #table ... Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }. - -- @return #number The ScheduleID of the planned schedule. + -- @return #string The Schedule ID of the planned schedule. function BASE:ScheduleRepeat( Start, Repeat, RandomizeFactor, Stop, SchedulerFunction, ... ) self:F2( { Start } ) self:T3( { ... } ) @@ -6981,8 +8690,9 @@ do -- Scheduling self.Scheduler = SCHEDULER:New( self ) end - local ScheduleID = self.Scheduler:Schedule( - self, + -- NOTE: MasterObject (first parameter) should(!) be nil as it will be the first argument passed to the SchedulerFunction! + local ScheduleID = self.Scheduler:Schedule( + nil, SchedulerFunction, { ... }, Start, @@ -6999,13 +8709,13 @@ do -- Scheduling --- Stops the Schedule. -- @param #BASE self - -- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments. - function BASE:ScheduleStop( SchedulerFunction ) - + -- @param #string SchedulerID (Optional) Scheduler ID to be stopped. If nil, all pending schedules are stopped. + function BASE:ScheduleStop( SchedulerID ) self:F3( { "ScheduleStop:" } ) - + if self.Scheduler then - _SCHEDULEDISPATCHER:Stop( self.Scheduler, self._.Schedules[SchedulerFunction] ) + --_SCHEDULEDISPATCHER:Stop( self.Scheduler, self._.Schedules[SchedulerFunction] ) + _SCHEDULEDISPATCHER:Stop(self.Scheduler, SchedulerID) end end @@ -7149,7 +8859,7 @@ end --- Set tracing for a class -- @param #BASE self --- @param #string Class +-- @param #string Class Class name. function BASE:TraceClass( Class ) _TraceClass[Class] = true _TraceClassMethod[Class] = {} @@ -7158,8 +8868,8 @@ end --- Set tracing for a specific method of class -- @param #BASE self --- @param #string Class --- @param #string Method +-- @param #string Class Class name. +-- @param #string Method Method. function BASE:TraceClassMethod( Class, Method ) if not _TraceClassMethod[Class] then _TraceClassMethod[Class] = {} @@ -7425,20 +9135,20 @@ end --- *In order for the light to shine so brightly, the darkness must be present.* -- Francis Bacon -- -- After attaching a @{#BEACON} to your @{Wrapper.Positionable#POSITIONABLE}, you need to select the right function to activate the kind of beacon you want. --- There are two types of BEACONs available : the AA TACAN Beacon and the general purpose Radio Beacon. +-- There are two types of BEACONs available : the (aircraft) TACAN Beacon and the general purpose Radio Beacon. -- Note that in both case, you can set an optional parameter : the `BeaconDuration`. This can be very usefull to simulate the battery time if your BEACON is -- attach to a cargo crate, for exemple. -- --- ## AA TACAN Beacon usage +-- ## Aircraft TACAN Beacon usage -- --- This beacon only works with airborne @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}. Use @{#BEACON:AATACAN}() to set the beacon parameters and start the beacon. --- Use @#BEACON:StopAATACAN}() to stop it. +-- This beacon only works with airborne @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}. Use @{#BEACON.ActivateTACAN}() to set the beacon parameters and start the beacon. +-- Use @{#BEACON.StopRadioBeacon}() to stop it. -- -- ## General Purpose Radio Beacon usage -- -- This beacon will work with any @{Wrapper.Positionable#POSITIONABLE}, but **it won't follow the @{Wrapper.Positionable#POSITIONABLE}** ! This means that you should only use it with --- @{Wrapper.Positionable#POSITIONABLE} that don't move, or move very slowly. Use @{#BEACON:RadioBeacon}() to set the beacon parameters and start the beacon. --- Use @{#BEACON:StopRadioBeacon}() to stop it. +-- @{Wrapper.Positionable#POSITIONABLE} that don't move, or move very slowly. Use @{#BEACON.RadioBeacon}() to set the beacon parameters and start the beacon. +-- Use @{#BEACON.StopRadioBeacon}() to stop it. -- -- @type BEACON -- @field #string ClassName Name of the class "BEACON". @@ -7579,6 +9289,8 @@ end function BEACON:ActivateTACAN(Channel, Mode, Message, Bearing, Duration) self:T({channel=Channel, mode=Mode, callsign=Message, bearing=Bearing, duration=Duration}) + Mode=Mode or "Y" + -- Get frequency. local Frequency=UTILS.TACANToFrequency(Channel, Mode) @@ -7596,11 +9308,16 @@ function BEACON:ActivateTACAN(Channel, Mode, Message, Bearing, Duration) -- Check if unit is an aircraft and set system accordingly. local AA=self.Positionable:IsAir() + + if AA then System=5 --NOTE: 5 is how you cat the correct tanker behaviour! --BEACON.System.TACAN_TANKER -- Check if "Y" mode is selected for aircraft. - if Mode~="Y" then - self:E({"WARNING: The POSITIONABLE you want to attach the AA Tacan Beacon is an aircraft: Mode should Y !The BEACON is not emitting.", self.Positionable}) + if Mode=="X" then + --self:E({"WARNING: The POSITIONABLE you want to attach the AA Tacan Beacon is an aircraft: Mode should Y!", self.Positionable}) + System=BEACON.System.TACAN_TANKER_X + else + System=BEACON.System.TACAN_TANKER_Y end end @@ -7647,7 +9364,34 @@ function BEACON:ActivateICLS(Channel, Callsign, Duration) return self end ---- Activates a TACAN BEACON on an Aircraft. +--- Activates a LINK4 BEACON. The unit the BEACON is attached to should be an aircraft carrier supporting this system. +-- @param #BEACON self +-- @param #number Frequency LINK4 FRequency in MHz, eg 336. +-- @param #string Morse The ID that is going to be coded in Morse and broadcasted by the beacon. +-- @param #number Duration How long will the beacon last in seconds. Omit for forever. +-- @return #BEACON self +function BEACON:ActivateLink4(Frequency, Morse, Duration) + self:F({Frequency=Frequency, Morse=Morse, Duration=Duration}) + + -- Attached unit. + local UnitID=self.Positionable:GetID() + + -- Debug + self:T2({"LINK4 BEACON started!"}) + + -- Start beacon. + self.Positionable:CommandActivateLink4(Frequency,UnitID,Morse) + + -- Stop sheduler + if Duration then -- Schedule the stop of the BEACON if asked by the MD + self.Positionable:CommandDeactivateLink4(Duration) + end + + return self +end + +--- DEPRECATED: Please use @{BEACON:ActivateTACAN}() instead. +-- Activates a TACAN BEACON on an Aircraft. -- @param #BEACON self -- @param #number TACANChannel (the "10" part in "10Y"). Note that AA TACAN are only available on Y Channels -- @param #string Message The Message that is going to be coded in Morse and broadcasted by the beacon @@ -7676,13 +9420,12 @@ function BEACON:AATACAN(TACANChannel, Message, Bearing, BeaconDuration) IsValid = false end - -- I'm using the beacon type 4 (BEACON_TYPE_TACAN). For System, I'm using 5 (TACAN_TANKER_MODE_Y) if the bearing shows its bearing - -- or 14 (TACAN_AA_MODE_Y) if it does not + -- I'm using the beacon type 4 (BEACON_TYPE_TACAN). For System, I'm using 5 (TACAN_TANKER_MODE_Y) if the bearing shows its bearing or 14 (TACAN_AA_MODE_Y) if it does not local System if Bearing then - System = 5 + System = BEACON.System.TACAN_TANKER_Y else - System = 14 + System = BEACON.System.TACAN_AA_MODE_Y end if IsValid then -- Starts the BEACON @@ -7690,10 +9433,13 @@ function BEACON:AATACAN(TACANChannel, Message, Bearing, BeaconDuration) self.Positionable:SetCommand({ id = "ActivateBeacon", params = { - type = 4, + type = BEACON.Type.TACAN, system = System, callsign = Message, + AA = true, frequency = Frequency, + bearing = Bearing, + modeChannel = "Y", } }) @@ -7725,7 +9471,7 @@ function BEACON:StopAATACAN() end ---- Activates a general pupose Radio Beacon +--- Activates a general purpose Radio Beacon -- This uses the very generic singleton function "trigger.action.radioTransmission()" provided by DCS to broadcast a sound file on a specific frequency. -- Although any frequency could be used, only 2 DCS Modules can home on radio beacons at the time of writing : the Huey and the Mi-8. -- They can home in on these specific frequencies : @@ -7802,7 +9548,7 @@ function BEACON:RadioBeacon(FileName, Frequency, Modulation, Power, BeaconDurati end end ---- Stops the AA TACAN BEACON +--- Stops the Radio Beacon -- @param #BEACON self -- @return #BEACON self function BEACON:StopRadioBeacon() @@ -7848,7 +9594,8 @@ function BEACON:_TACANToFrequency(TACANChannel, TACANMode) end return (A + TACANChannel - B) * 1000000 -end--- **Core** - Manage user flags to interact with the mission editor trigger system and server side scripts. +end +--- **Core** - Manage user flags to interact with the mission editor trigger system and server side scripts. -- -- === -- @@ -8080,7 +9827,7 @@ end -- -- # Demo Missions -- --- ### [SCHEDULER Demo Missions source code](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master-release/SCH%20-%20Scheduler) +-- ### [SCHEDULER Demo Missions source code](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/SCH%20-%20Scheduler) -- -- ### [SCHEDULER Demo Missions, only for beta testers](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/SCH%20-%20Scheduler) -- @@ -8304,7 +10051,7 @@ end -- @param #number Stop Time interval in seconds after which the scheduler will be stoppe. -- @param #number TraceLevel Trace level [0,3]. Default 3. -- @param Core.Fsm#FSM Fsm Finite state model. --- @return #table The ScheduleID of the planned schedule. +-- @return #string The Schedule ID of the planned schedule. function SCHEDULER:Schedule( MasterObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop, TraceLevel, Fsm ) self:F2( { Start, Repeat, RandomizeFactor, Stop } ) self:T3( { SchedulerArguments } ) @@ -8339,7 +10086,7 @@ end --- (Re-)Starts the schedules or a specific schedule if a valid ScheduleID is provided. -- @param #SCHEDULER self --- @param #string ScheduleID (Optional) The ScheduleID of the planned (repeating) schedule. +-- @param #string ScheduleID (Optional) The Schedule ID of the planned (repeating) schedule. function SCHEDULER:Start( ScheduleID ) self:F3( { ScheduleID } ) self:T(string.format("Starting scheduler ID=%s", tostring(ScheduleID))) @@ -8494,9 +10241,11 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr self.ObjectSchedulers = self.ObjectSchedulers or setmetatable( {}, { __mode = "v" } ) if Scheduler.MasterObject then + --env.info("FF Object Scheduler") self.ObjectSchedulers[CallID] = Scheduler self:F3( { CallID = CallID, ObjectScheduler = tostring(self.ObjectSchedulers[CallID]), MasterObject = tostring(Scheduler.MasterObject) } ) else + --env.info("FF Persistent Scheduler") self.PersistentSchedulers[CallID] = Scheduler self:F3( { CallID = CallID, PersistentScheduler = self.PersistentSchedulers[CallID] } ) end @@ -8604,6 +10353,7 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr if ShowTrace then SchedulerObject:T( Prefix .. Name .. ":" .. Line .. " (" .. Source .. ")" ) end + -- The master object is passed as first parameter. A few :Schedule() calls in MOOSE expect this currently. But in principle it should be removed. return ScheduleFunction( SchedulerObject, unpack( ScheduleArguments ) ) end Status, Result = xpcall( Timer, ErrorHandler ) @@ -8702,7 +10452,7 @@ end --- Stop dispatcher. -- @param #SCHEDULEDISPATCHER self -- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object. --- @param #table CallID Call ID. +-- @param #string CallID (Optional) Scheduler Call ID. If nil, all pending schedules are stopped recursively. function SCHEDULEDISPATCHER:Stop( Scheduler, CallID ) self:F2( { Stop = CallID, Scheduler = Scheduler } ) @@ -9745,13 +11495,12 @@ end -- @param #EVENTDATA Event Event data table. function EVENT:onEvent( Event ) + --- Function to handle errors. local ErrorHandler = function( errmsg ) - env.info( "Error in SCHEDULER function:" .. errmsg ) if BASE.Debug ~= nil then env.info( debug.traceback() ) end - return errmsg end @@ -9764,6 +11513,7 @@ function EVENT:onEvent( Event ) if self and self.Events and self.Events[Event.id] and self.MissionEnd==false and (Event.initiator~=nil or (Event.initiator==nil and Event.id~=EVENTS.PlayerLeaveUnit)) then + -- Check if mission has ended. if Event.id and Event.id == EVENTS.MissionEnd then self.MissionEnd = true end @@ -9771,35 +11521,12 @@ function EVENT:onEvent( Event ) if Event.initiator then Event.IniObjectCategory = Event.initiator:getCategory() - - if Event.IniObjectCategory == Object.Category.UNIT then - Event.IniDCSUnit = Event.initiator - Event.IniDCSUnitName = Event.IniDCSUnit:getName() - Event.IniUnitName = Event.IniDCSUnitName - Event.IniDCSGroup = Event.IniDCSUnit:getGroup() - Event.IniUnit = UNIT:FindByName( Event.IniDCSUnitName ) - if not Event.IniUnit then - -- Unit can be a CLIENT. Most likely this will be the case ... - Event.IniUnit = CLIENT:FindByName( Event.IniDCSUnitName, '', true ) - end - Event.IniDCSGroupName = "" - if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then - Event.IniDCSGroupName = Event.IniDCSGroup:getName() - Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) - --if Event.IniGroup then - Event.IniGroupName = Event.IniDCSGroupName - --end - end - Event.IniPlayerName = Event.IniDCSUnit:getPlayerName() - Event.IniCoalition = Event.IniDCSUnit:getCoalition() - Event.IniTypeName = Event.IniDCSUnit:getTypeName() - Event.IniCategory = Event.IniDCSUnit:getDesc().category - end - - if Event.IniObjectCategory == Object.Category.STATIC then - + + if Event.IniObjectCategory == Object.Category.STATIC then + --- + -- Static + --- if Event.id==31 then - -- Event.initiator is a Static object representing the pilot. But getName() errors due to DCS bug. Event.IniDCSUnit = Event.initiator local ID=Event.initiator.id_ @@ -9825,9 +11552,47 @@ function EVENT:onEvent( Event ) Event.IniCategory = Event.IniDCSUnit:getDesc().category Event.IniTypeName = Event.IniDCSUnit:getTypeName() end + + -- Dead events of units can be delayed and the initiator changed to a static. + -- Take care of that. + local Unit=UNIT:FindByName(Event.IniDCSUnitName) + if Unit then + Event.IniObjectCategory = Object.Category.UNIT + end + end + + if Event.IniObjectCategory == Object.Category.UNIT then + --- + -- Unit + --- + Event.IniDCSUnit = Event.initiator + Event.IniDCSUnitName = Event.IniDCSUnit:getName() + Event.IniUnitName = Event.IniDCSUnitName + Event.IniDCSGroup = Event.IniDCSUnit:getGroup() + Event.IniUnit = UNIT:FindByName( Event.IniDCSUnitName ) + + if not Event.IniUnit then + -- Unit can be a CLIENT. Most likely this will be the case ... + Event.IniUnit = CLIENT:FindByName( Event.IniDCSUnitName, '', true ) + end + + Event.IniDCSGroupName = Event.IniUnit and Event.IniUnit.GroupName or "" + if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then + Event.IniDCSGroupName = Event.IniDCSGroup:getName() + Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) + Event.IniGroupName = Event.IniDCSGroupName + end + + Event.IniPlayerName = Event.IniDCSUnit:getPlayerName() + Event.IniCoalition = Event.IniDCSUnit:getCoalition() + Event.IniTypeName = Event.IniDCSUnit:getTypeName() + Event.IniCategory = Event.IniDCSUnit:getDesc().category end if Event.IniObjectCategory == Object.Category.CARGO then + --- + -- Cargo + --- Event.IniDCSUnit = Event.initiator Event.IniDCSUnitName = Event.IniDCSUnit:getName() Event.IniUnitName = Event.IniDCSUnitName @@ -9838,15 +11603,22 @@ function EVENT:onEvent( Event ) end if Event.IniObjectCategory == Object.Category.SCENERY then + --- + -- Scenery + --- + Event.IniDCSUnit = Event.initiator Event.IniDCSUnitName = Event.IniDCSUnit:getName() Event.IniUnitName = Event.IniDCSUnitName Event.IniUnit = SCENERY:Register( Event.IniDCSUnitName, Event.initiator ) Event.IniCategory = Event.IniDCSUnit:getDesc().category - Event.IniTypeName = Event.initiator:isExist() and Event.IniDCSUnit:getTypeName() or "SCENERY" -- TODO: Bug fix for 2.1! + Event.IniTypeName = Event.initiator:isExist() and Event.IniDCSUnit:getTypeName() or "SCENERY" end if Event.IniObjectCategory == Object.Category.BASE then + --- + -- Base Object + --- Event.IniDCSUnit = Event.initiator Event.IniDCSUnitName = Event.IniDCSUnit:getName() Event.IniUnitName = Event.IniDCSUnitName @@ -9858,7 +11630,12 @@ function EVENT:onEvent( Event ) end if Event.target then + + --- + -- TARGET + --- + -- Target category. Event.TgtObjectCategory = Event.target:getCategory() if Event.TgtObjectCategory == Object.Category.UNIT then @@ -9871,9 +11648,7 @@ function EVENT:onEvent( Event ) if Event.TgtDCSGroup and Event.TgtDCSGroup:isExist() then Event.TgtDCSGroupName = Event.TgtDCSGroup:getName() Event.TgtGroup = GROUP:FindByName( Event.TgtDCSGroupName ) - --if Event.TgtGroup then - Event.TgtGroupName = Event.TgtDCSGroupName - --end + Event.TgtGroupName = Event.TgtDCSGroupName end Event.TgtPlayerName = Event.TgtDCSUnit:getPlayerName() Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() @@ -9921,6 +11696,7 @@ function EVENT:onEvent( Event ) end end + -- Weapon. if Event.weapon then Event.Weapon = Event.weapon Event.WeaponName = Event.Weapon:getTypeName() @@ -9955,23 +11731,22 @@ function EVENT:onEvent( Event ) Event.MarkGroupID = Event.groupID end + -- Cargo object. if Event.cargo then Event.Cargo = Event.cargo Event.CargoName = Event.cargo.Name end + -- Zone object. if Event.zone then Event.Zone = Event.zone Event.ZoneName = Event.zone.ZoneName end + -- Priority order. local PriorityOrder = EventMeta.Order local PriorityBegin = PriorityOrder == -1 and 5 or 1 - local PriorityEnd = PriorityOrder == -1 and 1 or 5 - - if Event.IniObjectCategory ~= Object.Category.STATIC then - self:F( { EventMeta.Text, Event, Event.IniDCSUnitName, Event.TgtDCSUnitName, PriorityOrder } ) - end + local PriorityEnd = PriorityOrder == -1 and 1 or 5 for EventPriority = PriorityBegin, PriorityEnd, PriorityOrder do @@ -9984,8 +11759,8 @@ function EVENT:onEvent( Event ) -- self:E( { "Evaluating: ", EventClass:GetClassNameAndID() } ) --end - Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) - Event.TgtGroup = GROUP:FindByName( Event.TgtDCSGroupName ) + Event.IniGroup = Event.IniGroup or GROUP:FindByName( Event.IniDCSGroupName ) + Event.TgtGroup = Event.TgtGroup or GROUP:FindByName( Event.TgtDCSGroupName ) -- If the EventData is for a UNIT, the call directly the EventClass EventFunction for that UNIT. if EventData.EventUnit then @@ -9995,20 +11770,17 @@ function EVENT:onEvent( Event ) Event.id == EVENTS.PlayerEnterUnit or Event.id == EVENTS.Crash or Event.id == EVENTS.Dead or - Event.id == EVENTS.RemoveUnit then + Event.id == EVENTS.RemoveUnit or + Event.id == EVENTS.UnitLost then local UnitName = EventClass:GetName() if ( EventMeta.Side == "I" and UnitName == Event.IniDCSUnitName ) or ( EventMeta.Side == "T" and UnitName == Event.TgtDCSUnitName ) then - + -- First test if a EventFunction is Set, otherwise search for the default function if EventData.EventFunction then - - if Event.IniObjectCategory ~= 3 then - self:F( { "Calling EventFunction for UNIT ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } ) - end - + local Result, Value = xpcall( function() return EventData.EventFunction( EventClass, Event ) @@ -10021,15 +11793,12 @@ function EVENT:onEvent( Event ) if EventFunction and type( EventFunction ) == "function" then -- Now call the default event function. - if Event.IniObjectCategory ~= 3 then - self:F( { "Calling " .. EventMeta.Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) - end - local Result, Value = xpcall( function() return EventFunction( EventClass, Event ) end, ErrorHandler ) end + end end else @@ -10047,7 +11816,8 @@ function EVENT:onEvent( Event ) Event.id == EVENTS.PlayerEnterUnit or Event.id == EVENTS.Crash or Event.id == EVENTS.Dead or - Event.id == EVENTS.RemoveUnit then + Event.id == EVENTS.RemoveUnit or + Event.id == EVENTS.UnitLost then -- We can get the name of the EventClass, which is now always a GROUP object. local GroupName = EventClass:GetName() @@ -10058,10 +11828,6 @@ function EVENT:onEvent( Event ) -- First test if a EventFunction is Set, otherwise search for the default function if EventData.EventFunction then - if Event.IniObjectCategory ~= 3 then - self:F( { "Calling EventFunction for GROUP ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } ) - end - local Result, Value = xpcall( function() return EventData.EventFunction( EventClass, Event, unpack( EventData.Params ) ) @@ -10074,10 +11840,6 @@ function EVENT:onEvent( Event ) if EventFunction and type( EventFunction ) == "function" then -- Now call the default event function. - if Event.IniObjectCategory ~= 3 then - self:F( { "Calling " .. EventMeta.Event .. " for GROUP ", EventClass:GetClassNameAndID(), EventPriority } ) - end - local Result, Value = xpcall( function() return EventFunction( EventClass, Event, unpack( EventData.Params ) ) @@ -10090,7 +11852,7 @@ function EVENT:onEvent( Event ) --self:RemoveEvent( EventClass, Event.id ) end else - + -- If the EventData is not bound to a specific unit, then call the EventClass EventFunction. -- Note that here the EventFunction will need to implement and determine the logic for the relevant source- or target unit, or weapon. if not EventData.EventUnit then @@ -10099,9 +11861,6 @@ function EVENT:onEvent( Event ) if EventData.EventFunction then -- There is an EventFunction defined, so call the EventFunction. - if Event.IniObjectCategory ~= 3 then - self:F2( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), EventPriority } ) - end local Result, Value = xpcall( function() return EventData.EventFunction( EventClass, Event ) @@ -10113,16 +11872,14 @@ function EVENT:onEvent( Event ) if EventFunction and type( EventFunction ) == "function" then -- Now call the default event function. - if Event.IniObjectCategory ~= 3 then - self:F2( { "Calling " .. EventMeta.Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) - end - local Result, Value = xpcall( function() local Result, Value = EventFunction( EventClass, Event ) return Result, Value end, ErrorHandler ) + end + end end @@ -10421,6 +12178,7 @@ do -- SETTINGS self:SetMessageTime( MESSAGE.Type.Overview, 60 ) self:SetMessageTime( MESSAGE.Type.Update, 15 ) self:SetEraModern() + self:SetLocale("en") return self else local Settings = _DATABASE:GetPlayerSettings( PlayerName ) @@ -10452,7 +12210,21 @@ do -- SETTINGS function SETTINGS:SetMetric() self.Metric = true end - + + --- Sets the SETTINGS default text locale. + -- @param #SETTINGS self + -- @param #string Locale + function SETTINGS:SetLocale(Locale) + self.Locale = Locale or "en" + end + + --- Gets the SETTINGS text locale. + -- @param #SETTINGS self + -- @return #string + function SETTINGS:GetLocale() + return self.Locale or _SETTINGS:GetLocale() + end + --- Gets if the SETTINGS is metric. -- @param #SETTINGS self -- @return #boolean true if metric. @@ -11263,7 +13035,7 @@ end -- * Only create or delete menus when required, and keep existing menus persistent. -- * Update menu structures. -- * Refresh menu structures intelligently, based on a time stamp of updates. --- - Delete obscolete menus. +-- - Delete obsolete menus. -- - Create new one where required. -- - Don't touch the existing ones. -- * Provide a variable amount of parameters to menus. @@ -11272,7 +13044,7 @@ end -- * Provide a great tool to manage menus in your code. -- -- DCS Menus can be managed using the MENU classes. --- The advantage of using MENU classes is that it hides the complexity of dealing with menu management in more advanced scanerios where you need to +-- The advantage of using MENU classes is that it hides the complexity of dealing with menu management in more advanced scenarios where you need to -- set menus and later remove them, and later set them again. You'll find while using use normal DCS scripting functions, that setting and removing -- menus is not a easy feat if you have complex menu hierarchies defined. -- Using the MOOSE menu classes, the removal and refreshing of menus are nicely being handled within these classes, and becomes much more easy. @@ -11302,7 +13074,6 @@ end -- @module Core.Menu -- @image Core_Menu.JPG - MENU_INDEX = {} MENU_INDEX.MenuMission = {} MENU_INDEX.MenuMission.Menus = {} @@ -11313,10 +13084,7 @@ MENU_INDEX.Coalition[coalition.side.RED] = {} MENU_INDEX.Coalition[coalition.side.RED].Menus = {} MENU_INDEX.Group = {} - - function MENU_INDEX:ParentPath( ParentMenu, MenuText ) - local Path = ParentMenu and "@" .. table.concat( ParentMenu.MenuPath or {}, "@" ) or "" if ParentMenu then if ParentMenu:IsInstanceOf( "MENU_GROUP" ) or ParentMenu:IsInstanceOf( "MENU_GROUP_COMMAND" ) then @@ -11344,20 +13112,16 @@ function MENU_INDEX:ParentPath( ParentMenu, MenuText ) Path = Path .. "@" .. MenuText return Path - end - function MENU_INDEX:PrepareMission() self.MenuMission.Menus = self.MenuMission.Menus or {} end - function MENU_INDEX:PrepareCoalition( CoalitionSide ) self.Coalition[CoalitionSide] = self.Coalition[CoalitionSide] or {} self.Coalition[CoalitionSide].Menus = self.Coalition[CoalitionSide].Menus or {} end - --- -- @param Wrapper.Group#GROUP Group function MENU_INDEX:PrepareGroup( Group ) @@ -11368,42 +13132,26 @@ function MENU_INDEX:PrepareGroup( Group ) end end - - function MENU_INDEX:HasMissionMenu( Path ) - return self.MenuMission.Menus[Path] end - function MENU_INDEX:SetMissionMenu( Path, Menu ) - self.MenuMission.Menus[Path] = Menu end - function MENU_INDEX:ClearMissionMenu( Path ) - self.MenuMission.Menus[Path] = nil end - - function MENU_INDEX:HasCoalitionMenu( Coalition, Path ) - return self.Coalition[Coalition].Menus[Path] end - function MENU_INDEX:SetCoalitionMenu( Coalition, Path, Menu ) - self.Coalition[Coalition].Menus[Path] = Menu end - function MENU_INDEX:ClearCoalitionMenu( Coalition, Path ) - self.Coalition[Coalition].Menus[Path] = nil end - - function MENU_INDEX:HasGroupMenu( Group, Path ) if Group and Group:IsAlive() then local MenuGroupName = Group:GetName() @@ -11411,53 +13159,36 @@ function MENU_INDEX:HasGroupMenu( Group, Path ) end return nil end - function MENU_INDEX:SetGroupMenu( Group, Path, Menu ) - local MenuGroupName = Group:GetName() Group:F({MenuGroupName=MenuGroupName,Path=Path}) self.Group[MenuGroupName].Menus[Path] = Menu end - function MENU_INDEX:ClearGroupMenu( Group, Path ) - local MenuGroupName = Group:GetName() self.Group[MenuGroupName].Menus[Path] = nil end - function MENU_INDEX:Refresh( Group ) - for MenuID, Menu in pairs( self.MenuMission.Menus ) do Menu:Refresh() end - for MenuID, Menu in pairs( self.Coalition[coalition.side.BLUE].Menus ) do Menu:Refresh() end - for MenuID, Menu in pairs( self.Coalition[coalition.side.RED].Menus ) do Menu:Refresh() end - local GroupName = Group:GetName() for MenuID, Menu in pairs( self.Group[GroupName].Menus ) do Menu:Refresh() end - + + return self end - - - - - - - do -- MENU_BASE - --- @type MENU_BASE - -- @extends Base#BASE - + -- @extends Core.Base#BASE --- Defines the main MENU class where other MENU classes are derived from. -- This is an abstract class, so don't use it. -- @field #MENU_BASE @@ -11465,10 +13196,10 @@ do -- MENU_BASE ClassName = "MENU_BASE", MenuPath = nil, MenuText = "", - MenuParentPath = nil + MenuParentPath = nil, } - --- Consructor + --- Constructor -- @param #MENU_BASE -- @return #MENU_BASE function MENU_BASE:New( MenuText, ParentMenu ) @@ -11477,27 +13208,25 @@ do -- MENU_BASE if ParentMenu ~= nil then MenuParentPath = ParentMenu.MenuPath end - - local self = BASE:Inherit( self, BASE:New() ) + local self = BASE:Inherit( self, BASE:New() ) - self.MenuPath = nil - self.MenuText = MenuText - self.ParentMenu = ParentMenu - self.MenuParentPath = MenuParentPath - self.Path = ( self.ParentMenu and "@" .. table.concat( self.MenuParentPath or {}, "@" ) or "" ) .. "@" .. self.MenuText + self.MenuPath = nil + self.MenuText = MenuText + self.ParentMenu = ParentMenu + self.MenuParentPath = MenuParentPath + self.Path = ( self.ParentMenu and "@" .. table.concat( self.MenuParentPath or {}, "@" ) or "" ) .. "@" .. self.MenuText self.Menus = {} self.MenuCount = 0 self.MenuStamp = timer.getTime() self.MenuRemoveParent = false - + if self.ParentMenu then self.ParentMenu.Menus = self.ParentMenu.Menus or {} self.ParentMenu.Menus[MenuText] = self end - - return self + + return self end - function MENU_BASE:SetParentMenu( MenuText, Menu ) if self.ParentMenu then self.ParentMenu.Menus = self.ParentMenu.Menus or {} @@ -11505,7 +13234,6 @@ do -- MENU_BASE self.ParentMenu.MenuCount = self.ParentMenu.MenuCount + 1 end end - function MENU_BASE:ClearParentMenu( MenuText ) if self.ParentMenu and self.ParentMenu.Menus[MenuText] then self.ParentMenu.Menus[MenuText] = nil @@ -11515,7 +13243,6 @@ do -- MENU_BASE end end end - --- Sets a @{Menu} to remove automatically the parent menu when the menu removed is the last child menu of that parent @{Menu}. -- @param #MENU_BASE self -- @param #boolean RemoveParent If true, the parent menu is automatically removed when this menu is the last child menu of that parent @{Menu}. @@ -11525,7 +13252,6 @@ do -- MENU_BASE self.MenuRemoveParent = RemoveParent return self end - --- Gets a @{Menu} from a parent @{Menu} -- @param #MENU_BASE self @@ -11534,7 +13260,7 @@ do -- MENU_BASE function MENU_BASE:GetMenu( MenuText ) return self.Menus[MenuText] end - + --- Sets a menu stamp for later prevention of menu removal. -- @param #MENU_BASE self -- @param MenuStamp @@ -11574,7 +13300,6 @@ do -- MENU_BASE end do -- MENU_COMMAND_BASE - --- @type MENU_COMMAND_BASE -- @field #function MenuCallHandler -- @extends Core.Menu#MENU_BASE @@ -11595,8 +13320,7 @@ do -- MENU_COMMAND_BASE -- @return #MENU_COMMAND_BASE function MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, CommandMenuArguments ) - local self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) -- #MENU_COMMAND_BASE - + local self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) -- #MENU_COMMAND_BASE -- When a menu function goes into error, DCS displays an obscure menu message. -- This error handler catches the menu error and displays the full call stack. local ErrorHandler = function( errmsg ) @@ -11616,7 +13340,7 @@ do -- MENU_COMMAND_BASE local Status, Result = xpcall( MenuFunction, ErrorHandler ) end - return self + return self end --- This sets the new command function of a menu, @@ -11629,7 +13353,6 @@ do -- MENU_COMMAND_BASE self.CommandMenuFunction = CommandMenuFunction return self end - --- This sets the new command arguments of a menu, -- so that if a menu is regenerated, or if command arguments change, -- that the arguments set for the menu are loosely coupled with the menu itself!!! @@ -11640,35 +13363,30 @@ do -- MENU_COMMAND_BASE self.CommandMenuArguments = CommandMenuArguments return self end - end - do -- MENU_MISSION - --- @type MENU_MISSION -- @extends Core.Menu#MENU_BASE - --- Manages the main menus for a complete mission. -- -- You can add menus with the @{#MENU_MISSION.New} method, which constructs a MENU_MISSION object and returns you the object reference. -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_MISSION.Remove}. -- @field #MENU_MISSION MENU_MISSION = { - ClassName = "MENU_MISSION" + ClassName = "MENU_MISSION", } --- MENU_MISSION constructor. Creates a new MENU_MISSION object and creates the menu for a complete mission file. -- @param #MENU_MISSION self -- @param #string MenuText The text for the menu. - -- @param #table ParentMenu The parent menu. This parameter can be ignored if you want the menu to be located at the perent menu of DCS world (under F10 other). + -- @param #table ParentMenu The parent menu. This parameter can be ignored if you want the menu to be located at the parent menu of DCS world (under F10 other). -- @return #MENU_MISSION function MENU_MISSION:New( MenuText, ParentMenu ) MENU_INDEX:PrepareMission() local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText ) local MissionMenu = MENU_INDEX:HasMissionMenu( Path ) - if MissionMenu then return MissionMenu else @@ -11681,17 +13399,15 @@ do -- MENU_MISSION end end - --- Refreshes a radio item for a mission -- @param #MENU_MISSION self -- @return #MENU_MISSION function MENU_MISSION:Refresh() - do missionCommands.removeItem( self.MenuPath ) self.MenuPath = missionCommands.addSubMenu( self.MenuText, self.MenuParentPath ) end - + return self end --- Removes the sub menus recursively of this MENU_MISSION. Note that the main menu is kept! @@ -11715,7 +13431,6 @@ do -- MENU_MISSION MENU_INDEX:PrepareMission() local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText ) local MissionMenu = MENU_INDEX:HasMissionMenu( Path ) - if MissionMenu == self then self:RemoveSubMenus() if not MenuStamp or self.MenuStamp ~= MenuStamp then @@ -11736,10 +13451,7 @@ do -- MENU_MISSION return self end - - end - do -- MENU_MISSION_COMMAND --- @type MENU_MISSION_COMMAND @@ -11752,7 +13464,7 @@ do -- MENU_MISSION_COMMAND -- -- @field #MENU_MISSION_COMMAND MENU_MISSION_COMMAND = { - ClassName = "MENU_MISSION_COMMAND" + ClassName = "MENU_MISSION_COMMAND", } --- MENU_MISSION constructor. Creates a new radio command item for a complete mission file, which can invoke a function with parameters. @@ -11767,7 +13479,6 @@ do -- MENU_MISSION_COMMAND MENU_INDEX:PrepareMission() local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText ) local MissionMenu = MENU_INDEX:HasMissionMenu( Path ) - if MissionMenu then MissionMenu:SetCommandMenuFunction( CommandMenuFunction ) MissionMenu:SetCommandMenuArguments( arg ) @@ -11781,17 +13492,15 @@ do -- MENU_MISSION_COMMAND return self end end - --- Refreshes a radio item for a mission -- @param #MENU_MISSION_COMMAND self -- @return #MENU_MISSION_COMMAND function MENU_MISSION_COMMAND:Refresh() - do missionCommands.removeItem( self.MenuPath ) missionCommands.addCommand( self.MenuText, self.MenuParentPath, self.MenuCallHandler ) end - + return self end --- Removes a radio command item for a coalition @@ -11802,7 +13511,6 @@ do -- MENU_MISSION_COMMAND MENU_INDEX:PrepareMission() local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText ) local MissionMenu = MENU_INDEX:HasMissionMenu( Path ) - if MissionMenu == self then if not MenuStamp or self.MenuStamp ~= MenuStamp then if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then @@ -11821,13 +13529,8 @@ do -- MENU_MISSION_COMMAND return self end - end - - - do -- MENU_COALITION - --- @type MENU_COALITION -- @extends Core.Menu#MENU_BASE @@ -11883,18 +13586,15 @@ do -- MENU_COALITION -- @param #MENU_COALITION self -- @param DCS#coalition.side Coalition The coalition owning the menu. -- @param #string MenuText The text for the menu. - -- @param #table ParentMenu The parent menu. This parameter can be ignored if you want the menu to be located at the perent menu of DCS world (under F10 other). + -- @param #table ParentMenu The parent menu. This parameter can be ignored if you want the menu to be located at the parent menu of DCS world (under F10 other). -- @return #MENU_COALITION self function MENU_COALITION:New( Coalition, MenuText, ParentMenu ) - MENU_INDEX:PrepareCoalition( Coalition ) local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText ) local CoalitionMenu = MENU_INDEX:HasCoalitionMenu( Coalition, Path ) - if CoalitionMenu then return CoalitionMenu else - local self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) MENU_INDEX:SetCoalitionMenu( Coalition, Path, self ) @@ -11905,17 +13605,15 @@ do -- MENU_COALITION return self end end - --- Refreshes a radio item for a coalition -- @param #MENU_COALITION self -- @return #MENU_COALITION function MENU_COALITION:Refresh() - do missionCommands.removeItemForCoalition( self.Coalition, self.MenuPath ) missionCommands.addSubMenuForCoalition( self.Coalition, self.MenuText, self.MenuParentPath ) end - + return self end --- Removes the sub menus recursively of this MENU_COALITION. Note that the main menu is kept! @@ -11938,7 +13636,6 @@ do -- MENU_COALITION MENU_INDEX:PrepareCoalition( self.Coalition ) local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText ) local CoalitionMenu = MENU_INDEX:HasCoalitionMenu( self.Coalition, Path ) - if CoalitionMenu == self then self:RemoveSubMenus() if not MenuStamp or self.MenuStamp ~= MenuStamp then @@ -11958,11 +13655,7 @@ do -- MENU_COALITION return self end - end - - - do -- MENU_COALITION_COMMAND --- @type MENU_COALITION_COMMAND @@ -11991,7 +13684,6 @@ do -- MENU_COALITION_COMMAND MENU_INDEX:PrepareCoalition( Coalition ) local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText ) local CoalitionMenu = MENU_INDEX:HasCoalitionMenu( Coalition, Path ) - if CoalitionMenu then CoalitionMenu:SetCommandMenuFunction( CommandMenuFunction ) CoalitionMenu:SetCommandMenuArguments( arg ) @@ -12006,20 +13698,17 @@ do -- MENU_COALITION_COMMAND self:SetParentMenu( self.MenuText, self ) return self end - end - - --- Refreshes a radio item for a coalition -- @param #MENU_COALITION_COMMAND self -- @return #MENU_COALITION_COMMAND function MENU_COALITION_COMMAND:Refresh() - do missionCommands.removeItemForCoalition( self.Coalition, self.MenuPath ) missionCommands.addCommandForCoalition( self.Coalition, self.MenuText, self.MenuParentPath, self.MenuCallHandler ) end - + + return self end --- Removes a radio command item for a coalition @@ -12030,7 +13719,6 @@ do -- MENU_COALITION_COMMAND MENU_INDEX:PrepareCoalition( self.Coalition ) local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText ) local CoalitionMenu = MENU_INDEX:HasCoalitionMenu( self.Coalition, Path ) - if CoalitionMenu == self then if not MenuStamp or self.MenuStamp ~= MenuStamp then if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then @@ -12049,20 +13737,16 @@ do -- MENU_COALITION_COMMAND return self end - end - --- MENU_GROUP - do -- This local variable is used to cache the menus registered under groups. - -- Menus don't dissapear when groups for players are destroyed and restarted. + -- Menus don't disappear when groups for players are destroyed and restarted. -- So every menu for a client created must be tracked so that program logic accidentally does not create. -- the same menus twice during initialization logic. -- These menu classes are handling this logic with this variable. local _MENUGROUPS = {} - --- @type MENU_GROUP -- @extends Core.Menu#MENU_BASE @@ -12138,16 +13822,13 @@ do MENU_INDEX:PrepareGroup( Group ) local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText ) local GroupMenu = MENU_INDEX:HasGroupMenu( Group, Path ) - if GroupMenu then return GroupMenu else self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) MENU_INDEX:SetGroupMenu( Group, Path, self ) - self.Group = Group self.GroupID = Group:GetID() - self.MenuPath = missionCommands.addSubMenuForGroup( self.GroupID, MenuText, self.MenuParentPath ) self:SetParentMenu( self.MenuText, self ) @@ -12155,12 +13836,11 @@ do end end - + --- Refreshes a new radio item for a group and submenus -- @param #MENU_GROUP self -- @return #MENU_GROUP function MENU_GROUP:Refresh() - do missionCommands.removeItemForGroup( self.GroupID, self.MenuPath ) missionCommands.addSubMenuForGroup( self.GroupID, self.MenuText, self.MenuParentPath ) @@ -12169,7 +13849,8 @@ do Menu:Refresh() end end - + + return self end --- Removes the sub menus recursively of this MENU_GROUP. @@ -12178,7 +13859,6 @@ do -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set. -- @return #MENU_GROUP self function MENU_GROUP:RemoveSubMenus( MenuStamp, MenuTag ) - for MenuText, Menu in pairs( self.Menus or {} ) do Menu:Remove( MenuStamp, MenuTag ) end @@ -12187,18 +13867,15 @@ do end - --- Removes the main menu and sub menus recursively of this MENU_GROUP. -- @param #MENU_GROUP self -- @param MenuStamp -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set. -- @return #nil function MENU_GROUP:Remove( MenuStamp, MenuTag ) - MENU_INDEX:PrepareGroup( self.Group ) local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText ) local GroupMenu = MENU_INDEX:HasGroupMenu( self.Group, Path ) - if GroupMenu == self then self:RemoveSubMenus( MenuStamp, MenuTag ) if not MenuStamp or self.MenuStamp ~= MenuStamp then @@ -12242,18 +13919,15 @@ do -- @param CommandMenuArgument An argument for the function. -- @return #MENU_GROUP_COMMAND function MENU_GROUP_COMMAND:New( Group, MenuText, ParentMenu, CommandMenuFunction, ... ) - MENU_INDEX:PrepareGroup( Group ) local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText ) local GroupMenu = MENU_INDEX:HasGroupMenu( Group, Path ) - if GroupMenu then GroupMenu:SetCommandMenuFunction( CommandMenuFunction ) GroupMenu:SetCommandMenuArguments( arg ) return GroupMenu else self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, arg ) ) - MENU_INDEX:SetGroupMenu( Group, Path, self ) self.Group = Group @@ -12264,19 +13938,17 @@ do self:SetParentMenu( self.MenuText, self ) return self end - end - --- Refreshes a radio item for a group -- @param #MENU_GROUP_COMMAND self -- @return #MENU_GROUP_COMMAND function MENU_GROUP_COMMAND:Refresh() - do missionCommands.removeItemForGroup( self.GroupID, self.MenuPath ) missionCommands.addCommandForGroup( self.GroupID, self.MenuText, self.MenuParentPath, self.MenuCallHandler ) end - + + return self end --- Removes a menu structure for a group. @@ -12285,11 +13957,9 @@ do -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set. -- @return #nil function MENU_GROUP_COMMAND:Remove( MenuStamp, MenuTag ) - MENU_INDEX:PrepareGroup( self.Group ) local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText ) local GroupMenu = MENU_INDEX:HasGroupMenu( self.Group, Path ) - if GroupMenu == self then if not MenuStamp or self.MenuStamp ~= MenuStamp then if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then @@ -12308,13 +13978,9 @@ do return self end - end - --- MENU_GROUP_DELAYED - do - --- @type MENU_GROUP_DELAYED -- @extends Core.Menu#MENU_BASE @@ -12342,16 +14008,13 @@ do MENU_INDEX:PrepareGroup( Group ) local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText ) local GroupMenu = MENU_INDEX:HasGroupMenu( Group, Path ) - if GroupMenu then return GroupMenu else self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) MENU_INDEX:SetGroupMenu( Group, Path, self ) - self.Group = Group self.GroupID = Group:GetID() - if self.MenuParentPath then self.MenuPath = UTILS.DeepCopy( self.MenuParentPath ) else @@ -12365,12 +14028,10 @@ do end - --- Refreshes a new radio item for a group and submenus -- @param #MENU_GROUP_DELAYED self -- @return #MENU_GROUP_DELAYED function MENU_GROUP_DELAYED:Set() - do if not self.MenuSet then missionCommands.addSubMenuForGroup( self.GroupID, self.MenuText, self.MenuParentPath ) @@ -12381,15 +14042,12 @@ do Menu:Set() end end - end - --- Refreshes a new radio item for a group and submenus -- @param #MENU_GROUP_DELAYED self -- @return #MENU_GROUP_DELAYED function MENU_GROUP_DELAYED:Refresh() - do missionCommands.removeItemForGroup( self.GroupID, self.MenuPath ) missionCommands.addSubMenuForGroup( self.GroupID, self.MenuText, self.MenuParentPath ) @@ -12398,7 +14056,8 @@ do Menu:Refresh() end end - + + return self end --- Removes the sub menus recursively of this MENU_GROUP_DELAYED. @@ -12407,7 +14066,6 @@ do -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set. -- @return #MENU_GROUP_DELAYED self function MENU_GROUP_DELAYED:RemoveSubMenus( MenuStamp, MenuTag ) - for MenuText, Menu in pairs( self.Menus or {} ) do Menu:Remove( MenuStamp, MenuTag ) end @@ -12416,18 +14074,15 @@ do end - --- Removes the main menu and sub menus recursively of this MENU_GROUP. -- @param #MENU_GROUP_DELAYED self -- @param MenuStamp -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set. -- @return #nil function MENU_GROUP_DELAYED:Remove( MenuStamp, MenuTag ) - MENU_INDEX:PrepareGroup( self.Group ) local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText ) local GroupMenu = MENU_INDEX:HasGroupMenu( self.Group, Path ) - if GroupMenu == self then self:RemoveSubMenus( MenuStamp, MenuTag ) if not MenuStamp or self.MenuStamp ~= MenuStamp then @@ -12472,18 +14127,15 @@ do -- @param CommandMenuArgument An argument for the function. -- @return #MENU_GROUP_COMMAND_DELAYED function MENU_GROUP_COMMAND_DELAYED:New( Group, MenuText, ParentMenu, CommandMenuFunction, ... ) - MENU_INDEX:PrepareGroup( Group ) local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText ) local GroupMenu = MENU_INDEX:HasGroupMenu( Group, Path ) - if GroupMenu then GroupMenu:SetCommandMenuFunction( CommandMenuFunction ) GroupMenu:SetCommandMenuArguments( arg ) return GroupMenu else self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, arg ) ) - MENU_INDEX:SetGroupMenu( Group, Path, self ) self.Group = Group @@ -12499,33 +14151,29 @@ do self:SetParentMenu( self.MenuText, self ) return self end - end - --- Refreshes a radio item for a group -- @param #MENU_GROUP_COMMAND_DELAYED self -- @return #MENU_GROUP_COMMAND_DELAYED function MENU_GROUP_COMMAND_DELAYED:Set() - do if not self.MenuSet then self.MenuPath = missionCommands.addCommandForGroup( self.GroupID, self.MenuText, self.MenuParentPath, self.MenuCallHandler ) self.MenuSet = true end end - end --- Refreshes a radio item for a group -- @param #MENU_GROUP_COMMAND_DELAYED self -- @return #MENU_GROUP_COMMAND_DELAYED function MENU_GROUP_COMMAND_DELAYED:Refresh() - do missionCommands.removeItemForGroup( self.GroupID, self.MenuPath ) missionCommands.addCommandForGroup( self.GroupID, self.MenuText, self.MenuParentPath, self.MenuCallHandler ) end - + + return self end --- Removes a menu structure for a group. @@ -12534,11 +14182,9 @@ do -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set. -- @return #nil function MENU_GROUP_COMMAND_DELAYED:Remove( MenuStamp, MenuTag ) - MENU_INDEX:PrepareGroup( self.Group ) local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText ) local GroupMenu = MENU_INDEX:HasGroupMenu( self.Group, Path ) - if GroupMenu == self then if not MenuStamp or self.MenuStamp ~= MenuStamp then if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then @@ -12557,9 +14203,7 @@ do return self end - end - --- **Core** - Define zones within your mission of various forms, with various capabilities. -- -- === @@ -12571,7 +14215,7 @@ end -- * Create polygon zones. -- * Create moving zones around a unit. -- * Create moving zones around a group. --- * Provide the zone behaviour. Some zones are static, while others are moveable. +-- * Provide the zone behavior. Some zones are static, while others are moveable. -- * Enquiry if a coordinate is within a zone. -- * Smoke zones. -- * Set a zone probability to control zone selection. @@ -12582,10 +14226,10 @@ end -- * Draw zones (circular and polygon) on the F10 map. -- -- --- There are essentially two core functions that zones accomodate: +-- There are essentially two core functions that zones accommodate: -- -- * Test if an object is within the zone boundaries. --- * Provide the zone behaviour. Some zones are static, while others are moveable. +-- * Provide the zone behavior. Some zones are static, while others are moveable. -- -- The object classes are using the zone classes to test the zone boundaries, which can take various forms: -- @@ -12621,6 +14265,10 @@ end -- @field #number ZoneProbability A value between 0 and 1. 0 = 0% and 1 = 100% probability. -- @field #number DrawID Unique ID of the drawn zone on the F10 map. -- @field #table Color Table with four entries, e.g. {1, 0, 0, 0.15}. First three are RGB color code. Fourth is the transparency Alpha value. +-- @field #table FillColor Table with four entries, e.g. {1, 0, 0, 0.15}. First three are RGB color code. Fourth is the transparency Alpha value. +-- @field #number drawCoalition Draw coalition. +-- @field #number ZoneID ID of zone. Only zones defined in the ME have an ID! +-- @field #number Surface Type of surface. Only determined at the center of the zone! -- @extends Core.Fsm#FSM @@ -12670,7 +14318,9 @@ ZONE_BASE = { ZoneName = "", ZoneProbability = 1, DrawID=nil, - Color={} + Color={}, + ZoneID=nil, + Sureface=nil, } @@ -12691,7 +14341,9 @@ function ZONE_BASE:New( ZoneName ) self:F( ZoneName ) self.ZoneName = ZoneName - + + --_DATABASE:AddZone(ZoneName,self) + return self end @@ -12732,6 +14384,7 @@ end -- @param DCS#Vec3 Vec3 The point to test. -- @return #boolean true if the Vec3 is within the zone. function ZONE_BASE:IsVec3InZone( Vec3 ) + if not Vec3 then return false end local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } ) return InZone end @@ -12850,6 +14503,23 @@ function ZONE_BASE:GetCoordinate( Height ) --R2.1 return self.Coordinate end +--- Get 2D distance to a coordinate. +-- @param #ZONE_BASE self +-- @param Core.Point#COORDINATE Coordinate Reference coordinate. Can also be a DCS#Vec2 or DCS#Vec3 object. +-- @return #number Distance to the reference coordinate in meters. +function ZONE_BASE:Get2DDistance(Coordinate) + local a=self:GetVec2() + local b={} + if Coordinate.z then + b.x=Coordinate.x + b.y=Coordinate.z + else + b.x=Coordinate.x + b.y=Coordinate.y + end + local dist=UTILS.VecDist2D(a,b) + return dist +end --- Define a random @{DCS#Vec2} within the zone. -- @param #ZONE_BASE self @@ -12876,18 +14546,41 @@ end -- @param #ZONE_BASE self -- @return #nil The bounding square. function ZONE_BASE:GetBoundingSquare() - --return { x1 = 0, y1 = 0, x2 = 0, y2 = 0 } return nil end +--- Get surface type of the zone. +-- @param #ZONE_BASE self +-- @return DCS#SurfaceType Type of surface. +function ZONE_BASE:GetSurfaceType() + local coord=self:GetCoordinate() + local surface=coord:GetSurfaceType() + return surface +end + --- Bound the zone boundaries with a tires. -- @param #ZONE_BASE self function ZONE_BASE:BoundZone() self:F2() - end +--- Set draw coalition of zone. +-- @param #ZONE_BASE self +-- @param #number Coalition Coalition. Default -1. +-- @return #ZONE_BASE self +function ZONE_BASE:SetDrawCoalition(Coalition) + self.drawCoalition=Coalition or -1 + return self +end + +--- Get draw coalition of zone. +-- @param #ZONE_BASE self +-- @return #number Draw coaliton. +function ZONE_BASE:GetDrawCoalition() + return self.drawCoalition or -1 +end + --- Set color of zone. -- @param #ZONE_BASE self -- @param #table RGBcolor RGB color table. Default `{1, 0, 0}`. @@ -12911,7 +14604,7 @@ end -- @param #ZONE_BASE self -- @return #table Table with four entries, e.g. {1, 0, 0, 0.15}. First three are RGB color code. Fourth is the transparency Alpha value. function ZONE_BASE:GetColor() - return self.Color + return self.Color or {1, 0, 0, 0.15} end --- Get RGB color of zone. @@ -12919,9 +14612,10 @@ end -- @return #table Table with three entries, e.g. {1, 0, 0}, which is the RGB color code. function ZONE_BASE:GetColorRGB() local rgb={} - rgb[1]=self.Color[1] - rgb[2]=self.Color[2] - rgb[3]=self.Color[3] + local Color=self:GetColor() + rgb[1]=Color[1] + rgb[2]=Color[2] + rgb[3]=Color[3] return rgb end @@ -12929,7 +14623,55 @@ end -- @param #ZONE_BASE self -- @return #number Alpha value. function ZONE_BASE:GetColorAlpha() - local alpha=self.Color[4] + local Color=self:GetColor() + local alpha=Color[4] + return alpha +end + +--- Set fill color of zone. +-- @param #ZONE_BASE self +-- @param #table RGBcolor RGB color table. Default `{1, 0, 0}`. +-- @param #number Alpha Transparacy between 0 and 1. Default 0.15. +-- @return #ZONE_BASE self +function ZONE_BASE:SetFillColor(RGBcolor, Alpha) + + RGBcolor=RGBcolor or {1, 0, 0} + Alpha=Alpha or 0.15 + + self.FillColor={} + self.FillColor[1]=RGBcolor[1] + self.FillColor[2]=RGBcolor[2] + self.FillColor[3]=RGBcolor[3] + self.FillColor[4]=Alpha + + return self +end + +--- Get fill color table of the zone. +-- @param #ZONE_BASE self +-- @return #table Table with four entries, e.g. {1, 0, 0, 0.15}. First three are RGB color code. Fourth is the transparency Alpha value. +function ZONE_BASE:GetFillColor() + return self.FillColor or {1, 0, 0, 0.15} +end + +--- Get RGB fill color of zone. +-- @param #ZONE_BASE self +-- @return #table Table with three entries, e.g. {1, 0, 0}, which is the RGB color code. +function ZONE_BASE:GetFillColorRGB() + local rgb={} + local FillColor=self:GetFillColor() + rgb[1]=FillColor[1] + rgb[2]=FillColor[2] + rgb[3]=FillColor[3] + return rgb +end + +--- Get transperency Alpha fill value of zone. +-- @param #ZONE_BASE self +-- @return #number Alpha value. +function ZONE_BASE:GetFillColorAlpha() + local FillColor=self:GetFillColor() + local alpha=FillColor[4] return alpha end @@ -13163,7 +14905,7 @@ function ZONE_RADIUS:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, Lin Color=Color or self:GetColorRGB() Alpha=Alpha or 1 - FillColor=FillColor or Color + FillColor=FillColor or UTILS.DeepCopy(Color) FillAlpha=FillAlpha or self:GetColorAlpha() self.DrawID=coordinate:CircleToAll(Radius, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) @@ -13351,7 +15093,7 @@ end --- Scan the zone for the presence of units of the given ObjectCategories. --- Note that after a zone has been scanned, the zone can be evaluated by: +-- Note that **only after** a zone has been scanned, the zone can be evaluated by: -- -- * @{ZONE_RADIUS.IsAllInZoneOfCoalition}(): Scan the presence of units in the zone of a coalition. -- * @{ZONE_RADIUS.IsAllInZoneOfOtherCoalition}(): Scan the presence of units in the zone of an other coalition. @@ -13360,10 +15102,10 @@ end -- * @{ZONE_RADIUS.IsNoneInZone}(): Scan if the zone is empty. -- @{#ZONE_RADIUS. -- @param #ZONE_RADIUS self --- @param ObjectCategories An array of categories of the objects to find in the zone. --- @param UnitCategories An array of unit categories of the objects to find in the zone. +-- @param ObjectCategories An array of categories of the objects to find in the zone. E.g. `{Object.Category.UNIT}` +-- @param UnitCategories An array of unit categories of the objects to find in the zone. E.g. `{Unit.Category.GROUND_UNIT,Unit.Category.SHIP}` -- @usage --- self.Zone:Scan() +-- self.Zone:Scan({Object.Category.UNIT},{Unit.Category.GROUND_UNIT}) -- local IsAttacked = self.Zone:IsSomeInZoneOfCoalition( self.Coalition ) function ZONE_RADIUS:Scan( ObjectCategories, UnitCategories ) @@ -13697,7 +15439,9 @@ end -- @return #boolean true if the location is within the zone. function ZONE_RADIUS:IsVec2InZone( Vec2 ) self:F2( Vec2 ) - + + if not Vec2 then return false end + local ZoneVec2 = self:GetVec2() if ZoneVec2 then @@ -13715,7 +15459,7 @@ end -- @return #boolean true if the point is within the zone. function ZONE_RADIUS:IsVec3InZone( Vec3 ) self:F2( Vec3 ) - + if not Vec3 then return false end local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } ) return InZone @@ -13723,24 +15467,54 @@ end --- Returns a random Vec2 location within the zone. -- @param #ZONE_RADIUS self --- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. --- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. +-- @param #number inner (Optional) Minimal distance from the center of the zone. Default is 0. +-- @param #number outer (Optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. +-- @param #table surfacetypes (Optional) Table of surface types. Can also be a single surface type. We will try max 100 times to find the right type! -- @return DCS#Vec2 The random location within the zone. -function ZONE_RADIUS:GetRandomVec2( inner, outer ) - self:F( self.ZoneName, inner, outer ) +function ZONE_RADIUS:GetRandomVec2(inner, outer, surfacetypes) - local Point = {} - local Vec2 = self:GetVec2() - local _inner = inner or 0 - local _outer = outer or self:GetRadius() + local Vec2 = self:GetVec2() + local _inner = inner or 0 + local _outer = outer or self:GetRadius() - local angle = math.random() * math.pi * 2; - Point.x = Vec2.x + math.cos( angle ) * math.random(_inner, _outer); - Point.y = Vec2.y + math.sin( angle ) * math.random(_inner, _outer); + if surfacetypes and type(surfacetypes)~="table" then + surfacetypes={surfacetypes} + end - self:T( { Point } ) + local function _getpoint() + local point = {} + local angle = math.random() * math.pi * 2 + point.x = Vec2.x + math.cos(angle) * math.random(_inner, _outer) + point.y = Vec2.y + math.sin(angle) * math.random(_inner, _outer) + return point + end - return Point + local function _checkSurface(point) + local stype=land.getSurfaceType(point) + for _,sf in pairs(surfacetypes) do + if sf==stype then + return true + end + end + return false + end + + local point=_getpoint() + + if surfacetypes then + local N=1 ; local Nmax=100 ; local gotit=false + while gotit==false and N<=Nmax do + gotit=_checkSurface(point) + if gotit then + --env.info(string.format("Got random coordinate with surface type %d after N=%d/%d iterations", land.getSurfaceType(point), N, Nmax)) + else + point=_getpoint() + N=N+1 + end + end + end + + return point end --- Returns a @{Core.Point#POINT_VEC2} object reflecting a random 2D location within the zone. @@ -13792,15 +15566,15 @@ end --- Returns a @{Core.Point#COORDINATE} object reflecting a random 3D location within the zone. -- @param #ZONE_RADIUS self --- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. --- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. --- @return Core.Point#COORDINATE -function ZONE_RADIUS:GetRandomCoordinate( inner, outer ) - self:F( self.ZoneName, inner, outer ) +-- @param #number inner (Optional) Minimal distance from the center of the zone in meters. Default is 0 m. +-- @param #number outer (Optional) Maximal distance from the outer edge of the zone in meters. Default is the radius of the zone. +-- @param #table surfacetypes (Optional) Table of surface types. Can also be a single surface type. We will try max 1000 times to find the right type! +-- @return Core.Point#COORDINATE The random coordinate. +function ZONE_RADIUS:GetRandomCoordinate(inner, outer, surfacetypes) - local Coordinate = COORDINATE:NewFromVec2( self:GetRandomVec2(inner, outer) ) + local vec2=self:GetRandomVec2(inner, outer, surfacetypes) - self:T3( { Coordinate = Coordinate } ) + local Coordinate = COORDINATE:NewFromVec2(vec2) return Coordinate end @@ -13863,7 +15637,7 @@ function ZONE:New( ZoneName ) -- Error! if not Zone then - error( "Zone " .. ZoneName .. " does not exist." ) + env.error( "ERROR: Zone " .. ZoneName .. " does not exist!" ) return nil end @@ -13881,9 +15655,9 @@ function ZONE:New( ZoneName ) end --- Find a zone in the _DATABASE using the name of the zone. --- @param #ZONE_BASE self +-- @param #ZONE self -- @param #string ZoneName The name of the zone. --- @return #ZONE_BASE self +-- @return #ZONE self function ZONE:FindByName( ZoneName ) local ZoneFound = _DATABASE:FindZone( ZoneName ) @@ -14119,7 +15893,7 @@ end --- @type ZONE_POLYGON_BASE --- --@field #ZONE_POLYGON_BASE.ListVec2 Polygon The polygon defined by an array of @{DCS#Vec2}. +-- @field #ZONE_POLYGON_BASE.ListVec2 Polygon The polygon defined by an array of @{DCS#Vec2}. -- @extends #ZONE_BASE @@ -14357,7 +16131,7 @@ function ZONE_POLYGON_BASE:BoundZone( UnBound ) end ---- Draw the zone on the F10 map. **NOTE** Currently, only polygons with **exactly four points** are supported! +--- Draw the zone on the F10 map. **NOTE** Currently, only polygons **up to ten points** are supported! -- @param #ZONE_POLYGON_BASE self -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red. @@ -14369,32 +16143,48 @@ end -- @return #ZONE_POLYGON_BASE self function ZONE_POLYGON_BASE:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) - local coordinate=COORDINATE:NewFromVec2(self._.Polygon[1]) - - Color=Color or self:GetColorRGB() - Alpha=Alpha or 1 - FillColor=FillColor or Color - FillAlpha=FillAlpha or self:GetColorAlpha() - - - if #self._.Polygon==4 then - - local Coord2=COORDINATE:NewFromVec2(self._.Polygon[2]) - local Coord3=COORDINATE:NewFromVec2(self._.Polygon[3]) - local Coord4=COORDINATE:NewFromVec2(self._.Polygon[4]) - - self.DrawID=coordinate:QuadToAll(Coord2, Coord3, Coord4, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) - - else - - local Coordinates=self:GetVerticiesCoordinates() - table.remove(Coordinates, 1) - - self.DrawID=coordinate:MarkupToAllFreeForm(Coordinates, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) + if self._.Polygon and #self._.Polygon>=3 then + local coordinate=COORDINATE:NewFromVec2(self._.Polygon[1]) + + Coalition=Coalition or self:GetDrawCoalition() + + -- Set draw coalition. + self:SetDrawCoalition(Coalition) + + Color=Color or self:GetColorRGB() + Alpha=Alpha or 1 + + -- Set color. + self:SetColor(Color, Alpha) + + FillColor=FillColor or self:GetFillColorRGB() + if not FillColor then UTILS.DeepCopy(Color) end + FillAlpha=FillAlpha or self:GetFillColorAlpha() + if not FillAlpha then FillAlpha=0.15 end + + -- Set fill color. + self:SetFillColor(FillColor, FillAlpha) + + if #self._.Polygon==4 then + + local Coord2=COORDINATE:NewFromVec2(self._.Polygon[2]) + local Coord3=COORDINATE:NewFromVec2(self._.Polygon[3]) + local Coord4=COORDINATE:NewFromVec2(self._.Polygon[4]) + + self.DrawID=coordinate:QuadToAll(Coord2, Coord3, Coord4, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) + + else + + local Coordinates=self:GetVerticiesCoordinates() + table.remove(Coordinates, 1) + + self.DrawID=coordinate:MarkupToAllFreeForm(Coordinates, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) + + end + end - return self end @@ -14475,7 +16265,7 @@ end -- @return #boolean true if the location is within the zone. function ZONE_POLYGON_BASE:IsVec2InZone( Vec2 ) self:F2( Vec2 ) - + if not Vec2 then return false end local Next local Prev local InPolygon = false @@ -14499,30 +16289,46 @@ function ZONE_POLYGON_BASE:IsVec2InZone( Vec2 ) return InPolygon end +--- Returns if a point is within the zone. +-- @param #ZONE_POLYGON_BASE self +-- @param DCS#Vec3 Vec3 The point to test. +-- @return #boolean true if the point is within the zone. +function ZONE_POLYGON_BASE:IsVec3InZone( Vec3 ) + self:F2( Vec3 ) + + if not Vec3 then return false end + + local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } ) + + return InZone +end + --- Define a random @{DCS#Vec2} within the zone. -- @param #ZONE_POLYGON_BASE self -- @return DCS#Vec2 The Vec2 coordinate. function ZONE_POLYGON_BASE:GetRandomVec2() - self:F2() - --- It is a bit tricky to find a random point within a polygon. Right now i am doing it the dirty and inefficient way... - local Vec2Found = false - local Vec2 + -- It is a bit tricky to find a random point within a polygon. Right now i am doing it the dirty and inefficient way... + + -- Get the bounding square. local BS = self:GetBoundingSquare() - self:T2( BS ) + local Nmax=1000 ; local n=0 + while n0 then + self:ScheduleOnce(Delay, ZONE_ELASTIC.StopUpdate, self) + else + + if self.updateID then + + self:ScheduleStop(self.updateID) + + self.updateID=nil + + end + + end + + return self + end + + + --- Create a convec hull. + -- @param #ZONE_ELASTIC self + -- @param #table pl Points + -- @return #table Points + function ZONE_ELASTIC:_ConvexHull(pl) + + if #pl == 0 then + return {} + end + + table.sort(pl, function(left,right) + return left.x < right.x + end) + + local h = {} + + -- Function: ccw > 0 if three points make a counter-clockwise turn, clockwise if ccw < 0, and collinear if ccw = 0. + local function ccw(a,b,c) + return (b.x - a.x) * (c.y - a.y) > (b.y - a.y) * (c.x - a.x) + end + + -- lower hull + for i,pt in pairs(pl) do + while #h >= 2 and not ccw(h[#h-1], h[#h], pt) do + table.remove(h,#h) + end + table.insert(h,pt) + end + + -- upper hull + local t = #h + 1 + for i=#pl, 1, -1 do + local pt = pl[i] + while #h >= t and not ccw(h[#h-1], h[#h], pt) do + table.remove(h, #h) + end + table.insert(h, pt) + end + + table.remove(h, #h) + + return h + end + +end + do -- ZONE_AIRBASE --- @type ZONE_AIRBASE + -- @field #boolean isShip If `true`, airbase is a ship. + -- @field #boolean isHelipad If `true`, airbase is a helipad. + -- @field #boolean isAirdrome If `true`, airbase is an airdrome. -- @extends #ZONE_RADIUS @@ -14742,6 +16751,20 @@ do -- ZONE_AIRBASE self._.ZoneAirbase = Airbase self._.ZoneVec2Cache = self._.ZoneAirbase:GetVec2() + + if Airbase:IsShip() then + self.isShip=true + self.isHelipad=false + self.isAirdrome=false + elseif Airbase:IsHelipad() then + self.isShip=false + self.isHelipad=true + self.isAirdrome=false + elseif Airbase:IsAirdrome() then + self.isShip=false + self.isHelipad=false + self.isAirdrome=true + end -- Zone objects are added to the _DATABASE and SET_ZONE objects. _EVENTDISPATCHER:CreateEventNewZone( self ) @@ -14756,9 +16779,9 @@ do -- ZONE_AIRBASE return self._.ZoneAirbase end - --- Returns the current location of the @{Wrapper.Group}. + --- Returns the current location of the AIRBASE. -- @param #ZONE_AIRBASE self - -- @return DCS#Vec2 The location of the zone based on the @{Wrapper.Group} location. + -- @return DCS#Vec2 The location of the zone based on the AIRBASE location. function ZONE_AIRBASE:GetVec2() self:F( self.ZoneName ) @@ -14776,24 +16799,6 @@ do -- ZONE_AIRBASE return ZoneVec2 end - --- Returns a random location within the zone of the @{Wrapper.Group}. - -- @param #ZONE_AIRBASE self - -- @return DCS#Vec2 The random location of the zone based on the @{Wrapper.Group} location. - function ZONE_AIRBASE:GetRandomVec2() - self:F( self.ZoneName ) - - local Point = {} - local Vec2 = self._.ZoneAirbase:GetVec2() - - local angle = math.random() * math.pi*2; - Point.x = Vec2.x + math.cos( angle ) * math.random() * self:GetRadius(); - Point.y = Vec2.y + math.sin( angle ) * math.random() * self:GetRadius(); - - self:T( { Point } ) - - return Point - end - --- Returns a @{Core.Point#POINT_VEC2} object reflecting a random 2D location within the zone. -- @param #ZONE_AIRBASE self -- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. @@ -15140,6 +17145,7 @@ function DATABASE:New() self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) self:HandleEvent( EVENTS.RemoveUnit, self._EventOnDeadOrCrash ) + --self:HandleEvent( EVENTS.UnitLost, self._EventOnDeadOrCrash ) -- DCS 2.7.1 for Aerial units no dead event ATM self:HandleEvent( EVENTS.Hit, self.AccountHits ) self:HandleEvent( EVENTS.NewCargo ) self:HandleEvent( EVENTS.DeleteCargo ) @@ -15178,17 +17184,11 @@ end function DATABASE:AddUnit( DCSUnitName ) if not self.UNITS[DCSUnitName] then - -- Debug info. self:T( { "Add UNIT:", DCSUnitName } ) - --local UnitRegister = UNIT:Register( DCSUnitName ) - -- Register unit self.UNITS[DCSUnitName]=UNIT:Register(DCSUnitName) - - -- This is not used anywhere in MOOSE as far as I can see so I remove it until there comes an error somewhere. - --table.insert(self.UNITS_Index, DCSUnitName ) end return self.UNITS[DCSUnitName] @@ -15198,7 +17198,6 @@ end --- Deletes a Unit from the DATABASE based on the Unit Name. -- @param #DATABASE self function DATABASE:DeleteUnit( DCSUnitName ) - self.UNITS[DCSUnitName] = nil end @@ -15233,16 +17232,6 @@ function DATABASE:FindStatic( StaticName ) return StaticFound end ---- Finds a AIRBASE based on the AirbaseName. --- @param #DATABASE self --- @param #string AirbaseName --- @return Wrapper.Airbase#AIRBASE The found AIRBASE. -function DATABASE:FindAirbase( AirbaseName ) - - local AirbaseFound = self.AIRBASES[AirbaseName] - return AirbaseFound -end - --- Adds a Airbase based on the Airbase Name in the DATABASE. -- @param #DATABASE self -- @param #string AirbaseName The name of the airbase. @@ -15354,6 +17343,9 @@ do -- Zones -- Store color of zone. Zone.Color=color + + -- Store zone ID. + Zone.ZoneID=ZoneData.zoneId -- Store in DB. self.ZONENAMES[ZoneName] = ZoneName @@ -15794,7 +17786,9 @@ function DATABASE:_RegisterStaticTemplate( StaticTemplate, CoalitionID, Category local StaticTemplate = UTILS.DeepCopy( StaticTemplate ) - local StaticTemplateName = env.getValueDictByKey(StaticTemplate.name) + local StaticTemplateGroupName = env.getValueDictByKey(StaticTemplate.name) + + local StaticTemplateName=StaticTemplate.units[1].name self.Templates.Statics[StaticTemplateName] = self.Templates.Statics[StaticTemplateName] or {} @@ -15802,7 +17796,7 @@ function DATABASE:_RegisterStaticTemplate( StaticTemplate, CoalitionID, Category StaticTemplate.CoalitionID = CoalitionID StaticTemplate.CountryID = CountryID - self.Templates.Statics[StaticTemplateName].StaticName = StaticTemplateName + self.Templates.Statics[StaticTemplateName].StaticName = StaticTemplateGroupName self.Templates.Statics[StaticTemplateName].GroupTemplate = StaticTemplate self.Templates.Statics[StaticTemplateName].UnitTemplate = StaticTemplate.units[1] self.Templates.Statics[StaticTemplateName].CategoryID = CategoryID @@ -15876,6 +17870,20 @@ function DATABASE:GetGroupTemplateFromUnitName( UnitName ) end end +--- Get group template from unit name. +-- @param #DATABASE self +-- @param #string UnitName Name of the unit. +-- @return #table Group template. +function DATABASE:GetUnitTemplateFromUnitName( UnitName ) + if self.Templates.Units[UnitName] then + return self.Templates.Units[UnitName] + else + self:E("ERROR: Unit template does not exist for unit "..tostring(UnitName)) + return nil + end +end + + --- Get coalition ID from client name. -- @param #DATABASE self -- @param #string ClientName Name of the Client. @@ -15992,13 +18000,15 @@ function DATABASE:_RegisterClients() for ClientName, ClientTemplate in pairs( self.Templates.ClientsByName ) do self:I(string.format("Register Client: %s", tostring(ClientName))) - self:AddClient( ClientName ) + local client=self:AddClient( ClientName ) + client.SpawnCoord=COORDINATE:New(ClientTemplate.x, ClientTemplate.alt, ClientTemplate.y) end return self end ---- @param #DATABASE self +--- Private method that registeres all static objects. +-- @param #DATABASE self function DATABASE:_RegisterStatics() local CoalitionsData={GroupsRed=coalition.getStaticObjects(coalition.side.RED), GroupsBlue=coalition.getStaticObjects(coalition.side.BLUE), GroupsNeutral=coalition.getStaticObjects(coalition.side.NEUTRAL)} @@ -16040,7 +18050,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]) @@ -16049,11 +18059,6 @@ function DATABASE:_RegisterAirbases() text=text.."]" self:I(text) - -- Check for DCS bug IDs. - if airbaseID~=airbase:GetID() then - --self:E("WARNING: :getID does NOT match :GetID!") - end - end return self @@ -16158,6 +18163,22 @@ function DATABASE:_EventOnDeadOrCrash( Event ) if self.STATICS[Event.IniDCSUnitName] then self:DeleteStatic( Event.IniDCSUnitName ) end + + --- + -- Maybe a UNIT? + --- + + -- Delete unit. + if self.UNITS[Event.IniDCSUnitName] then + self:T("STATIC Event for UNIT "..tostring(Event.IniDCSUnitName)) + local DCSUnit = _DATABASE:FindUnit( Event.IniDCSUnitName ) + self:T({DCSUnit}) + if DCSUnit then + --self:I("Creating DEAD Event for UNIT "..tostring(Event.IniDCSUnitName)) + --DCSUnit:Destroy(true) + return + end + end else @@ -16490,19 +18511,19 @@ function DATABASE:SetPlayerSettings( PlayerName, Settings ) self.PLAYERSETTINGS[PlayerName] = Settings end ---- Add a flight group to the data base. +--- Add an OPS group (FLIGHTGROUP, ARMYGROUP, NAVYGROUP) to the data base. -- @param #DATABASE self --- @param Ops.FlightGroup#FLIGHTGROUP flightgroup -function DATABASE:AddFlightGroup(flightgroup) - self:T({NewFlightGroup=flightgroup.groupname}) - self.FLIGHTGROUPS[flightgroup.groupname]=flightgroup +-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group added to the DB. +function DATABASE:AddOpsGroup(opsgroup) + --env.info("Adding OPSGROUP "..tostring(opsgroup.groupname)) + self.FLIGHTGROUPS[opsgroup.groupname]=opsgroup end ---- Get a flight group from the data base. +--- Get an OPS group (FLIGHTGROUP, ARMYGROUP, NAVYGROUP) from the data base. -- @param #DATABASE self --- @param #string groupname Group name of the flight group. Can also be passed as GROUP object. --- @return Ops.FlightGroup#FLIGHTGROUP Flight group object. -function DATABASE:GetFlightGroup(groupname) +-- @param #string groupname Group name of the group. Can also be passed as GROUP object. +-- @return Ops.OpsGroup#OPSGROUP OPS group object. +function DATABASE:GetOpsGroup(groupname) -- Get group and group name. if type(groupname)=="string" then @@ -16510,9 +18531,53 @@ function DATABASE:GetFlightGroup(groupname) groupname=groupname:GetName() end + --env.info("Getting OPSGROUP "..tostring(groupname)) return self.FLIGHTGROUPS[groupname] end +--- Find an OPSGROUP (FLIGHTGROUP, ARMYGROUP, NAVYGROUP) in the data base. +-- @param #DATABASE self +-- @param #string groupname Group name of the group. Can also be passed as GROUP object. +-- @return Ops.OpsGroup#OPSGROUP OPS group object. +function DATABASE:FindOpsGroup(groupname) + + -- Get group and group name. + if type(groupname)=="string" then + else + groupname=groupname:GetName() + end + + --env.info("Getting OPSGROUP "..tostring(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 @@ -16597,7 +18662,7 @@ function DATABASE:_RegisterTemplates() if obj_type_name ~= "static" and Template and Template.units and type(Template.units) == 'table' then --making sure again- this is a valid group - self:_RegisterGroupTemplate(Template, CoalitionSide, _DATABASECategory[string.lower(CategoryName)], CountryID) + self:_RegisterGroupTemplate(Template, CoalitionSide, _DATABASECategory[string.lower(CategoryName)], CountryID) else @@ -16726,9 +18791,9 @@ end -- Various types of SET_ classes are available: -- -- * @{#SET_GROUP}: Defines a collection of @{Wrapper.Group}s filtered by filter criteria. --- * @{#SET_UNIT}: Defines a colleciton of @{Wrapper.Unit}s filtered by filter criteria. +-- * @{#SET_UNIT}: Defines a collection of @{Wrapper.Unit}s filtered by filter criteria. -- * @{#SET_STATIC}: Defines a collection of @{Wrapper.Static}s filtered by filter criteria. --- * @{#SET_CLIENT}: Defines a collection of @{Client}s filterd by filter criteria. +-- * @{#SET_CLIENT}: Defines a collection of @{Client}s filtered by filter criteria. -- * @{#SET_AIRBASE}: Defines a collection of @{Wrapper.Airbase}s filtered by filter criteria. -- * @{#SET_CARGO}: Defines a collection of @{Cargo.Cargo}s filtered by filter criteria. -- * @{#SET_ZONE}: Defines a collection of @{Core.Zone}s filtered by filter criteria. @@ -16756,14 +18821,14 @@ do -- SET_BASE --- @type SET_BASE -- @field #table Filter Table of filters. -- @field #table Set Table of objects. - -- @field #table Index Table of indicies. + -- @field #table Index Table of indices. -- @field #table List Unused table. -- @field Core.Scheduler#SCHEDULER CallScheduler -- @extends Core.Base#BASE --- The @{Core.Set#SET_BASE} class defines the core functions that define a collection of objects. - -- A SET provides iterators to iterate the SET, but will **temporarily** yield the ForEach interator loop at defined **"intervals"** to the mail simulator loop. + -- A SET provides iterators to iterate the SET, but will **temporarily** yield the ForEach iterator loop at defined **"intervals"** to the mail simulator loop. -- In this way, large loops can be done while not blocking the simulator main processing loop. -- The default **"yield interval"** is after 10 objects processed. -- The default **"time interval"** is after 0.001 seconds. @@ -16774,7 +18839,7 @@ do -- SET_BASE -- -- ## Define the SET iterator **"yield interval"** and the **"time interval"** -- - -- Modify the iterator intervals with the @{Core.Set#SET_BASE.SetInteratorIntervals} method. + -- Modify the iterator intervals with the @{Core.Set#SET_BASE.SetIteratorIntervals} method. -- You can set the **"yield interval"**, and the **"time interval"**. (See above). -- -- @field #SET_BASE SET_BASE @@ -16914,7 +18979,10 @@ do -- SET_BASE -- @param NoTriggerEvent (optional) When `true`, the :Remove() method will not trigger a **Removed** event. function SET_BASE:Remove( ObjectName, NoTriggerEvent ) self:F2( { ObjectName = ObjectName } ) - + + local TriggerEvent = true + if NoTriggerEvent then TriggerEvent = false end + local Object = self.Set[ObjectName] if Object then @@ -16926,7 +18994,7 @@ do -- SET_BASE end end -- When NoTriggerEvent is true, then no Removed event will be triggered. - if not NoTriggerEvent then + if TriggerEvent then self:Removed( ObjectName, Object ) end end @@ -16939,7 +19007,9 @@ do -- SET_BASE -- @param Core.Base#BASE Object The object itself. -- @return Core.Base#BASE The added BASE Object. function SET_BASE:Add( ObjectName, Object ) - self:F2( { ObjectName = ObjectName, Object = Object } ) + + -- Debug info. + self:T( { ObjectName = ObjectName, Object = Object } ) -- Ensure that the existing element is removed from the Set before a new one is inserted to the Set if self.Set[ObjectName] then @@ -16969,6 +19039,32 @@ do -- SET_BASE end + --- Sort the set by name. + -- @param #SET_BASE self + -- @return Core.Base#BASE The added BASE Object. + function SET_BASE:SortByName() + + local function sort(a, b) + return a1 then + local norm=UTILS.VecNorm(vec) + f=Fraction/norm + end -- Scale the vector. vec.x=f*vec.x @@ -23502,7 +26509,9 @@ do -- COORDINATE -- Move the vector to start at the end of A. vec=UTILS.VecAdd(self, vec) + -- Create a new coordiante object. local coord=COORDINATE:New(vec.x,vec.y,vec.z) + return coord end @@ -23511,9 +26520,8 @@ do -- COORDINATE -- @param #COORDINATE TargetCoordinate The target COORDINATE. Can also be a DCS#Vec3. -- @return DCS#Distance Distance The distance in meters. function COORDINATE:Get2DDistance(TargetCoordinate) - + if not TargetCoordinate then return 1000000 end local a={x=TargetCoordinate.x-self.x, y=0, z=TargetCoordinate.z-self.z} - local norm=UTILS.VecNorm(a) return norm end @@ -23536,7 +26544,7 @@ do -- COORDINATE -- The text will reflect the temperature like this: -- -- - For Russian and European aircraft using the metric system - Degrees Celcius (°C) - -- - For Americain aircraft we link to the imperial system - Degrees Farenheit (°F) + -- - For American aircraft we link to the imperial system - Degrees Fahrenheit (°F) -- -- A text containing a pressure will look like this: -- @@ -23556,7 +26564,7 @@ do -- COORDINATE if Settings:IsMetric() then return string.format( " %-2.2f °C", DegreesCelcius ) else - return string.format( " %-2.2f °F", UTILS.CelciusToFarenheit( DegreesCelcius ) ) + return string.format( " %-2.2f °F", UTILS.CelsiusToFahrenheit( DegreesCelcius ) ) end else return " no temperature" @@ -23582,7 +26590,7 @@ do -- COORDINATE -- The text will contain always the pressure in hPa and: -- -- - For Russian and European aircraft using the metric system - hPa and mmHg - -- - For Americain and European aircraft we link to the imperial system - hPa and inHg + -- - For American and European aircraft we link to the imperial system - hPa and inHg -- -- A text containing a pressure will look like this: -- @@ -23675,7 +26683,7 @@ do -- COORDINATE -- The text will reflect the wind like this: -- -- - For Russian and European aircraft using the metric system - Wind direction in degrees (°) and wind speed in meters per second (mps). - -- - For Americain aircraft we link to the imperial system - Wind direction in degrees (°) and wind speed in knots per second (kps). + -- - For American aircraft we link to the imperial system - Wind direction in degrees (°) and wind speed in knots per second (kps). -- -- A text containing a pressure will look like this: -- @@ -23736,25 +26744,28 @@ do -- COORDINATE -- @param #COORDINATE self -- @param #number Distance The distance in meters. -- @param Core.Settings#SETTINGS Settings + -- @param #string Language (optional) "EN" or "RU" + -- @param #number Precision (optional) round to this many decimal places -- @return #string The distance text expressed in the units of measurement. - function COORDINATE:GetDistanceText( Distance, Settings, Language ) + function COORDINATE:GetDistanceText( Distance, Settings, Language, Precision ) local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS local Language = Language or "EN" - + local Precision = Precision or 0 + local DistanceText if Settings:IsMetric() then if Language == "EN" then - DistanceText = " for " .. UTILS.Round( Distance / 1000, 2 ) .. " km" + DistanceText = " for " .. UTILS.Round( Distance / 1000, Precision ) .. " km" elseif Language == "RU" then - DistanceText = " за " .. UTILS.Round( Distance / 1000, 2 ) .. " километров" + DistanceText = " за " .. UTILS.Round( Distance / 1000, Precision ) .. " километров" end else if Language == "EN" then - DistanceText = " for " .. UTILS.Round( UTILS.MetersToNM( Distance ), 2 ) .. " miles" + DistanceText = " for " .. UTILS.Round( UTILS.MetersToNM( Distance ), Precision ) .. " miles" elseif Language == "RU" then - DistanceText = " за " .. UTILS.Round( UTILS.MetersToNM( Distance ), 2 ) .. " миль" + DistanceText = " за " .. UTILS.Round( UTILS.MetersToNM( Distance ), Precision ) .. " миль" end end @@ -23832,7 +26843,7 @@ do -- COORDINATE local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS local BearingText = self:GetBearingText( AngleRadians, 0, Settings, Language ) - local DistanceText = self:GetDistanceText( Distance, Settings, Language ) + local DistanceText = self:GetDistanceText( Distance, Settings, Language, 0 ) local BRText = BearingText .. DistanceText @@ -23850,7 +26861,7 @@ do -- COORDINATE local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS local BearingText = self:GetBearingText( AngleRadians, 0, Settings, Language ) - local DistanceText = self:GetDistanceText( Distance, Settings, Language ) + local DistanceText = self:GetDistanceText( Distance, Settings, Language, 0 ) local AltitudeText = self:GetAltitudeText( Settings, Language ) local BRAText = BearingText .. DistanceText .. AltitudeText -- When the POINT is a VEC2, there will be no altitude shown. @@ -24285,8 +27296,8 @@ do -- COORDINATE if x and y then local vec2={ x = x, y = y } coord=COORDINATE:NewFromVec2(vec2) - end - return coord + end + return coord end @@ -24392,7 +27403,7 @@ do -- COORDINATE return self:GetSurfaceType()==land.SurfaceType.LAND end - --- Checks if the surface type is road. + --- Checks if the surface type is land. -- @param #COORDINATE self -- @return #boolean If true, the surface type at the coordinate is land. function COORDINATE:IsSurfaceTypeLand() @@ -24432,10 +27443,9 @@ do -- COORDINATE --- Creates an explosion at the point of a certain intensity. -- @param #COORDINATE self -- @param #number ExplosionIntensity Intensity of the explosion in kg TNT. Default 100 kg. - -- @param #number Delay Delay before explosion in seconds. + -- @param #number Delay (Optional) Delay before explosion is triggered in seconds. -- @return #COORDINATE self function COORDINATE:Explosion( ExplosionIntensity, Delay ) - self:F2( { ExplosionIntensity } ) ExplosionIntensity=ExplosionIntensity or 100 if Delay and Delay>0 then self:ScheduleOnce(Delay, self.Explosion, self, ExplosionIntensity) @@ -24447,11 +27457,17 @@ do -- COORDINATE --- Creates an illumination bomb at the point. -- @param #COORDINATE self - -- @param #number power Power of illumination bomb in Candela. + -- @param #number Power Power of illumination bomb in Candela. Default 1000 cd. + -- @param #number Delay (Optional) Delay before bomb is ignited in seconds. -- @return #COORDINATE self - function COORDINATE:IlluminationBomb(power) - self:F2() - trigger.action.illuminationBomb( self:GetVec3(), power ) + function COORDINATE:IlluminationBomb(Power, Delay) + Power=Power or 1000 + if Delay and Delay>0 then + self:ScheduleOnce(Delay, self.IlluminationBomb, self, Power) + else + trigger.action.illuminationBomb(self:GetVec3(), Power) + end + return self end @@ -24502,82 +27518,101 @@ do -- COORDINATE -- @param #COORDINATE self -- @param Utilities.Utils#BIGSMOKEPRESET preset Smoke preset (1=small smoke and fire, 2=medium smoke and fire, 3=large smoke and fire, 4=huge smoke and fire, 5=small smoke, 6=medium smoke, 7=large smoke, 8=huge smoke). -- @param #number density (Optional) Smoke density. Number in [0,...,1]. Default 0.5. - function COORDINATE:BigSmokeAndFire( preset, density ) + -- @param #string name (Optional) Name of the fire to stop it later again if not using the same COORDINATE object. Defaults to "Fire-" plus a random 5-digit-number. + function COORDINATE:BigSmokeAndFire( preset, density, name ) self:F2( { preset=preset, density=density } ) density=density or 0.5 - trigger.action.effectSmokeBig( self:GetVec3(), preset, density ) + self.firename = name or "Fire-"..math.random(1,10000) + trigger.action.effectSmokeBig( self:GetVec3(), preset, density, self.firename ) + end + + --- Stop big smoke and fire at the coordinate. + -- @param #COORDINATE self + -- @param #string name (Optional) Name of the fire to stop it, if not using the same COORDINATE object. + function COORDINATE:StopBigSmokeAndFire( name ) + self:F2( { name = name } ) + name = name or self.firename + trigger.action.effectSmokeStop( name ) end --- Small smoke and fire at the coordinate. -- @param #COORDINATE self - -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. - function COORDINATE:BigSmokeAndFireSmall( density ) + -- @param #number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. + -- @param #string name (Optional) Name of the fire to stop it later again if not using the same COORDINATE object. Defaults to "Fire-" plus a random 5-digit-number. + function COORDINATE:BigSmokeAndFireSmall( density, name ) self:F2( { density=density } ) density=density or 0.5 - self:BigSmokeAndFire(BIGSMOKEPRESET.SmallSmokeAndFire, density) + self:BigSmokeAndFire(BIGSMOKEPRESET.SmallSmokeAndFire, density, name) end --- Medium smoke and fire at the coordinate. -- @param #COORDINATE self - -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. - function COORDINATE:BigSmokeAndFireMedium( density ) + -- @param #number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. + -- @param #string name (Optional) Name of the fire to stop it later again if not using the same COORDINATE object. Defaults to "Fire-" plus a random 5-digit-number. + function COORDINATE:BigSmokeAndFireMedium( density, name ) self:F2( { density=density } ) density=density or 0.5 - self:BigSmokeAndFire(BIGSMOKEPRESET.MediumSmokeAndFire, density) + self:BigSmokeAndFire(BIGSMOKEPRESET.MediumSmokeAndFire, density, name) end --- Large smoke and fire at the coordinate. -- @param #COORDINATE self - -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. - function COORDINATE:BigSmokeAndFireLarge( density ) + -- @param #number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. + -- @param #string name (Optional) Name of the fire to stop it later again if not using the same COORDINATE object. Defaults to "Fire-" plus a random 5-digit-number. + function COORDINATE:BigSmokeAndFireLarge( density, name ) self:F2( { density=density } ) density=density or 0.5 - self:BigSmokeAndFire(BIGSMOKEPRESET.LargeSmokeAndFire, density) + self:BigSmokeAndFire(BIGSMOKEPRESET.LargeSmokeAndFire, density, name) end --- Huge smoke and fire at the coordinate. -- @param #COORDINATE self - -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. - function COORDINATE:BigSmokeAndFireHuge( density ) + -- @param #number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. + -- @param #string name (Optional) Name of the fire to stop it later again if not using the same COORDINATE object. Defaults to "Fire-" plus a random 5-digit-number. + function COORDINATE:BigSmokeAndFireHuge( density, name ) self:F2( { density=density } ) density=density or 0.5 - self:BigSmokeAndFire(BIGSMOKEPRESET.HugeSmokeAndFire, density) + self:BigSmokeAndFire(BIGSMOKEPRESET.HugeSmokeAndFire, density, name) end --- Small smoke at the coordinate. -- @param #COORDINATE self - -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. - function COORDINATE:BigSmokeSmall( density ) + -- @param #number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. + -- @param #string name (Optional) Name of the fire to stop it later again if not using the same COORDINATE object. Defaults to "Fire-" plus a random 5-digit-number. + function COORDINATE:BigSmokeSmall( density, name ) self:F2( { density=density } ) density=density or 0.5 - self:BigSmokeAndFire(BIGSMOKEPRESET.SmallSmoke, density) + self:BigSmokeAndFire(BIGSMOKEPRESET.SmallSmoke, density, name) end --- Medium smoke at the coordinate. -- @param #COORDINATE self - -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. - function COORDINATE:BigSmokeMedium( density ) + -- @param number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. + -- @param #string name (Optional) Name of the fire to stop it later again if not using the same COORDINATE object. Defaults to "Fire-" plus a random 5-digit-number. + function COORDINATE:BigSmokeMedium( density, name ) self:F2( { density=density } ) density=density or 0.5 - self:BigSmokeAndFire(BIGSMOKEPRESET.MediumSmoke, density) + self:BigSmokeAndFire(BIGSMOKEPRESET.MediumSmoke, density, name) end --- Large smoke at the coordinate. -- @param #COORDINATE self - -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. - function COORDINATE:BigSmokeLarge( density ) + -- @param #number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. + -- @param #string name (Optional) Name of the fire to stop it later again if not using the same COORDINATE object. Defaults to "Fire-" plus a random 5-digit-number. + function COORDINATE:BigSmokeLarge( density, name ) self:F2( { density=density } ) density=density or 0.5 - self:BigSmokeAndFire(BIGSMOKEPRESET.LargeSmoke, density) + self:BigSmokeAndFire(BIGSMOKEPRESET.LargeSmoke, density,name) end --- Huge smoke at the coordinate. -- @param #COORDINATE self - -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. - function COORDINATE:BigSmokeHuge( density ) + -- @param #number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. + -- @param #string name (Optional) Name of the fire to stop it later again if not using the same COORDINATE object. Defaults to "Fire-" plus a random 5-digit-number. + function COORDINATE:BigSmokeHuge( density, name ) self:F2( { density=density } ) density=density or 0.5 - self:BigSmokeAndFire(BIGSMOKEPRESET.HugeSmoke, density) + self:BigSmokeAndFire(BIGSMOKEPRESET.HugeSmoke, density,name) end --- Flares the point in a color. @@ -24724,7 +27759,7 @@ do -- COORDINATE --- Line to all. -- Creates a line on the F10 map from one point to another. -- @param #COORDINATE self - -- @param #COORDINATE Endpoint COORDIANTE to where the line is drawn. + -- @param #COORDINATE Endpoint COORDINATE to where the line is drawn. -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default). -- @param #number Alpha Transparency [0,1]. Default 1. @@ -24749,7 +27784,7 @@ do -- COORDINATE --- Circle to all. -- Creates a circle on the map with a given radius, color, fill color, and outline. -- @param #COORDINATE self - -- @param #numberr Radius Radius in meters. Default 1000 m. + -- @param #number Radius Radius in meters. Default 1000 m. -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default). -- @param #number Alpha Transparency [0,1]. Default 1. @@ -24764,14 +27799,21 @@ do -- COORDINATE if ReadOnly==nil then ReadOnly=false end + local vec3=self:GetVec3() + Radius=Radius or 1000 + Coalition=Coalition or -1 + Color=Color or {1,0,0} Color[4]=Alpha or 1.0 + LineType=LineType or 1 - FillColor=FillColor or Color + + FillColor=FillColor or UTILS.DeepCopy(Color) FillColor[4]=FillAlpha or 0.15 + trigger.action.circleToAll(Coalition, MarkID, vec3, Radius, Color, FillColor, LineType, ReadOnly, Text or "") return MarkID end @@ -24781,7 +27823,7 @@ do -- COORDINATE --- Rectangle to all. Creates a rectangle on the map from the COORDINATE in one corner to the end COORDINATE in the opposite corner. -- Creates a line on the F10 map from one point to another. -- @param #COORDINATE self - -- @param #COORDINATE Endpoint COORDIANTE in the opposite corner. + -- @param #COORDINATE Endpoint COORDINATE in the opposite corner. -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default). -- @param #number Alpha Transparency [0,1]. Default 1. @@ -24796,22 +27838,28 @@ do -- COORDINATE if ReadOnly==nil then ReadOnly=false end + local vec3=Endpoint:GetVec3() + Coalition=Coalition or -1 + Color=Color or {1,0,0} Color[4]=Alpha or 1.0 + LineType=LineType or 1 - FillColor=FillColor or Color + + FillColor=FillColor or UTILS.DeepCopy(Color) FillColor[4]=FillAlpha or 0.15 + trigger.action.rectToAll(Coalition, MarkID, self:GetVec3(), vec3, Color, FillColor, LineType, ReadOnly, Text or "") return MarkID end --- Creates a shape defined by 4 points on the F10 map. The first point is the current COORDINATE. The remaining three points need to be specified. -- @param #COORDINATE self - -- @param #COORDINATE Coord2 Second COORDIANTE of the quad shape. - -- @param #COORDINATE Coord3 Third COORDIANTE of the quad shape. - -- @param #COORDINATE Coord4 Fourth COORDIANTE of the quad shape. + -- @param #COORDINATE Coord2 Second COORDINATE of the quad shape. + -- @param #COORDINATE Coord3 Third COORDINATE of the quad shape. + -- @param #COORDINATE Coord4 Fourth COORDINATE of the quad shape. -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default). -- @param #number Alpha Transparency [0,1]. Default 1. @@ -24826,22 +27874,28 @@ do -- COORDINATE if ReadOnly==nil then ReadOnly=false end + local point1=self:GetVec3() local point2=Coord2:GetVec3() local point3=Coord3:GetVec3() local point4=Coord4:GetVec3() + Coalition=Coalition or -1 + Color=Color or {1,0,0} Color[4]=Alpha or 1.0 + LineType=LineType or 1 - FillColor=FillColor or Color + + FillColor=FillColor or UTILS.DeepCopy(Color) FillColor[4]=FillAlpha or 0.15 - trigger.action.quadToAll(Coalition, MarkID, self:GetVec3(), point2, point3, point4, Color, FillColor, LineType, ReadOnly, Text or "") + + trigger.action.quadToAll(Coalition, MarkID, point1, point2, point3, point4, Color, FillColor, LineType, ReadOnly, Text or "") return MarkID end --- Creates a free form shape on the F10 map. The first point is the current COORDINATE. The remaining points need to be specified. - -- **NOTE**: A free form polygon must have **at least three points** in total and currently only **up to 10 points** in total are supported. + -- **NOTE**: A free form polygon must have **at least three points** in total and currently only **up to 15 points** in total are supported. -- @param #COORDINATE self -- @param #table Coordinates Table of coordinates of the remaining points of the shape. -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. @@ -24894,8 +27948,28 @@ do -- COORDINATE trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], vecs[7], vecs[8], vecs[9], Color, FillColor, LineType, ReadOnly, Text or "") elseif #vecs==10 then trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], vecs[7], vecs[8], vecs[9], vecs[10], Color, FillColor, LineType, ReadOnly, Text or "") + elseif #vecs==11 then + trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], vecs[7], vecs[8], vecs[9], vecs[10], + vecs[11], + Color, FillColor, LineType, ReadOnly, Text or "") + elseif #vecs==12 then + trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], vecs[7], vecs[8], vecs[9], vecs[10], + vecs[11], vecs[12], + Color, FillColor, LineType, ReadOnly, Text or "") + elseif #vecs==13 then + trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], vecs[7], vecs[8], vecs[9], vecs[10], + vecs[11], vecs[12], vecs[13], + Color, FillColor, LineType, ReadOnly, Text or "") + elseif #vecs==14 then + trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], vecs[7], vecs[8], vecs[9], vecs[10], + vecs[11], vecs[12], vecs[13], vecs[14], + Color, FillColor, LineType, ReadOnly, Text or "") + elseif #vecs==15 then + trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], vecs[7], vecs[8], vecs[9], vecs[10], + vecs[11], vecs[12], vecs[13], vecs[14], vecs[15], + Color, FillColor, LineType, ReadOnly, Text or "") else - self:E("ERROR: Currently a free form polygon can only have 10 points in total!") + self:E("ERROR: Currently a free form polygon can only have 15 points in total!") -- Unfortunately, unpack(vecs) does not work! So no idea how to generalize this :( trigger.action.markupToAll(7, Coalition, MarkID, unpack(vecs), Color, FillColor, LineType, ReadOnly, Text or "") end @@ -24920,11 +27994,15 @@ do -- COORDINATE ReadOnly=false end Coalition=Coalition or -1 + Color=Color or {1,0,0} Color[4]=Alpha or 1.0 - FillColor=FillColor or Color + + FillColor=FillColor or UTILS.DeepCopy(Color) FillColor[4]=FillAlpha or 0.3 + FontSize=FontSize or 14 + trigger.action.textToAll(Coalition, MarkID, self:GetVec3(), Color, FillColor, FontSize, ReadOnly, Text or "Hello World") return MarkID end @@ -24946,13 +28024,19 @@ do -- COORDINATE if ReadOnly==nil then ReadOnly=false end + local vec3=Endpoint:GetVec3() + Coalition=Coalition or -1 + Color=Color or {1,0,0} Color[4]=Alpha or 1.0 + LineType=LineType or 1 - FillColor=FillColor or Color + + FillColor=FillColor or UTILS.DeepCopy(Color) FillColor[4]=FillAlpha or 0.15 + --trigger.action.textToAll(Coalition, MarkID, self:GetVec3(), Color, FillColor, FontSize, ReadOnly, Text or "Hello World") trigger.action.arrowToAll(Coalition, MarkID, vec3, self:GetVec3(), Color, FillColor, LineType, ReadOnly, Text or "") return MarkID @@ -25315,7 +28399,7 @@ do -- COORDINATE return "BR, " .. self:GetBRText( AngleRadians, Distance, Settings ) end - --- Return a BRAA string from a COORDINATE to the COORDINATE. + --- Return a BRA string from a COORDINATE to the COORDINATE. -- @param #COORDINATE self -- @param #COORDINATE FromCoordinate The coordinate to measure the distance and the bearing from. -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. @@ -25327,7 +28411,95 @@ do -- COORDINATE local Altitude = self:GetAltitudeText() return "BRA, " .. self:GetBRAText( AngleRadians, Distance, Settings, Language ) end + + --- Create a BRAA NATO call string to this COORDINATE from the FromCOORDINATE. Note - BRA delivered if no aspect can be obtained and "Merged" if range < 3nm + -- @param #COORDINATE self + -- @param #COORDINATE FromCoordinate The coordinate to measure the distance and the bearing from. + -- @param #boolean Bogey Add "Bogey" at the end if true (not yet declared hostile or friendly) + -- @param #boolean Spades Add "Spades" at the end if true (no IFF/VID ID yet known) + -- @param #boolean SSML Add SSML tags speaking aspect as 0 1 2 and "brah" instead of BRAA + -- @param #boolean Angels If true, altitude is e.g. "Angels 25" (i.e., a friendly plane), else "25 thousand" + -- @param #boolean Zeros If using SSML, be aware that Google TTS will say "oh" and not "zero" for "0"; if Zeros is set to true, "0" will be replaced with "zero" + -- @return #string The BRAA text. + function COORDINATE:ToStringBRAANATO(FromCoordinate,Bogey,Spades,SSML,Angels,Zeros) + + -- Thanks to @Pikey + local BRAANATO = "Merged." + local currentCoord = FromCoordinate + local DirectionVec3 = FromCoordinate:GetDirectionVec3( self ) + local AngleRadians = self:GetAngleRadians( DirectionVec3 ) + + local bearing = UTILS.Round( UTILS.ToDegree( AngleRadians ),0 ) + + local rangeMetres = self:Get2DDistance(currentCoord) + local rangeNM = UTILS.Round( UTILS.MetersToNM(rangeMetres), 0) + + local aspect = self:ToStringAspect(currentCoord) + + local alt = UTILS.Round(UTILS.MetersToFeet(self.y)/1000,0)--*1000 + + local alttext = string.format("%d thousand",alt) + + if Angels then + alttext = string.format("Angels %d",alt) + end + + if alt < 1 then + alttext = "very low" + end + + local track = UTILS.BearingToCardinal(bearing) or "North" + + if rangeNM > 3 then + if SSML then -- google says "oh" instead of zero, be aware + if Zeros then + bearing = string.format("%03d",bearing) + local AngleDegText = string.gsub(bearing,"%d","%1 ") -- "0 5 1 " + AngleDegText = string.gsub(AngleDegText," $","") -- "0 5 1" + AngleDegText = string.gsub(AngleDegText,"0","zero") + if aspect == "" then + BRAANATO = string.format("brah %s, %d miles, %s, Track %s", AngleDegText, rangeNM, alttext, track) + else + BRAANATO = string.format("brah %s, %d miles, %s, %s, Track %s", AngleDegText, rangeNM, alttext, aspect, track) + end + else + if aspect == "" then + BRAANATO = string.format("brah %03d, %d miles, %s, Track %s", bearing, rangeNM, alttext, track) + else + BRAANATO = string.format("brah %03d, %d miles, %s, %s, Track %s", bearing, rangeNM, alttext, aspect, track) + end + end + if Bogey and Spades then + BRAANATO = BRAANATO..", Bogey, Spades." + elseif Bogey then + BRAANATO = BRAANATO..", Bogey." + elseif Spades then + BRAANATO = BRAANATO..", Spades." + else + BRAANATO = BRAANATO.."." + end + else + if aspect == "" then + BRAANATO = string.format("BRA %03d, %d miles, %s, Track %s",bearing, rangeNM, alttext, track) + else + BRAANATO = string.format("BRAA %03d, %d miles, %s, %s, Track %s",bearing, rangeNM, alttext, aspect, track) + end + if Bogey and Spades then + BRAANATO = BRAANATO..", Bogey, Spades." + elseif Bogey then + BRAANATO = BRAANATO..", Bogey." + elseif Spades then + BRAANATO = BRAANATO..", Spades." + else + BRAANATO = BRAANATO.."." + end + end + end + + return BRAANATO + end + --- Return a BULLS string out of the BULLS of the coalition to the COORDINATE. -- @param #COORDINATE self -- @param DCS#coalition.side Coalition The coalition. @@ -25626,7 +28798,7 @@ do -- POINT_VEC3 -- @type POINT_VEC3 -- @field #number x The x coordinate in 3D space. -- @field #number y The y coordinate in 3D space. - -- @field #number z The z coordiante in 3D space. + -- @field #number z The z COORDINATE in 3D space. -- @field Utilities.Utils#SMOKECOLOR SmokeColor -- @field Utilities.Utils#FLARECOLOR FlareColor -- @field #POINT_VEC3.RoutePointAltType RoutePointAltType @@ -26260,6 +29432,7 @@ end--- **Core** - Informs the players using messages during a simulation. -- * Send message to all players. -- * Send messages to a coalition. -- * Send messages to a specific group. +-- * Send messages to a specific unit or client. -- -- === -- @@ -26285,6 +29458,7 @@ end--- **Core** - Informs the players using messages during a simulation. -- -- * To a @{Client} using @{#MESSAGE.ToClient}(). -- * To a @{Wrapper.Group} using @{#MESSAGE.ToGroup}() +-- * To a @{Wrapper.Unit} using @{#MESSAGE.ToUnit}() -- * To a coalition using @{#MESSAGE.ToCoalition}(). -- * To the red coalition using @{#MESSAGE.ToRed}(). -- * To the blue coalition using @{#MESSAGE.ToBlue}(). @@ -26449,11 +29623,14 @@ function MESSAGE:ToClient( Client, Settings ) self.MessageDuration = Settings:GetMessageTime( self.MessageType ) self.MessageCategory = "" -- self.MessageType .. ": " end - + + local Unit = Client:GetClient() + if self.MessageDuration ~= 0 then local ClientGroupID = Client:GetClientGroupID() self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) - trigger.action.outTextForGroup( ClientGroupID, self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration , self.ClearScreen) + --trigger.action.outTextForGroup( ClientGroupID, self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration , self.ClearScreen) + trigger.action.outTextForUnit( Unit:GetID(), self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration , self.ClearScreen) end end @@ -26483,6 +29660,31 @@ function MESSAGE:ToGroup( Group, Settings ) return self end + +--- Sends a MESSAGE to a Unit. +-- @param #MESSAGE self +-- @param Wrapper.Unit#UNIT Unit to which the message is displayed. +-- @return #MESSAGE Message object. +function MESSAGE:ToUnit( Unit, Settings ) + self:F( Unit.IdentifiableName ) + + if Unit then + + if self.MessageType then + local Settings = Settings or ( Unit and _DATABASE:GetPlayerSettings( Unit:GetPlayerName() ) ) or _SETTINGS -- Core.Settings#SETTINGS + self.MessageDuration = Settings:GetMessageTime( self.MessageType ) + self.MessageCategory = "" -- self.MessageType .. ": " + end + + if self.MessageDuration ~= 0 then + self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) + trigger.action.outTextForUnit( Unit:GetID(), self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration, self.ClearScreen ) + end + end + + return self +end + --- Sends a MESSAGE to the Blue coalition. -- @param #MESSAGE self -- @return #MESSAGE @@ -27021,7 +30223,7 @@ do -- FSM Transition.To = To -- Debug message. - self:T2( Transition ) + --self:T3( Transition ) self._Transitions[Transition] = Transition self:_eventmap( self.Events, Transition ) @@ -27043,7 +30245,7 @@ do -- FSM -- @param #table ReturnEvents A table indicating for which returned events of the SubFSM which Event must be triggered in the FSM. -- @return Core.Fsm#FSM_PROCESS The SubFSM. function FSM:AddProcess( From, Event, Process, ReturnEvents ) - self:T( { From, Event } ) + --self:T3( { From, Event } ) local Sub = {} Sub.From = From @@ -27144,7 +30346,7 @@ do -- FSM Process._Scores[State].ScoreText = ScoreText Process._Scores[State].Score = Score - self:T( Process._Scores ) + --self:T3( Process._Scores ) return Process end @@ -27187,7 +30389,7 @@ do -- FSM self[__Event] = self[__Event] or self:_delayed_transition(Event) -- Debug message. - self:T2( "Added methods: " .. Event .. ", " .. __Event ) + --self:T3( "Added methods: " .. Event .. ", " .. __Event ) Events[Event] = self.Events[Event] or { map = {} } self:_add_to_map( Events[Event].map, EventStructure ) @@ -27402,7 +30604,7 @@ do -- FSM return function( self, DelaySeconds, ... ) -- Debug. - self:T2( "Delayed Event: " .. EventName ) + self:T3( "Delayed Event: " .. EventName ) local CallID = 0 if DelaySeconds ~= nil then @@ -27420,23 +30622,23 @@ do -- FSM self._EventSchedules[EventName] = CallID -- Debug output. - self:T2(string.format("NEGATIVE Event %s delayed by %.1f sec SCHEDULED with CallID=%s", EventName, DelaySeconds, tostring(CallID))) + self:T2(string.format("NEGATIVE Event %s delayed by %.3f sec SCHEDULED with CallID=%s", EventName, DelaySeconds, tostring(CallID))) else - self:T2(string.format("NEGATIVE Event %s delayed by %.1f sec CANCELLED as we already have such an event in the queue.", EventName, DelaySeconds)) + self:T2(string.format("NEGATIVE Event %s delayed by %.3f sec CANCELLED as we already have such an event in the queue.", EventName, DelaySeconds)) -- reschedule end else CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1, nil, nil, nil, 4, true ) - self:T2(string.format("Event %s delayed by %.1f sec SCHEDULED with CallID=%s", EventName, DelaySeconds, tostring(CallID))) + self:T2(string.format("Event %s delayed by %.3f sec SCHEDULED with CallID=%s", EventName, DelaySeconds, tostring(CallID))) end else error( "FSM: An asynchronous event trigger requires a DelaySeconds parameter!!! This can be positive or negative! Sorry, but will not process this." ) end -- Debug. - self:T2( { CallID = CallID } ) + --self:T3( { CallID = CallID } ) end end @@ -27457,7 +30659,7 @@ do -- FSM function FSM:_gosub( ParentFrom, ParentEvent ) local fsmtable = {} if self.subs[ParentFrom] and self.subs[ParentFrom][ParentEvent] then - self:T( { ParentFrom, ParentEvent, self.subs[ParentFrom], self.subs[ParentFrom][ParentEvent] } ) + --self:T3( { ParentFrom, ParentEvent, self.subs[ParentFrom], self.subs[ParentFrom][ParentEvent] } ) return self.subs[ParentFrom][ParentEvent] else return {} @@ -27504,7 +30706,7 @@ do -- FSM end end - self:T3( { Map, Event } ) + --self:T3( { Map, Event } ) end --- Get current state. @@ -27761,7 +30963,7 @@ do -- FSM_PROCESS -- @param #FSM_PROCESS self -- @return #FSM_PROCESS function FSM_PROCESS:Copy( Controllable, Task ) - self:T( { self:GetClassNameAndID() } ) + --self:T3( { self:GetClassNameAndID() } ) local NewFsm = self:New( Controllable, Task ) -- Core.Fsm#FSM_PROCESS @@ -27787,13 +30989,13 @@ do -- FSM_PROCESS -- Copy End States for EndStateID, EndState in pairs( self:GetEndStates() ) do - self:T( EndState ) + --self:T3( EndState ) NewFsm:AddEndState( EndState ) end -- Copy the score tables for ScoreID, Score in pairs( self:GetScores() ) do - self:T( Score ) + --self:T3( Score ) NewFsm:AddScore( ScoreID, Score.ScoreText, Score.Score ) end @@ -28042,7 +31244,7 @@ do -- FSM_SET -- @param #FSM_SET self -- @return Core.Set#SET_BASE function FSM_SET:Get() - return self.Controllable + return self.Set end function FSM_SET:_call_handler( step, trigger, params, EventName ) @@ -28088,7 +31290,7 @@ end -- FSM_SET -- -- === -- --- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master-release/SPA%20-%20Spawning) +-- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/SPA%20-%20Spawning) -- -- === -- @@ -30481,8 +33683,7 @@ end -- @param #SPAWN self -- @param DCS#Vec3 Vec3 The Vec3 coordinates where to spawn the group. -- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. --- @return Wrapper.Group#GROUP that was spawned. --- @return #nil Nothing was spawned. +-- @return Wrapper.Group#GROUP that was spawned or #nil if nothing was spawned. function SPAWN:SpawnFromVec3( Vec3, SpawnIndex ) self:F( { self.SpawnTemplatePrefix, Vec3, SpawnIndex } ) @@ -30551,8 +33752,7 @@ end -- @param #SPAWN self -- @param Core.Point#Coordinate Coordinate The Coordinate coordinates where to spawn the group. -- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. --- @return Wrapper.Group#GROUP that was spawned. --- @return #nil Nothing was spawned. +-- @return Wrapper.Group#GROUP that was spawned or #nil if nothing was spawned. function SPAWN:SpawnFromCoordinate( Coordinate, SpawnIndex ) self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) @@ -30568,8 +33768,7 @@ end -- @param #SPAWN self -- @param Core.Point#POINT_VEC3 PointVec3 The PointVec3 coordinates where to spawn the group. -- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. --- @return Wrapper.Group#GROUP that was spawned. --- @return #nil Nothing was spawned. +-- @return Wrapper.Group#GROUP that was spawned or #nil if nothing was spawned. -- @usage -- -- local SpawnPointVec3 = ZONE:New( ZoneName ):GetPointVec3( 2000 ) -- Get the center of the ZONE object at 2000 meters from the ground. @@ -30593,8 +33792,7 @@ end -- @param #number MinHeight (optional) The minimum height to spawn an airborne group into the zone. -- @param #number MaxHeight (optional) The maximum height to spawn an airborne group into the zone. -- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. --- @return Wrapper.Group#GROUP that was spawned. --- @return #nil Nothing was spawned. +-- @return Wrapper.Group#GROUP that was spawned or #nil if nothing was spawned. -- @usage -- -- local SpawnVec2 = ZONE:New( ZoneName ):GetVec2() @@ -30627,8 +33825,7 @@ end -- @param #number MinHeight (optional) The minimum height to spawn an airborne group into the zone. -- @param #number MaxHeight (optional) The maximum height to spawn an airborne group into the zone. -- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. --- @return Wrapper.Group#GROUP that was spawned. --- @return #nil Nothing was spawned. +-- @return Wrapper.Group#GROUP that was spawned or #nil if nothing was spawned. -- @usage -- -- local SpawnPointVec2 = ZONE:New( ZoneName ):GetPointVec2() @@ -30684,8 +33881,7 @@ end -- @param #number MinHeight (optional) The minimum height to spawn an airborne group into the zone. -- @param #number MaxHeight (optional) The maximum height to spawn an airborne group into the zone. -- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. --- @return Wrapper.Group#GROUP that was spawned. --- @return #nil Nothing was spawned. +-- @return Wrapper.Group#GROUP that was spawned or #nil if nothing was spawned. -- @usage -- -- local SpawnStatic = STATIC:FindByName( StaticName ) @@ -30716,8 +33912,7 @@ end -- @param #number MinHeight (optional) The minimum height to spawn an airborne group into the zone. -- @param #number MaxHeight (optional) The maximum height to spawn an airborne group into the zone. -- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. --- @return Wrapper.Group#GROUP that was spawned. --- @return #nil when nothing was spawned. +-- @return Wrapper.Group#GROUP that was spawned or #nil if nothing was spawned. -- @usage -- -- local SpawnZone = ZONE:New( ZoneName ) @@ -30912,18 +34107,37 @@ end -- The method will search for a #-mark, and will return the text before the #-mark. -- It will return nil of no prefix was found. -- @param #SPAWN self --- @param DCS#UNIT DCSUnit The @{DCSUnit} to be searched. --- @return #string The prefix --- @return #nil Nothing found +-- @param Wrapper.Group#GROUP SpawnGroup The GROUP object. +-- @return #string The prefix or #nil if nothing was found. function SPAWN:_GetPrefixFromGroup( SpawnGroup ) - self:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnGroup } ) local GroupName = SpawnGroup:GetName() + if GroupName then - local SpawnPrefix = string.match( GroupName, ".*#" ) + + local SpawnPrefix=self:_GetPrefixFromGroupName(GroupName) + + return SpawnPrefix + end + + return nil +end + +--- Return the prefix of a spawned group. +-- The method will search for a `#`-mark, and will return the text before the `#`-mark. It will return nil of no prefix was found. +-- @param #SPAWN self +-- @param #string SpawnGroupName The name of the spawned group. +-- @return #string The prefix or #nil if nothing was found. +function SPAWN:_GetPrefixFromGroupName(SpawnGroupName) + + if SpawnGroupName then + + local SpawnPrefix=string.match(SpawnGroupName, ".*#") + if SpawnPrefix then - SpawnPrefix = SpawnPrefix:sub( 1, -2 ) + SpawnPrefix = SpawnPrefix:sub(1, -2) end + return SpawnPrefix end @@ -31337,19 +34551,25 @@ end -- @param Core.Event#EVENTDATA EventData function SPAWN:_OnDeadOrCrash( EventData ) self:F( self.SpawnTemplatePrefix ) - - local SpawnGroup = EventData.IniGroup - if SpawnGroup then - local EventPrefix = self:_GetPrefixFromGroup( SpawnGroup ) - if EventPrefix then -- EventPrefix can be nil if no # is found, which means, no spawnable group! + local unit=UNIT:FindByName(EventData.IniUnitName) + + if unit then + + local EventPrefix = self:_GetPrefixFromGroupName(unit.GroupName) + + if EventPrefix then -- EventPrefix can be nil if no # is found, which means, no spawnable group! self:T( { "Dead event: " .. EventPrefix } ) - if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then - self.AliveUnits = self.AliveUnits - 1 - self:T( "Alive Units: " .. self.AliveUnits ) - end + + if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then + + self.AliveUnits = self.AliveUnits - 1 + + self:T( "Alive Units: " .. self.AliveUnits ) + end + end - end + end end --- Will detect AIR Units taking off... When the event takes place, the spawned Group is registered as airborne... @@ -31552,14 +34772,14 @@ end -- @field #number InitOffsetAngle Link offset angle in degrees. -- @field #number InitStaticHeading Heading of the static. -- @field #string InitStaticLivery Livery for aircraft. --- @field #string InitStaticShape Shape of teh static. +-- @field #string InitStaticShape Shape of the static. -- @field #string InitStaticType Type of the static. -- @field #string InitStaticCategory Categrory of the static. -- @field #string InitStaticName Name of the static. -- @field Core.Point#COORDINATE InitStaticCoordinate Coordinate where to spawn the static. --- @field #boolean InitDead Set static to be dead if true. --- @field #boolean InitCargo If true, static can act as cargo. --- @field #number InitCargoMass Mass of cargo in kg. +-- @field #boolean InitStaticDead Set static to be dead if true. +-- @field #boolean InitStaticCargo If true, static can act as cargo. +-- @field #number InitStaticCargoMass Mass of cargo in kg. -- @extends Core.Base#BASE @@ -31766,7 +34986,7 @@ end -- @param #number Mass Mass of the cargo in kg. -- @return #SPAWNSTATIC self function SPAWNSTATIC:InitCargoMass(Mass) - self.InitCargoMass=Mass + self.InitStaticCargoMass=Mass return self end @@ -31775,7 +34995,16 @@ end -- @param #boolean IsCargo If true, this static can act as cargo. -- @return #SPAWNSTATIC self function SPAWNSTATIC:InitCargo(IsCargo) - self.InitCargo=IsCargo + self.InitStaticCargo=IsCargo + return self +end + +--- Initialize as dead. +-- @param #SPAWNSTATIC self +-- @param #boolean IsCargo If true, this static is dead. +-- @return #SPAWNSTATIC self +function SPAWNSTATIC:InitDead(IsDead) + self.InitStaticDead=IsDead return self end @@ -31923,16 +35152,16 @@ function SPAWNSTATIC:_SpawnStatic(Template, CountryID) Template.livery_id=self.InitStaticLivery end - if self.InitDead~=nil then - Template.dead=self.InitDead + if self.InitStaticDead~=nil then + Template.dead=self.InitStaticDead end - if self.InitCargo~=nil then - Template.canCargo=self.InitCargo + if self.InitStaticCargo~=nil then + Template.canCargo=self.InitStaticCargo end - if self.InitCargoMass~=nil then - Template.mass=self.InitCargoMass + if self.InitStaticCargoMass~=nil then + Template.mass=self.InitStaticCargoMass end if self.InitLinkUnit then @@ -31985,6 +35214,8 @@ function SPAWNSTATIC:_SpawnStatic(Template, CountryID) -- ED's dirty way to spawn FARPS. Static=coalition.addGroup(CountryID, -1, TemplateGroup) else + self:T("Spawning Static") + self:T2({Template=Template}) Static=coalition.addStaticObject(CountryID, Template) end @@ -32026,8 +35257,6 @@ end -- -- === -- --- ![Banner Image](..\Presentations\Timer\TIMER_Main.jpg) --- -- # The TIMER Concept -- -- The TIMER class is the little sister of the @{Core.Scheduler#SCHEDULER} class. It does the same thing but is a bit easier to use and has less overhead. It should be sufficient in many cases. @@ -32099,19 +35328,17 @@ TIMER = { --- Timer ID. _TIMERID=0 ---- Timer data base. ---_TIMERDB={} - --- TIMER class version. -- @field #string version -TIMER.version="0.1.1" +TIMER.version="0.1.2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: A lot. --- TODO: Write docs. +-- TODO: Randomization. +-- TODO: Pause/unpause. +-- DONE: Write docs. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor @@ -32148,9 +35375,6 @@ function TIMER:New(Function, ...) -- Log id. self.lid=string.format("TIMER UID=%d | ", self.uid) - -- Add to DB. - --_TIMERDB[self.uid]=self - return self end @@ -32166,7 +35390,7 @@ function TIMER:Start(Tstart, dT, Duration) local Tnow=timer.getTime() -- Start time in sec. - self.Tstart=Tstart and Tnow+Tstart or Tnow+0.001 -- one millisecond delay if Tstart=nil + self.Tstart=Tstart and Tnow+math.max(Tstart, 0.001) or Tnow+0.001 -- one millisecond delay if Tstart=nil -- Set time interval. self.dT=dT @@ -32211,10 +35435,7 @@ function TIMER:Stop(Delay) -- Not running any more. self.isrunning=false - - -- Remove DB entry. - --_TIMERDB[self.uid]=nil - + end end @@ -32231,6 +35452,15 @@ function TIMER:SetMaxFunctionCalls(Nmax) return self end +--- Set time interval. Can also be set when the timer is already running and is applied after the next function call. +-- @param #TIMER self +-- @param #number dT Time interval in seconds. +-- @return #TIMER self +function TIMER:SetTimeInterval(dT) + self.dT=dT + return self +end + --- Check if the timer has been started and was not stopped. -- @param #TIMER self -- @return #boolean If `true`, the timer is running. @@ -32848,6 +36078,8 @@ end--- **Core** - A* Pathfinding. -- === -- -- ### Author: **funkyfranky** +-- +-- === -- @module Core.Astar -- @image CORE_Astar.png @@ -32874,12 +36106,10 @@ end--- **Core** - A* Pathfinding. -- @field #table CostArg Optional arguments passed to the cost function. -- @extends Core.Base#BASE ---- **When nothing goes right... Go left!** +--- *When nothing goes right... Go left!* -- -- === -- --- ![Banner Image](..\Presentations\Astar\ASTAR_Main.jpg) --- -- # The ASTAR Concept -- -- Pathfinding algorithm. @@ -34029,7 +37259,503 @@ end -------------------------------------------------------------------------- -- MARKEROPS_BASE Class Definition End. -------------------------------------------------------------------------- ---- **Wrapper** -- OBJECT wraps the DCS Object derived objects. +--- **Core** - TEXTANDSOUND (MOOSE gettext) system +-- +-- === +-- +-- ## Main Features: +-- +-- * A GetText for Moose +-- * Build a set of localized text entries, alongside their sounds and subtitles +-- * Aimed at class developers to offer localizable language support +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/). +-- +-- === +-- +-- ### Author: **applevangelist** +-- ## Date: April 2022 +-- +-- === +-- +-- @module Core.TextAndSound +-- @image MOOSE.JPG + +--- Text and Sound class. +-- @type TEXTANDSOUND +-- @field #string ClassName Name of this class. +-- @field #string version Versioning. +-- @field #string lid LID for log entries. +-- @field #string locale Default locale of this object. +-- @field #table entries Table of entries. +-- @field #string textclass Name of the class the texts belong to. +-- @extends Core.Base#BASE + +--- +-- +-- @field #TEXTANDSOUND +TEXTANDSOUND = { + ClassName = "TEXTANDSOUND", + version = "0.0.1", + lid = "", + locale = "en", + entries = {}, + textclass = "", +} + +--- Text and Sound entry. +-- @type TEXTANDSOUND.Entry +-- @field #string Classname Name of the class this entry is for. +-- @field #string Locale Locale of this entry, defaults to "en". +-- @field #table Data The list of entries. + +--- Text and Sound data +-- @type TEXTANDSOUND.Data +-- @field #string ID ID of this entry for retrieval. +-- @field #string Text Text of this entry. +-- @field #string Soundfile (optional) Soundfile File name of the corresponding sound file. +-- @field #number Soundlength (optional) Length of the sound file in seconds. +-- @field #string Subtitle (optional) Subtitle for the sound file. + +--- Instantiate a new object +-- @param #TEXTANDSOUND self +-- @param #string ClassName Name of the class this instance is providing texts for. +-- @param #string Defaultlocale (Optional) Default locale of this instance, defaults to "en". +-- @return #TEXTANDSOUND self +function TEXTANDSOUND:New(ClassName,Defaultlocale) + -- Inherit everything from BASE class. + local self=BASE:Inherit(self, BASE:New()) + -- Set some string id for output to DCS.log file. + self.lid=string.format("%s (%s) | ", self.ClassName, self.version) + self.locale = Defaultlocale or (_SETTINGS:GetLocale() or "en") + self.textclass = ClassName or "none" + self.entries = {} + local initentry = {} -- #TEXTANDSOUND.Entry + initentry.Classname = ClassName + initentry.Data = {} + initentry.Locale = self.locale + self.entries[self.locale] = initentry + self:I(self.lid .. "Instantiated.") + self:T({self.entries[self.locale]}) + return self +end + +--- Add an entry +-- @param #TEXTANDSOUND self +-- @param #string Locale Locale to set for this entry, e.g. "de". +-- @param #string ID Unique(!) ID of this entry under this locale (i.e. use the same ID to get localized text for the entry in another language). +-- @param #string Text Text for this entry. +-- @param #string Soundfile (Optional) Sound file name for this entry. +-- @param #number Soundlength (Optional) Length of the sound file in seconds. +-- @param #string Subtitle (Optional) Subtitle to be used alongside the sound file. +-- @return #TEXTANDSOUND self +function TEXTANDSOUND:AddEntry(Locale,ID,Text,Soundfile,Soundlength,Subtitle) + self:T(self.lid .. "AddEntry") + local locale = Locale or self.locale + local dataentry = {} -- #TEXTANDSOUND.Data + dataentry.ID = ID or "1" + dataentry.Text = Text or "none" + dataentry.Soundfile = Soundfile + dataentry.Soundlength = Soundlength or 0 + dataentry.Subtitle = Subtitle + if not self.entries[locale] then + local initentry = {} -- #TEXTANDSOUND.Entry + initentry.Classname = self.textclass -- class name entry + initentry.Data = {} -- data array + initentry.Locale = locale -- default locale + self.entries[locale] = initentry + end + self.entries[locale].Data[ID] = dataentry + self:T({self.entries[locale].Data}) + return self +end + +--- Get an entry +-- @param #TEXTANDSOUND self +-- @param #string ID The unique ID of the data to be retrieved. +-- @param #string Locale (Optional) The locale of the text to be retrieved - defauls to default locale set with `New()`. +-- @return #string Text Text or nil if not found and no fallback. +-- @return #string Soundfile Filename or nil if not found and no fallback. +-- @return #string Soundlength Length of the sound or 0 if not found and no fallback. +-- @return #string Subtitle Text for subtitle or nil if not found and no fallback. +function TEXTANDSOUND:GetEntry(ID,Locale) + self:T(self.lid .. "GetEntry") + local locale = Locale or self.locale + if not self.entries[locale] then + -- fall back to default "en" + locale = self.locale + end + local Text,Soundfile,Soundlength,Subtitle = nil, nil, 0, nil + if self.entries[locale] then + if self.entries[locale].Data then + local data = self.entries[locale].Data[ID] -- #TEXTANDSOUND.Data + if data then + Text = data.Text + Soundfile = data.Soundfile + Soundlength = data.Soundlength + Subtitle = data.Subtitle + elseif self.entries[self.locale].Data[ID] then + -- no matching entry, try default + local data = self.entries[self.locale].Data[ID] + Text = data.Text + Soundfile = data.Soundfile + Soundlength = data.Soundlength + Subtitle = data.Subtitle + end + end + else + return nil, nil, 0, nil + end + return Text,Soundfile,Soundlength,Subtitle +end + +--- Get the default locale of this object +-- @param #TEXTANDSOUND self +-- @return #string locale +function TEXTANDSOUND:GetDefaultLocale() + self:T(self.lid .. "GetDefaultLocale") + return self.locale +end + +--- Set default locale of this object +-- @param #TEXTANDSOUND self +-- @param #string locale +-- @return #TEXTANDSOUND self +function TEXTANDSOUND:SetDefaultLocale(locale) + self:T(self.lid .. "SetDefaultLocale") + self.locale = locale or "en" + return self +end + +--- Check if a locale exists +-- @param #TEXTANDSOUND self +-- @return #boolean outcome +function TEXTANDSOUND:HasLocale(Locale) + self:T(self.lid .. "HasLocale") + return self.entries[Locale] and true or false +end + +--- Flush all entries to the log +-- @param #TEXTANDSOUND self +-- @return #TEXTANDSOUND self +function TEXTANDSOUND:FlushToLog() + self:I(self.lid .. "Flushing entries:") + local text = string.format("Textclass: %s | Default Locale: %s",self.textclass, self.locale) + for _,_entry in pairs(self.entries) do + local entry = _entry -- #TEXTANDSOUND.Entry + local text = string.format("Textclassname: %s | Locale: %s",entry.Classname, entry.Locale) + self:I(text) + for _ID,_data in pairs(entry.Data) do + local data = _data -- #TEXTANDSOUND.Data + local text = string.format("ID: %s\nText: %s\nSoundfile: %s With length: %d\nSubtitle: %s",tostring(_ID), data.Text or "none",data.Soundfile or "none",data.Soundlength or 0,data.Subtitle or "none") + self:I(text) + end + end + return self +end + +---------------------------------------------------------------- +-- End TextAndSound +---------------------------------------------------------------- +--- **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 +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- **Wrapper** -- OBJECT wraps the DCS Object derived objects. -- -- === -- @@ -34161,7 +37887,7 @@ IDENTIFIABLE = { local _CategoryName = { [Unit.Category.AIRPLANE] = "Airplane", - [Unit.Category.HELICOPTER] = "Helicoper", + [Unit.Category.HELICOPTER] = "Helicopter", [Unit.Category.GROUND_UNIT] = "Ground Identifiable", [Unit.Category.SHIP] = "Ship", [Unit.Category.STRUCTURE] = "Structure", @@ -34182,8 +37908,7 @@ end -- If the Identifiable is not alive, nil is returned. -- If the Identifiable is alive, true is returned. -- @param #IDENTIFIABLE self --- @return #boolean true if Identifiable is alive. --- @return #nil if the Identifiable is not existing or is not alive. +-- @return #boolean true if Identifiable is alive or `#nil` if the Identifiable is not existing or is not alive. function IDENTIFIABLE:IsAlive() self:F3( self.IdentifiableName ) @@ -34203,11 +37928,8 @@ end --- Returns DCS Identifiable object name. -- The function provides access to non-activated objects too. -- @param #IDENTIFIABLE self --- @return #string The name of the DCS Identifiable. --- @return #nil The DCS Identifiable is not existing or alive. +-- @return #string The name of the DCS Identifiable or `#nil`. function IDENTIFIABLE:GetName() - self:F2( self.IdentifiableName ) - local IdentifiableName = self.IdentifiableName return IdentifiableName end @@ -34274,8 +37996,7 @@ end --- Returns coalition of the Identifiable. -- @param #IDENTIFIABLE self --- @return DCS#coalition.side The side of the coalition. --- @return #nil The DCS Identifiable is not existing or alive. +-- @return DCS#coalition.side The side of the coalition or `#nil` The DCS Identifiable is not existing or alive. function IDENTIFIABLE:GetCoalition() self:F2( self.IdentifiableName ) @@ -34316,8 +38037,7 @@ end --- Returns country of the Identifiable. -- @param #IDENTIFIABLE self --- @return DCS#country.id The country identifier. --- @return #nil The DCS Identifiable is not existing or alive. +-- @return DCS#country.id The country identifier or `#nil` The DCS Identifiable is not existing or alive. function IDENTIFIABLE:GetCountry() self:F2( self.IdentifiableName ) @@ -34348,8 +38068,7 @@ end --- Returns Identifiable descriptor. Descriptor type depends on Identifiable category. -- @param #IDENTIFIABLE self --- @return DCS#Object.Desc The Identifiable descriptor. --- @return #nil The DCS Identifiable is not existing or alive. +-- @return DCS#Object.Desc The Identifiable descriptor or `#nil` The DCS Identifiable is not existing or alive. function IDENTIFIABLE:GetDesc() self:F2( self.IdentifiableName ) @@ -34368,8 +38087,7 @@ end --- Check if the Object has the attribute. -- @param #IDENTIFIABLE self -- @param #string AttributeName The attribute name. --- @return #boolean true if the attribute exists. --- @return #nil The DCS Identifiable is not existing or alive. +-- @return #boolean true if the attribute exists or `#nil` The DCS Identifiable is not existing or alive. function IDENTIFIABLE:HasAttribute( AttributeName ) self:F2( self.IdentifiableName ) @@ -34392,9 +38110,11 @@ function IDENTIFIABLE:GetCallsign() return '' end - +--- Gets the threat level. +-- @param #IDENTIFIABLE self +-- @return #number Threat level. +-- @return #string Type. function IDENTIFIABLE:GetThreatLevel() - return 0, "Scenery" end --- **Wrapper** -- POSITIONABLE wraps DCS classes that are "positionable". @@ -34775,7 +38495,8 @@ function POSITIONABLE:GetCoordinate() local PositionableVec3 = self:GetVec3() local coord=COORDINATE:NewFromVec3(PositionableVec3) - + local heading = self:GetHeading() + coord.Heading = heading -- Return a new coordiante object. return coord @@ -34930,7 +38651,7 @@ function POSITIONABLE:GetBoundingRadius(mindist) return nil end ---- Returns the altitude of the POSITIONABLE. +--- Returns the altitude above sea level of the POSITIONABLE. -- @param Wrapper.Positionable#POSITIONABLE self -- @return DCS#Distance The altitude of the POSITIONABLE. -- @return #nil The POSITIONABLE is not existing or alive. @@ -35173,7 +38894,7 @@ function POSITIONABLE:GetRelativeVelocity(positionable) end ---- Returns the POSITIONABLE height in meters. +--- Returns the POSITIONABLE height above sea level in meters. -- @param Wrapper.Positionable#POSITIONABLE self -- @return DCS#Vec3 The height of the positionable. -- @return #nil The POSITIONABLE is not existing or alive. @@ -35585,6 +39306,33 @@ function POSITIONABLE:MessageToGroup( Message, Duration, MessageGroup, Name ) return nil end +--- Send a message to a @{Wrapper.Unit}. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param DCS#Duration Duration The duration of the message. +-- @param Wrapper.Unit#UNIT MessageUnit The UNIT object receiving the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +function POSITIONABLE:MessageToUnit( Message, Duration, MessageUnit, Name ) + self:F2( { Message, Duration } ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + if DCSObject:isExist() then + if MessageUnit:IsAlive() then + self:GetMessage( Message, Duration, Name ):ToUnit( MessageUnit ) + else + BASE:E( { "Message not sent to Unit; Unit is not alive...", Message = Message, MessageUnit = MessageUnit } ) + end + else + BASE:E( { "Message not sent to Unit; Positionable is not alive ...", Message = Message, Positionable = self, MessageUnit = MessageUnit } ) + end + end + + + return nil +end + --- Send a message of a message type to a @{Wrapper.Group}. -- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. -- @param #POSITIONABLE self @@ -35629,6 +39377,30 @@ function POSITIONABLE:MessageToSetGroup( Message, Duration, MessageSetGroup, Nam return nil end +--- Send a message to a @{Core.Set#SET_UNIT}. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param DCS#Duration Duration The duration of the message. +-- @param Core.Set#SET_UNIT MessageSetUnit The SET_UNIT collection receiving the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +function POSITIONABLE:MessageToSetUnit( Message, Duration, MessageSetUnit, Name ) + self:F2( { Message, Duration } ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + if DCSObject:isExist() then + MessageSetUnit:ForEachUnit( + function( MessageGroup ) + self:GetMessage( Message, Duration, Name ):ToUnit( MessageGroup ) + end + ) + end + end + + return nil +end + --- Send a message to the players in the @{Wrapper.Group}. -- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. -- @param #POSITIONABLE self @@ -35767,8 +39539,6 @@ do -- Cargo return self.__.Cargo end - - --- Remove cargo. -- @param #POSITIONABLE self -- @param Core.Cargo#CARGO Cargo @@ -35813,17 +39583,15 @@ do -- Cargo return ItemCount end --- --- Get Cargo Bay Free Volume in m3. --- -- @param #POSITIONABLE self --- -- @return #number CargoBayFreeVolume --- function POSITIONABLE:GetCargoBayFreeVolume() --- local CargoVolume = 0 --- for CargoName, Cargo in pairs( self.__.Cargo ) do --- CargoVolume = CargoVolume + Cargo:GetVolume() --- end --- return self.__.CargoBayVolumeLimit - CargoVolume --- end --- + --- Get the number of infantry soldiers that can be embarked into an aircraft (airplane or helicopter). + -- Returns `nil` for ground or ship units. + -- @param #POSITIONABLE self + -- @return #number Descent number of soldiers that fit into the unit. Returns `#nil` for ground and ship units. + function POSITIONABLE:GetTroopCapacity() + local DCSunit=self:GetDCSObject() --DCS#Unit + local capacity=DCSunit:getDescentCapacity() + return capacity + end --- Get Cargo Bay Free Weight in kg. -- @param #POSITIONABLE self @@ -35842,55 +39610,97 @@ do -- Cargo return self.__.CargoBayWeightLimit - CargoWeight end --- --- Get Cargo Bay Volume Limit in m3. --- -- @param #POSITIONABLE self --- -- @param #number VolumeLimit --- function POSITIONABLE:SetCargoBayVolumeLimit( VolumeLimit ) --- self.__.CargoBayVolumeLimit = VolumeLimit --- end - --- Set Cargo Bay Weight Limit in kg. -- @param #POSITIONABLE self - -- @param #number WeightLimit + -- @param #number WeightLimit (Optional) Weight limit in kg. If not given, the value is taken from the descriptors or hard coded. function POSITIONABLE:SetCargoBayWeightLimit( WeightLimit ) - if WeightLimit then + if WeightLimit then + --- + -- User defined value + --- self.__.CargoBayWeightLimit = WeightLimit elseif self.__.CargoBayWeightLimit~=nil then -- Value already set ==> Do nothing! else - -- If weightlimit is not provided, we will calculate it depending on the type of unit. + --- + -- Weightlimit is not provided, we will calculate it depending on the type of unit. + --- + + -- Descriptors that contain the type name and for aircraft also weights. + local Desc = self:GetDesc() + self:F({Desc=Desc}) + + -- Unit type name. + local TypeName=Desc.typeName or "Unknown Type" -- When an airplane or helicopter, we calculate the weightlimit based on the descriptor. if self:IsAir() then - local Desc = self:GetDesc() - self:F({Desc=Desc}) + -- Max takeoff weight if DCS descriptors have unrealstic values. local Weights = { - ["C-17A"] = 35000, --77519 cannot be used, because it loads way too much apcs and infantry., - ["C-130"] = 22000 --The real value cannot be used, because it loads way too much apcs and infantry., + -- C-17A + -- Wiki says: max=265,352, empty=128,140, payload=77,516 (134 troops, 1 M1 Abrams tank, 2 M2 Bradley or 3 Stryker) + -- DCS says: max=265,350, empty=125,645, fuel=132,405 ==> Cargo Bay=7300 kg with a full fuel load (lot of fuel!) and 73300 with half a fuel load. + --["C-17A"] = 35000, --77519 cannot be used, because it loads way too much apcs and infantry. + -- C-130: + -- DCS says: max=79,380, empty=36,400, fuel=10,415 kg ==> Cargo Bay=32,565 kg with fuel load. + -- Wiki says: max=70,307, empty=34,382, payload=19,000 kg (92 passengers, 2-3 Humvees or 2 M113s), max takeoff weight 70,037 kg. + -- Here we say two M113s should be transported. Each one weights 11,253 kg according to DCS. So the cargo weight should be 23,000 kg with a full load of fuel. + -- This results in a max takeoff weight of 69,815 kg (23,000+10,415+36,400), which is very close to the Wiki value of 70,037 kg. + ["C-130"] = 70000, } - - self.__.CargoBayWeightLimit = Weights[Desc.typeName] or ( Desc.massMax - ( Desc.massEmpty + Desc.fuelMassMax ) ) + + -- Max (takeoff) weight (empty+fuel+cargo weight). + local massMax= Desc.massMax or 0 + + -- Adjust value if set above. + local maxTakeoff=Weights[TypeName] + if maxTakeoff then + massMax=maxTakeoff + end + + -- Empty weight. + local massEmpty=Desc.massEmpty or 0 + + -- Fuel. The descriptor provides the max fuel mass in kg. This needs to be multiplied by the relative fuel amount to calculate the actual fuel mass on board. + local massFuelMax=Desc.fuelMassMax or 0 + local relFuel=math.min(self:GetFuel() or 1.0, 1.0) -- We take 1.0 as max in case of external fuel tanks. + local massFuel=massFuelMax*relFuel + + -- Number of soldiers according to DCS function + --local troopcapacity=self:GetTroopCapacity() or 0 + + -- Calculate max cargo weight, which is the max (takeoff) weight minus the empty weight minus the actual fuel weight. + local CargoWeight=massMax-(massEmpty+massFuel) + + -- Debug info. + self:T(string.format("Setting Cargo bay weight limit [%s]=%d kg (Mass max=%d, empty=%d, fuelMax=%d kg (rel=%.3f), fuel=%d kg", TypeName, CargoWeight, massMax, massEmpty, massFuelMax, relFuel, massFuel)) + --self:T(string.format("Descent Troop Capacity=%d ==> %d kg (for 95 kg soldier)", troopcapacity, troopcapacity*95)) + + -- Set value. + self.__.CargoBayWeightLimit = CargoWeight + elseif self:IsShip() then - local Desc = self:GetDesc() - self:F({Desc=Desc}) + -- Hard coded cargo weights in kg. local Weights = { - ["Type_071"] = 245000, - ["LHA_Tarawa"] = 500000, - ["Ropucha-class"] = 150000, - ["Dry-cargo ship-1"] = 70000, - ["Dry-cargo ship-2"] = 70000, - ["Higgins_boat"] = 3700, -- Higgins Boat can load 3700 kg of general cargo or 36 men (source wikipedia). - ["USS_Samuel_Chase"] = 25000, -- Let's say 25 tons for now. Wiki says 33 Higgins boats, which would be 264 tons (can't be right!) and/or 578 troops. - ["LST_Mk2"] =2100000, -- Can carry 2100 tons according to wiki source! + ["Type_071"] = 245000, + ["LHA_Tarawa"] = 500000, + ["Ropucha-class"] = 150000, + ["Dry-cargo ship-1"] = 70000, + ["Dry-cargo ship-2"] = 70000, + ["Higgins_boat"] = 3700, -- Higgins Boat can load 3700 kg of general cargo or 36 men (source wikipedia). + ["USS_Samuel_Chase"] = 25000, -- Let's say 25 tons for now. Wiki says 33 Higgins boats, which would be 264 tons (can't be right!) and/or 578 troops. + ["LST_Mk2"] = 2100000, -- Can carry 2100 tons according to wiki source! + ["speedboat"] = 500, -- 500 kg ~ 5 persons + ["Seawise_Giant"] =261000000, -- Gross tonnage is 261,000 tonns. } - self.__.CargoBayWeightLimit = ( Weights[Desc.typeName] or 50000 ) + self.__.CargoBayWeightLimit = ( Weights[TypeName] or 50000 ) else - local Desc = self:GetDesc() + -- Hard coded number of soldiers. local Weights = { ["AAV7"] = 25, ["Bedford_MWD"] = 8, -- new by kappa @@ -35926,7 +39736,7 @@ do -- Cargo ["KrAZ6322"] = 12, ["M 818"] = 12, ["Tigr_233036"] = 6, - ["TPZ"] = 10, + ["TPZ"] = 10, -- Fuchs ["UAZ-469"] = 4, -- new by kappa ["Ural-375"] = 12, ["Ural-4320-31"] = 14, @@ -35934,14 +39744,34 @@ do -- Cargo ["Ural-4320T"] = 14, ["ZBD04A"] = 7, -- new by kappa ["VAB_Mephisto"] = 8, -- new by Apple + ["tt_KORD"] = 6, -- 2.7.1 HL/TT + ["tt_DSHK"] = 6, + ["HL_KORD"] = 6, + ["HL_DSHK"] = 6, } - local CargoBayWeightLimit = ( Weights[Desc.typeName] or 0 ) * 95 + -- Assuming that each passenger weighs 95 kg on average. + local CargoBayWeightLimit = ( Weights[TypeName] or 0 ) * 95 + self.__.CargoBayWeightLimit = CargoBayWeightLimit end end + self:F({CargoBayWeightLimit = self.__.CargoBayWeightLimit}) end + + --- Get Cargo Bay Weight Limit in kg. + -- @param #POSITIONABLE self + -- @return #number Max cargo weight in kg. + function POSITIONABLE:GetCargoBayWeightLimit() + + if self.__.CargoBayWeightLimit==nil then + self:SetCargoBayWeightLimit() + end + + return self.__.CargoBayWeightLimit + end + end --- Cargo --- Signal a flare at the position of the POSITIONABLE. @@ -36751,7 +40581,7 @@ end --- Activate ICLS system of the CONTROLLABLE. The controllable should be an aircraft carrier! -- @param #CONTROLLABLE self -- @param #number Channel ICLS channel. --- @param #number UnitID The ID of the unit the ICLS system is attached to. Useful if more units are in one group. +-- @param #number UnitID The DCS UNIT ID of the unit the ICLS system is attached to. Useful if more units are in one group. -- @param #string Callsign Morse code identification callsign. -- @param #number Delay (Optional) Delay in seconds before the ICLS is deactivated. -- @return #CONTROLLABLE self @@ -36777,6 +40607,33 @@ function CONTROLLABLE:CommandActivateICLS(Channel, UnitID, Callsign, Delay) return self end +--- Activate LINK4 system of the CONTROLLABLE. The controllable should be an aircraft carrier! +-- @param #CONTROLLABLE self +-- @param #number Frequency Link4 Frequency in MHz, e.g. 336 +-- @param #number UnitID The DCS UNIT ID of the unit the LINK4 system is attached to. Useful if more units are in one group. +-- @param #string Callsign Morse code identification callsign. +-- @param #number Delay (Optional) Delay in seconds before the LINK4 is deactivated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandActivateLink4(Frequency, UnitID, Callsign, Delay) + + -- Command to activate Link4 system. + local CommandActivateLink4= { + id = "ActivateLink4", + params= { + ["frequency "] = Frequency*1000, + ["unitId"] = UnitID, + ["name"] = Callsign, + } + } + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandActivateLink4, {self}, Delay) + else + self:SetCommand(CommandActivateLink4) + end + + return self +end --- Deactivate the active beacon of the CONTROLLABLE. -- @param #CONTROLLABLE self @@ -36788,7 +40645,7 @@ function CONTROLLABLE:CommandDeactivateBeacon(Delay) local CommandDeactivateBeacon={id='DeactivateBeacon', params={}} if Delay and Delay>0 then - SCHEDULER:New(nil, self.CommandActivateBeacon, {self}, Delay) + SCHEDULER:New(nil, self.CommandDeactivateBeacon, {self}, Delay) else self:SetCommand(CommandDeactivateBeacon) end @@ -36796,6 +40653,24 @@ function CONTROLLABLE:CommandDeactivateBeacon(Delay) return self end +--- Deactivate the active Link4 of the CONTROLLABLE. +-- @param #CONTROLLABLE self +-- @param #number Delay (Optional) Delay in seconds before the Link4 is deactivated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandDeactivateLink4(Delay) + + -- Command to deactivate + local CommandDeactivateLink4={id='DeactivateLink4', params={}} + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandDeactivateLink4, {self}, Delay) + else + self:SetCommand(CommandDeactivateLink4) + end + + return self +end + --- Deactivate the ICLS of the CONTROLLABLE. -- @param #CONTROLLABLE self -- @param #number Delay (Optional) Delay in seconds before the ICLS is deactivated. @@ -37477,7 +41352,7 @@ function CONTROLLABLE:TaskEscort( FollowControllable, Vec3, LastWaypointIndex, E DCSTask = { id = 'Escort', params = { - groupId = FollowControllable:GetID(), + groupId = FollowControllable and FollowControllable:GetID() or nil, pos = Vec3, lastWptIndexFlag = LastWaypointIndex and true or false, lastWptIndex = LastWaypointIndex, @@ -37507,11 +41382,11 @@ function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount, WeaponType, Alti id = 'FireAtPoint', params = { point = Vec2, - x=Vec2.x, - y=Vec2.y, + x = Vec2.x, + y = Vec2.y, zoneRadius = Radius, radius = Radius, - expendQty = 100, -- dummy value + expendQty = 1, -- dummy value expendQtyEnabled = false, alt_type = ASL and 0 or 1 } @@ -37530,7 +41405,8 @@ function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount, WeaponType, Alti DCSTask.params.weaponType=WeaponType end - --self:I(DCSTask) + --env.info("FF fireatpoint") + --BASE:I(DCSTask) return DCSTask end @@ -37626,6 +41502,27 @@ function CONTROLLABLE:EnRouteTaskEngageTargetsInZone( Vec2, Radius, TargetTypes, return DCSTask end +--- (AIR) Enroute anti-ship task. +-- @param #CONTROLLABLE self +-- @param DCS#AttributeNameArray TargetTypes Array of target categories allowed to engage. Default `{"Ships"}`. +-- @param #number Priority (Optional) All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. Default 0. +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:EnRouteTaskAntiShip(TargetTypes, Priority) + + local DCSTask = { + id = 'EngageTargets', + key = "AntiShip", + --auto = false, + --enabled = true, + params = { + targetTypes = TargetTypes or {"Ships"}, + priority = Priority or 0 + } + } + + return DCSTask +end + --- (AIR) Engaging a controllable. The task does not assign the target controllable to the unit/controllable to attack now; it just allows the unit/controllable to engage the target controllable as well as other assigned targets. -- @param #CONTROLLABLE self @@ -38092,22 +41989,20 @@ do -- Patrol methods end ---- Return a Misson task to follow a given route defined by Points. +--- Return a "Misson" task to follow a given route defined by Points. -- @param #CONTROLLABLE self -- @param #table Points A table of route points. --- @return DCS#Task +-- @return DCS#Task DCS mission task. Has entries `.id="Mission"`, `params`, were params has entries `airborne` and `route`, which is a table of `points`. function CONTROLLABLE:TaskRoute( Points ) - self:F2( Points ) local DCSTask = { id = 'Mission', params = { - airborne = self:IsAir(), + airborne = self:IsAir(), -- This is important to make aircraft land without respawning them (which was a long standing DCS issue). route = {points = Points}, }, } - - self:T3( { DCSTask } ) + return DCSTask end @@ -39915,7 +43810,48 @@ function POSITIONABLE:IsSubmarine() return nil end ---- **Wrapper** -- GROUP wraps the DCS Class Group objects. + + +--- Sets the controlled group to go at the specified speed in meters per second. +-- @param #CONTROLLABLE self +-- @param #number Speed Speed in meters per second +-- @param #boolean Keep (Optional) When set to true, will maintain the speed on passing waypoints. If not present or false, the controlled group will return to the speed as defined by their route. +-- @return #CONTROLLABLE self +function CONTROLLABLE:SetSpeed(Speed, Keep) + self:F2( { self.ControllableName } ) + -- Set default if not specified. + local speed = Speed or 5 + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + if Controller then + Controller:setSpeed(speed, Keep) + end + end + return self +end + +--- [AIR] Sets the controlled aircraft group to fly at the specified altitude in meters. +-- @param #CONTROLLABLE self +-- @param #number Altitude Altitude in meters. +-- @param #boolean Keep (Optional) When set to true, will maintain the altitude on passing waypoints. If not present or false, the controlled group will return to the altitude as defined by their route. +-- @param #string AltType (Optional) Specifies the altitude type used. If nil, the altitude type of the current waypoint will be used. Accepted values are "BARO" and "RADIO". +-- @return #CONTROLLABLE self +function CONTROLLABLE:SetAltitude(Altitude, Keep, AltType) + self:F2( { self.ControllableName } ) + -- Set default if not specified. + local altitude = Altitude or 1000 + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + if Controller then + if self:IsAir() then + Controller:setAltitude(altitude, Keep, AltType) + end + end + end + return self +end--- **Wrapper** -- GROUP wraps the DCS Class Group objects. -- -- === -- @@ -40104,6 +44040,7 @@ GROUPTEMPLATE.Takeoff = { -- @field #string GROUND_APC Infantry carriers, in particular Amoured Personell Carrier. This can be used to transport other assets. -- @field #string GROUND_TRUCK Unarmed ground vehicles, which has the DCS "Truck" attribute. -- @field #string GROUND_INFANTRY Ground infantry assets. +-- @field #string GROUND_IFV Ground Infantry Fighting Vehicle. -- @field #string GROUND_ARTILLERY Artillery assets. -- @field #string GROUND_TANK Tanks (modern or old). -- @field #string GROUND_TRAIN Trains. Not that trains are **not** yet properly implemented in DCS and cannot be used currently. @@ -40130,6 +44067,7 @@ GROUP.Attribute = { GROUND_APC="Ground_APC", GROUND_TRUCK="Ground_Truck", GROUND_INFANTRY="Ground_Infantry", + GROUND_IFV="Ground_IFV", GROUND_ARTILLERY="Ground_Artillery", GROUND_TANK="Ground_Tank", GROUND_TRAIN="Ground_Train", @@ -40227,8 +44165,7 @@ end --- Returns the @{DCS#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission. -- @param Wrapper.Positionable#POSITIONABLE self --- @return DCS#Position The 3D position vectors of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. +-- @return DCS#Position The 3D position vectors of the POSITIONABLE or #nil if the groups not existing or alive. function GROUP:GetPositionVec3() -- Overridden from POSITIONABLE:GetPositionVec3() self:F2( self.PositionableName ) @@ -40256,9 +44193,7 @@ end -- If the first @{Wrapper.Unit} of the group is inactive, it will return false. -- -- @param #GROUP self --- @return #boolean true if the group is alive and active. --- @return #boolean false if the group is alive but inactive. --- @return #nil if the group does not exist anymore. +-- @return #boolean `true` if the group is alive *and* active, `false` if the group is alive but inactive or `#nil` if the group does not exist anymore. function GROUP:IsAlive() self:F2( self.GroupName ) @@ -40280,8 +44215,7 @@ end --- Returns if the group is activated. -- @param #GROUP self --- @return #boolean true if group is activated. --- @return #nil The group is not existing or alive. +-- @return #boolean `true` if group is activated or `#nil` The group is not existing or alive. function GROUP:IsActive() self:F2( self.GroupName ) @@ -40305,7 +44239,7 @@ end -- So all event listeners will catch the destroy event of this group for each unit in the group. -- To raise these events, provide the `GenerateEvent` parameter. -- @param #GROUP self --- @param #boolean GenerateEvent If true, a crash or dead event for each unit is generated. If false, if no event is triggered. If nil, a RemoveUnit event is triggered. +-- @param #boolean GenerateEvent If true, a crash [AIR] or dead [GROUND] event for each unit is generated. If false, if no event is triggered. If nil, a RemoveUnit event is triggered. -- @param #number delay Delay in seconds before despawning the group. -- @usage -- -- Air unit example: destroy the Helicopter and generate a S_EVENT_CRASH for each unit in the Helicopter group. @@ -40329,7 +44263,6 @@ function GROUP:Destroy( GenerateEvent, delay ) self:F2( self.GroupName ) if delay and delay>0 then - --SCHEDULER:New(nil, GROUP.Destroy, {self, GenerateEvent}, delay) self:ScheduleOnce(delay, GROUP.Destroy, self, GenerateEvent) else @@ -40451,26 +44384,32 @@ function GROUP:HasAttribute(attribute, all) -- Get all units of the group. local _units=self:GetUnits() - local _allhave=true - local _onehas=false + if _units then - for _,_unit in pairs(_units) do - local _unit=_unit --Wrapper.Unit#UNIT - if _unit then - local _hastit=_unit:HasAttribute(attribute) - if _hastit==true then - _onehas=true - else - _allhave=false - end - end + local _allhave=true + local _onehas=false + + for _,_unit in pairs(_units) do + local _unit=_unit --Wrapper.Unit#UNIT + if _unit then + local _hastit=_unit:HasAttribute(attribute) + if _hastit==true then + _onehas=true + else + _allhave=false + end + end + end + + if all==true then + return _allhave + else + return _onehas + end + end - if all==true then - return _allhave - else - return _onehas - end + return nil end --- Returns the maximum speed of the group. @@ -40489,12 +44428,15 @@ function GROUP:GetSpeedMax() for _,unit in pairs(Units) do local unit=unit --Wrapper.Unit#UNIT + local speed=unit:GetSpeedMax() - if speedmax==nil then - speedmax=speed - elseif speedmaxtl then + maxtl=tl + threat=unit + end + end + end + + return threat, maxtl + end + + return nil, nil +end + --do -- Smoke -- ----- Signal a flare at the position of the GROUP. @@ -42645,6 +46697,7 @@ end --- @type UNIT -- @field #string ClassName Name of the class. -- @field #string UnitName Name of the unit. +-- @field #string GroupName Name of the group the unit belongs to. -- @extends Wrapper.Controllable#CONTROLLABLE --- For each DCS Unit object alive within a running mission, a UNIT wrapper object (instance) will be created within the _@{DATABASE} object. @@ -42707,10 +46760,11 @@ end -- * Use the @{#UNIT.IsLOS}() method to check if the given unit is within line of sight. -- -- --- @field #UNIT UNIT +-- @field #UNIT UNIT = { ClassName="UNIT", UnitName=nil, + GroupName=nil, } @@ -42731,11 +46785,20 @@ UNIT = { function UNIT:Register( UnitName ) -- Inherit CONTROLLABLE. - local self = BASE:Inherit( self, CONTROLLABLE:New( UnitName ) ) + local self = BASE:Inherit( self, CONTROLLABLE:New( UnitName ) ) --#UNIT -- Set unit name. self.UnitName = UnitName + local unit=Unit.getByName(self.UnitName) + + if unit then + local group = unit:getGroup() + if group then + self.GroupName=group:getName() + end + end + -- Set event prio. self:SetEventPriority( 3 ) @@ -42789,8 +46852,28 @@ function UNIT:GetDCSObject() return nil end +--- Returns the unit altitude above sea level in meters. +-- @param Wrapper.Unit#UNIT self +-- @param #boolean FromGround Measure from the ground or from sea level (ASL). Provide **true** for measuring from the ground (AGL). **false** or **nil** if you measure from sea level. +-- @return #number The height of the group or nil if is not existing or alive. +function UNIT:GetAltitude(FromGround) + + local DCSUnit = Unit.getByName( self.UnitName ) + if DCSUnit then + local altitude = 0 + local point = DCSUnit:getPoint() --DCS#Vec3 + altitude = point.y + if FromGround then + local land = land.getHeight( { x = point.x, y = point.z } ) or 0 + altitude = altitude - land + end + return altitude + end + return nil + +end --- Respawn the @{Wrapper.Unit} using a (tweaked) template of the parent Group. -- @@ -43029,6 +47112,17 @@ function UNIT:GetClient() return nil end +--- [AIRPLANE] Get the NATO reporting name of a UNIT. Currently airplanes only! +--@param #UNIT self +--@return #string NatoReportingName or "Bogey" if unknown. +function UNIT:GetNatoReportingName() + + local typename = self:GetTypeName() + return UTILS.GetReportingName(typename) + +end + + --- Returns the unit's number in the group. -- The number is the same number the unit has in ME. -- It may not be changed during the mission. @@ -43063,7 +47157,7 @@ function UNIT:GetSpeedMax() return SpeedMax*3.6 end - return nil + return 0 end --- Returns the unit's max range in meters derived from the DCS descriptors. @@ -43145,6 +47239,63 @@ function UNIT:IsTanker() return tanker, system end +--- Check if the unit can supply ammo. Currently, we have +-- +-- * M 818 +-- * Ural-375 +-- * ZIL-135 +-- +-- This list needs to be extended, if DCS adds other units capable of supplying ammo. +-- +-- @param #UNIT self +-- @return #boolean If `true`, unit can supply ammo. +function UNIT:IsAmmoSupply() + + -- Type name is the only thing we can check. There is no attribute (Sep. 2021) which would tell us. + local typename=self:GetTypeName() + + if typename=="M 818" then + -- Blue ammo truck. + return true + elseif typename=="Ural-375" then + -- Red ammo truck. + return true + elseif typename=="ZIL-135" then + -- Red ammo truck. Checked that it can also provide ammo. + return true + end + + return false +end + +--- Check if the unit can supply fuel. Currently, we have +-- +-- * M978 HEMTT Tanker +-- * ATMZ-5 +-- * ATMZ-10 +-- * ATZ-5 +-- +-- This list needs to be extended, if DCS adds other units capable of supplying fuel. +-- +-- @param #UNIT self +-- @return #boolean If `true`, unit can supply fuel. +function UNIT:IsFuelSupply() + + -- Type name is the only thing we can check. There is no attribute (Sep. 2021) which would tell us. + local typename=self:GetTypeName() + + if typename=="M978 HEMTT Tanker" then + return true + elseif typename=="ATMZ-5" then + return true + elseif typename=="ATMZ-10" then + return true + elseif typename=="ATZ-5" then + return true + end + + return false +end --- Returns the unit's group if it exist and nil otherwise. -- @param Wrapper.Unit#UNIT self @@ -43247,8 +47398,8 @@ function UNIT:GetAmmunition() -- Type name of current weapon. local Tammo=ammotable[w]["desc"]["typeName"] - local _weaponString = UTILS.Split(Tammo,"%.") - local _weaponName = _weaponString[#_weaponString] + --local _weaponString = UTILS.Split(Tammo,"%.") + --local _weaponName = _weaponString[#_weaponString] -- Get the weapon category: shell=0, missile=1, rocket=2, bomb=3 local Category=ammotable[w].desc.category @@ -43276,8 +47427,9 @@ function UNIT:GetAmmunition() nbombs=nbombs+Nammo elseif Category==Weapon.Category.MISSILE then - - -- Add up all cruise missiles (category 5) + + + -- Add up all missiles (category 5) if MissileCategory==Weapon.MissileCategory.AAM then nmissiles=nmissiles+Nammo elseif MissileCategory==Weapon.MissileCategory.ANTI_SHIP then @@ -43286,6 +47438,10 @@ function UNIT:GetAmmunition() nmissiles=nmissiles+Nammo elseif MissileCategory==Weapon.MissileCategory.OTHER then nmissiles=nmissiles+Nammo + elseif MissileCategory==Weapon.MissileCategory.SAM then + nmissiles=nmissiles+Nammo + elseif MissileCategory==Weapon.MissileCategory.CRUISE then + nmissiles=nmissiles+Nammo end end @@ -43299,8 +47455,6 @@ function UNIT:GetAmmunition() return nammo, nshells, nrockets, nbombs, nmissiles end - - --- Returns the unit sensors. -- @param #UNIT self -- @return DCS#Unit.Sensors Table of sensors. @@ -43617,7 +47771,7 @@ function UNIT:GetThreatLevel() elseif ( Attributes["Tanks"] or Attributes["IFV"] ) and not Attributes["ATGM"] then ThreatLevel = 3 elseif Attributes["Old Tanks"] or Attributes["APC"] or Attributes["Artillery"] then ThreatLevel = 2 - elseif Attributes["Infantry"] then ThreatLevel = 1 + elseif Attributes["Infantry"] or Attributes["EWR"] then ThreatLevel = 1 end ThreatText = ThreatLevels[ThreatLevel+1] @@ -44072,6 +48226,17 @@ end --- The CLIENT class -- @type CLIENT +-- @field #string ClassName Name of the class. +-- @field #string ClientName Name of the client. +-- @field #string ClientBriefing Briefing. +-- @field #function ClientCallBack Callback function. +-- @field #table ClientParameters Parameters of the callback function. +-- @field #number ClientGroupID Group ID of the client. +-- @field #string ClientGroupName Group name. +-- @field #boolean ClientAlive Client alive. +-- @field #boolean ClientAlive2 Client alive 2. +-- @field #table Players Player table. +-- @field Core.Point#COORDINATE SpawnCoord Spawn coordinate from the template. -- @extends Wrapper.Unit#UNIT @@ -44896,9 +49061,11 @@ end -- @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: @@ -44940,7 +49107,6 @@ AIRBASE = { [Airbase.Category.HELIPAD] = "Helipad", [Airbase.Category.SHIP] = "Ship", }, - activerwyno=nil, } --- Enumeration to identify the airbases in the Caucasus region. @@ -45008,31 +49174,31 @@ AIRBASE.Caucasus = { -- * AIRBASE.Nevada.Laughlin_Airport -- * AIRBASE.Nevada.Lincoln_County -- * AIRBASE.Nevada.Mesquite --- * AIRBASE.Nevada.Mina_Airport_3Q0 +-- * AIRBASE.Nevada.Mina_Airport -- * AIRBASE.Nevada.North_Las_Vegas -- * AIRBASE.Nevada.Pahute_Mesa_Airstrip -- * AIRBASE.Nevada.Tonopah_Airport -- * AIRBASE.Nevada.Tonopah_Test_Range_Airfield --- +-- -- @field Nevada AIRBASE.Nevada = { - ["Creech_AFB"] = "Creech AFB", - ["Groom_Lake_AFB"] = "Groom Lake AFB", - ["McCarran_International_Airport"] = "McCarran International Airport", - ["Nellis_AFB"] = "Nellis AFB", - ["Beatty_Airport"] = "Beatty Airport", - ["Boulder_City_Airport"] = "Boulder City Airport", + ["Creech_AFB"] = "Creech", + ["Groom_Lake_AFB"] = "Groom Lake", + ["McCarran_International_Airport"] = "McCarran International", + ["Nellis_AFB"] = "Nellis", + ["Beatty_Airport"] = "Beatty", + ["Boulder_City_Airport"] = "Boulder City", ["Echo_Bay"] = "Echo Bay", - ["Henderson_Executive_Airport"] = "Henderson Executive Airport", - ["Jean_Airport"] = "Jean Airport", - ["Laughlin_Airport"] = "Laughlin Airport", + ["Henderson_Executive_Airport"] = "Henderson Executive", + ["Jean_Airport"] = "Jean", + ["Laughlin_Airport"] = "Laughlin", ["Lincoln_County"] = "Lincoln County", ["Mesquite"] = "Mesquite", - ["Mina_Airport_3Q0"] = "Mina Airport 3Q0", + ["Mina_Airport"] = "Mina", ["North_Las_Vegas"] = "North Las Vegas", - ["Pahute_Mesa_Airstrip"] = "Pahute Mesa Airstrip", - ["Tonopah_Airport"] = "Tonopah Airport", - ["Tonopah_Test_Range_Airfield"] = "Tonopah Test Range Airfield", + ["Pahute_Mesa_Airstrip"] = "Pahute Mesa", + ["Tonopah_Airport"] = "Tonopah", + ["Tonopah_Test_Range_Airfield"] = "Tonopah Test Range", } --- Airbases of the Normandy map: @@ -45068,7 +49234,7 @@ AIRBASE.Nevada = { -- * AIRBASE.Normandy.Funtington -- * AIRBASE.Normandy.Tangmere -- * AIRBASE.Normandy.Ford_AF --- +-- -- @field Normandy AIRBASE.Normandy = { ["Saint_Pierre_du_Mont"] = "Saint Pierre du Mont", @@ -45142,7 +49308,7 @@ AIRBASE.Normandy = { -- * AIRBASE.PersianGulf.Sirri_Island -- * AIRBASE.PersianGulf.Tunb_Island_AFB -- * AIRBASE.PersianGulf.Tunb_Kochak --- +-- -- @field PersianGulf AIRBASE.PersianGulf = { ["Abu_Dhabi_International_Airport"] = "Abu Dhabi Intl", @@ -45187,6 +49353,9 @@ AIRBASE.PersianGulf = { -- * AIRBASE.TheChannel.Lympne -- * AIRBASE.TheChannel.Detling -- * AIRBASE.TheChannel.High_Halden +-- * AIRBASE.TheChannel.Biggin_Hill +-- * AIRBASE.TheChannel.Eastchurch +-- * AIRBASE.TheChannel.Headcorn -- -- @field TheChannel AIRBASE.TheChannel = { @@ -45199,6 +49368,9 @@ AIRBASE.TheChannel = { ["Lympne"] = "Lympne", ["Detling"] = "Detling", ["High_Halden"] = "High Halden", + ["Biggin_Hill"] = "Biggin Hill", + ["Eastchurch"] = "Eastchurch", + ["Headcorn"] = "Headcorn", } --- Airbases of the Syria map: @@ -45217,7 +49389,6 @@ AIRBASE.TheChannel = { -- * AIRBASE.Syria.Wujah_Al_Hajar -- * AIRBASE.Syria.Al_Dumayr -- * AIRBASE.Syria.Gazipasa --- * AIRBASE.Syria.Ru_Convoy_4 -- * AIRBASE.Syria.Hatay -- * AIRBASE.Syria.Nicosia -- * AIRBASE.Syria.Pinarbashi @@ -45235,7 +49406,6 @@ AIRBASE.TheChannel = { -- * AIRBASE.Syria.Akrotiri -- * AIRBASE.Syria.Naqoura -- * AIRBASE.Syria.Gaziantep --- * AIRBASE.Syria.CVN_71 -- * AIRBASE.Syria.Sayqal -- * AIRBASE.Syria.Tiyas -- * AIRBASE.Syria.Shayrat @@ -45256,6 +49426,17 @@ AIRBASE.TheChannel = { -- * AIRBASE.Syria.Beirut_Rafic_Hariri -- * AIRBASE.Syria.An_Nasiriyah -- * AIRBASE.Syria.Abu_al_Duhur +-- * AIRBASE.Syria.At_Tanf +-- * AIRBASE.Syria.H3 +-- * AIRBASE.Syria.H3_Northwest +-- * AIRBASE.Syria.H3_Southwest +-- * AIRBASE.Syria.Kharab_Ishk +-- * AIRBASE.Syria.Raj_al_Issa_East +-- * AIRBASE.Syria.Raj_al_Issa_West +-- * AIRBASE.Syria.Ruwayshid +-- * AIRBASE.Syria.Sanliurfa +-- * AIRBASE.Syria.Tal_Siman +-- * AIRBASE.Syria.Deir_ez_Zor -- --@field Syria AIRBASE.Syria={ @@ -45273,7 +49454,7 @@ AIRBASE.Syria={ ["Wujah_Al_Hajar"]="Wujah Al Hajar", ["Al_Dumayr"]="Al-Dumayr", ["Gazipasa"]="Gazipasa", - ["Ru_Convoy_4"]="Ru Convoy-4", + --["Ru_Convoy_4"]="Ru Convoy-4", ["Hatay"]="Hatay", ["Nicosia"]="Nicosia", ["Pinarbashi"]="Pinarbashi", @@ -45311,10 +49492,19 @@ AIRBASE.Syria={ ["Beirut_Rafic_Hariri"]="Beirut-Rafic Hariri", ["An_Nasiriyah"]="An Nasiriyah", ["Abu_al_Duhur"]="Abu al-Duhur", + ["At_Tanf"]="At Tanf", + ["H3"]="H3", + ["H3_Northwest"]="H3 Northwest", + ["H3_Southwest"]="H3 Southwest", + ["Kharab_Ishk"]="Kharab Ishk", + ["Raj_al_Issa_East"]="Raj al Issa East", + ["Raj_al_Issa_West"]="Raj al Issa West", + ["Ruwayshid"]="Ruwayshid", + ["Sanliurfa"]="Sanliurfa", + ["Tal_Siman"]="Tal Siman", + ["Deir_ez_Zor"] = "Deir ez-Zor", } - - --- Airbases of the Mariana Islands map: -- -- * AIRBASE.MarianaIslands.Rota_Intl @@ -45334,6 +49524,32 @@ AIRBASE.MarianaIslands={ ["Olf_Orote"]="Olf Orote", } +--- Airbases of the South Atlantic map: +-- +-- * AIRBASE.SouthAtlantic.Port_Stanley +-- * AIRBASE.SouthAtlantic.Mount_Pleasant +-- * AIRBASE.SouthAtlantic.San_Carlos_FOB +-- * AIRBASE.SouthAtlantic.Rio_Grande +-- * AIRBASE.SouthAtlantic.Rio_Gallegos +-- * AIRBASE.SouthAtlantic.Ushuaia +-- * AIRBASE.SouthAtlantic.Ushuaia_Helo_Port +-- * AIRBASE.SouthAtlantic.Punta_Arenas +-- * AIRBASE.SouthAtlantic.Pampa_Guanaco +-- * AIRBASE.SouthAtlantic.San_Julian +-- +--@field MarianaIslands +AIRBASE.SouthAtlantic={ + ["Port_Stanley"]="Port Stanley", + ["Mount_Pleasant"]="Mount Pleasant", + ["San_Carlos_FOB"]="San Carlos FOB", + ["Rio_Grande"]="Rio Grande", + ["Rio_Gallegos"]="Rio Gallegos", + ["Ushuaia"]="Ushuaia", + ["Ushuaia_Helo_Port"]="Ushuaia Helo Port", + ["Punta_Arenas"]="Punta Arenas", + ["Pampa_Guanaco"]="Pampa Guanaco", + ["San_Julian"]="San Julian", +} --- AIRBASE.ParkingSpot ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy". -- @type AIRBASE.ParkingSpot @@ -45344,6 +49560,14 @@ AIRBASE.MarianaIslands={ -- @field #boolean Free This spot is currently free, i.e. there is no alive aircraft on it at the present moment. -- @field #number TerminalID0 Unknown what this means. If you know, please tell us! -- @field #number DistToRwy Distance to runway in meters. Currently bugged and giving the same number as the TerminalID. +-- @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 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. --- Terminal Types of parking spots. See also https://wiki.hoggitworld.com/view/DCS_func_getParking -- @@ -45378,13 +49602,30 @@ AIRBASE.TerminalType = { FighterAircraft=244, } +--- Status of a parking spot. +-- @type AIRBASE.SpotStatus +-- @field #string FREE Spot is free. +-- @field #string OCCUPIED Spot is occupied. +-- @field #string RESERVED Spot is reserved. +AIRBASE.SpotStatus = { + FREE="Free", + OCCUPIED="Occupied", + RESERVED="Reserved", +} + --- 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 @@ -45408,6 +49649,9 @@ function AIRBASE:Register(AirbaseName) -- Get descriptors. self.descriptors=self:GetDesc() + -- Debug info. + --self:I({airbase=AirbaseName, descriptors=self.descriptors}) + -- Category. self.category=self.descriptors and self.descriptors.category or Airbase.Category.AIRDROME @@ -45424,13 +49668,23 @@ function AIRBASE:Register(AirbaseName) self.isShip=false self.category=Airbase.Category.HELIPAD _DATABASE:AddStatic(AirbaseName) - end + end 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() + -- Get 2D position vector. local vec2=self:GetVec2() -- Init coordinate. @@ -45643,6 +49897,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 @@ -45824,6 +50114,21 @@ 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 client=_client --Wrapper.Client#CLIENT + if client and client.SpawnCoord then + local dist=client.SpawnCoord: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 @@ -45837,6 +50142,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 @@ -45895,6 +50202,7 @@ function AIRBASE:GetParkingSpotsTable(termtype) spot.Free=_isfree(_spot) -- updated spot.TOAC=_spot.TO_AC -- updated + spot.AirbaseName=self.AirbaseName table.insert(spots, spot) @@ -45931,6 +50239,7 @@ function AIRBASE:GetFreeParkingSpotsTable(termtype, allowTOAC) spot.Free=true -- updated spot.TOAC=_spot.TO_AC -- updated + spot.AirbaseName=self.AirbaseName table.insert(freespots, spot) @@ -46012,7 +50321,7 @@ end -- @param #table parkingdata (Optional) Parking spots data table. If not given it is automatically derived from the GetParkingSpotsTable() function. -- @return #table Table of coordinates and terminal IDs of free parking spots. Each table entry has the elements .Coordinate and .TerminalID. function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, scanunits, scanstatics, scanscenery, verysafe, nspots, parkingdata) - + -- Init default scanradius=scanradius or 50 if scanunits==nil then @@ -46068,7 +50377,7 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, az = 17 -- width end - + -- Number of spots we are looking for. Note that, e.g. grouping can require a number different from the group size! local _nspots=nspots or group:GetSize() @@ -46196,7 +50505,7 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, -- Retrun spots we found, even if there were not enough. return validspots - + end --- Check black and white lists. @@ -46215,7 +50524,7 @@ function AIRBASE:_CheckParkingLists(TerminalID) end end - + -- Check if a whitelist was defined. if self.parkingWhitelist and #self.parkingWhitelist>0 then for _,terminalID in pairs(self.parkingWhitelist or {}) do @@ -46282,6 +50591,231 @@ 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 + + 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) + + -- 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. @@ -46325,7 +50859,7 @@ function AIRBASE:GetRunwayData(magvar, mark) name==AIRBASE.PersianGulf.Abu_Dhabi_International_Airport or name==AIRBASE.PersianGulf.Dubai_Intl or name==AIRBASE.PersianGulf.Shiraz_International_Airport or - name==AIRBASE.PersianGulf.Kish_International_Airport or + name==AIRBASE.PersianGulf.Kish_International_Airport or name==AIRBASE.MarianaIslands.Andersen_AFB then -- 1-->4, 2-->3, 3-->2, 4-->1 @@ -46449,26 +50983,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(string.format("%s: Setting active runway for landing as %s", self.AirbaseName, 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(string.format("%s: Setting active runway for takeoff as %s", self.AirbaseName, 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() @@ -46489,33 +51097,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 self.Fratricide * 0.50 then + + -- TODO: make fratricide switchable + if self.Players[PlayerName].Penalty > self.Fratricide * 0.50 and self.penaltyonfratricide then if self.Players[PlayerName].PenaltyWarning < 1 then MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "': WARNING! If you continue to commit FRATRICIDE and have a PENALTY score higher than " .. self.Fratricide .. ", you will be COURT MARTIALED and DISMISSED from this mission! \nYour total penalty is: " .. self.Players[PlayerName].Penalty, MESSAGE.Type.Information @@ -51324,7 +55994,7 @@ function SCORING:_AddPlayerFromUnit( UnitData ) end end - if self.Players[PlayerName].Penalty > self.Fratricide then + if self.Players[PlayerName].Penalty > self.Fratricide and self.penaltyonfratricide then MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' committed FRATRICIDE, he will be COURT MARTIALED and is DISMISSED from this mission!", MESSAGE.Type.Information ):ToAll() @@ -51769,9 +56439,9 @@ function SCORING:_EventOnHit( Event ) if InitCoalition then -- A coalition object was hit, probably a static. if InitCoalition == TargetCoalition then -- TODO: Penalty according scale - Player.Penalty = Player.Penalty + 10 - PlayerHit.Penalty = PlayerHit.Penalty + 10 - PlayerHit.PenaltyHit = PlayerHit.PenaltyHit + 1 + Player.Penalty = Player.Penalty + 10 --* self.ScaleDestroyPenalty + PlayerHit.Penalty = PlayerHit.Penalty + 10 --* self.ScaleDestroyPenalty + PlayerHit.PenaltyHit = PlayerHit.PenaltyHit + 1 * self.ScaleDestroyPenalty MESSAGE :NewType( self.DisplayMessagePrefix .. "Player '" .. Event.WeaponPlayerName .. "' hit friendly target " .. @@ -52331,7 +57001,7 @@ function SCORING:ReportScoreGroupDetailed( PlayerGroup ) self:F( { ReportMissions, ScoreMissions, PenaltyMissions } ) local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions - local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + ScoreGoals + PenaltyMissions + local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + PenaltyGoals + PenaltyMissions PlayerMessage = string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )%s%s%s%s%s", @@ -52387,7 +57057,7 @@ function SCORING:ReportScoreAllSummary( PlayerGroup ) self:F( { ReportMissions, ScoreMissions, PenaltyMissions } ) local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions - local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + ScoreGoals + PenaltyMissions + local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + PenaltyGoals + PenaltyMissions PlayerMessage = string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )", @@ -53129,6 +57799,8 @@ end -- -- * When SAM sites are being fired upon, the SAMs will take evasive action will reposition themselves when possible. -- * When SAM sites are being fired upon, the SAMs will take defensive action by shutting down their radars. +-- * SEAD calculates the time it takes for a HARM to reach the target - and will attempt to minimize the shut-down time. +-- * Detection and evasion of shots has a random component based on the skill level of the SAM groups. -- -- === -- @@ -53140,7 +57812,7 @@ end -- -- ### Authors: **FlightControl**, **applevangelist** -- --- Last Update: Aug 2021 +-- Last Update: Feb 2022 -- -- === -- @@ -53154,7 +57826,11 @@ end --- Make SAM sites execute evasive and defensive behaviour when being fired upon. -- -- This class is very easy to use. Just setup a SEAD object by using @{#SEAD.New}() and SAMs will evade and take defensive action when being fired upon. --- +-- Once a HARM attack is detected, SEAD will shut down the radars of the attacked SAM site and take evasive action by moving the SAM +-- vehicles around (*if* they are drivable, that is). There's a component of randomness in detection and evasion, which is based on the +-- skill set of the SAM set (the higher the skill, the more likely). When a missile is fired from far away, the SAM will stay active for a +-- period of time to stay defensive, before it takes evasive actions. +-- -- # Constructor: -- -- Use the @{#SEAD.New}() constructor to create a new SEAD object. @@ -53174,6 +57850,9 @@ SEAD = { SuppressedGroups = {}, EngagementRange = 75, -- default 75% engagement range Feature Request #1355 Padding = 10, + CallBack = nil, + UseCallBack = false, + debug = false, } --- Missile enumerators @@ -53191,6 +57870,9 @@ SEAD = { ["X_25"] = "X_25", ["X_31"] = "X_31", ["Kh25"] = "Kh25", + ["BGM_109"] = "BGM_109", + ["AGM_154"] = "AGM_154", + ["HY-2"] = "HY-2", } --- Missile enumerators - from DCS ME and Wikipedia @@ -53200,7 +57882,7 @@ SEAD = { ["AGM_88"] = { 150, 3}, ["AGM_45"] = { 12, 2}, ["AGM_122"] = { 16.5, 2.3}, - ["AGM_84"] = { 280, 0.85}, + ["AGM_84"] = { 280, 0.8}, ["ALARM"] = { 45, 2}, ["LD-10"] = { 60, 4}, ["X_58"] = { 70, 4}, @@ -53208,6 +57890,9 @@ SEAD = { ["X_25"] = { 25, 0.76}, ["X_31"] = {150, 3}, ["Kh25"] = {25, 0.8}, + ["BGM_109"] = {460, 0.705}, --in-game ~465kn + ["AGM_154"] = {130, 0.61}, + ["HY-2"] = {90,1}, } --- Creates the main object which is handling defensive actions for SA sites or moving SA vehicles. @@ -53223,8 +57908,8 @@ SEAD = { -- SEAD_RU_SAM_Defenses = SEAD:New( { 'RU SA-6 Kub', 'RU SA-6 Defenses', 'RU MI-26 Troops', 'RU Attack Gori' } ) function SEAD:New( SEADGroupPrefixes, Padding ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( SEADGroupPrefixes ) + local self = BASE:Inherit( self, FSM:New() ) + self:T( SEADGroupPrefixes ) if type( SEADGroupPrefixes ) == 'table' then for SEADGroupPrefixID, SEADGroupPrefix in pairs( SEADGroupPrefixes ) do @@ -53237,14 +57922,25 @@ function SEAD:New( SEADGroupPrefixes, Padding ) local padding = Padding or 10 if padding < 10 then padding = 10 end self.Padding = padding - + self.UseEmissionsOnOff = true + + self.debug = false + + self.CallBack = nil + self.UseCallBack = false + self:HandleEvent( EVENTS.Shot, self.HandleEventShot ) - - self:I("*** SEAD - Started Version 0.3.1") + + -- Start State. + self:SetStartState("Running") + self:AddTransition("*", "ManageEvasion", "*") + self:AddTransition("*", "CalculateHitZone", "*") + + self:I("*** SEAD - Started Version 0.4.3") return self end ---- Update the active SEAD Set +--- Update the active SEAD Set (while running) -- @param #SEAD self -- @param #table SEADGroupPrefixes The prefixes to add, note: can also be a single #string -- @return #SEAD self @@ -53265,7 +57961,7 @@ end --- Sets the engagement range of the SAMs. Defaults to 75% to make it more deadly. Feature Request #1355 -- @param #SEAD self --- @param #number range Set the engagement range in percent, e.g. 50 +-- @param #number range Set the engagement range in percent, e.g. 55 (default 75) -- @return #SEAD self function SEAD:SetEngagementRange(range) self:T( { range } ) @@ -53280,7 +57976,7 @@ end --- Set the padding in seconds, which extends the radar off time calculated by SEAD -- @param #SEAD self --- @param #number Padding Extra number of seconds to add for the switch-on +-- @param #number Padding Extra number of seconds to add for the switch-on (default 10 seconds) -- @return #SEAD self function SEAD:SetPadding(Padding) self:T( { Padding } ) @@ -53290,60 +57986,264 @@ function SEAD:SetPadding(Padding) return self end - --- Check if a known HARM was fired - -- @param #SEAD self - -- @param #string WeaponName - -- @return #boolean Returns true for a match - -- @return #string name Name of hit in table - function SEAD:_CheckHarms(WeaponName) - self:T( { WeaponName } ) - local hit = false - local name = "" - for _,_name in pairs (SEAD.Harms) do - if string.find(WeaponName,_name,1) then - hit = true - name = _name - break - end - end - return hit, name - end +--- Set SEAD to use emissions on/off in addition to alarm state. +-- @param #SEAD self +-- @param #boolean Switch True for on, false for off. +-- @return #SEAD self +function SEAD:SwitchEmissions(Switch) + self:T({Switch}) + self.UseEmissionsOnOff = Switch + return self +end - --- (Internal) Return distance in meters between two coordinates or -1 on error. - -- @param #SEAD self - -- @param Core.Point#COORDINATE _point1 Coordinate one - -- @param Core.Point#COORDINATE _point2 Coordinate two - -- @return #number Distance in meters - function SEAD:_GetDistance(_point1, _point2) - self:T("_GetDistance") - if _point1 and _point2 then - local distance1 = _point1:Get2DDistance(_point2) - local distance2 = _point1:DistanceFromPointVec2(_point2) - --self:T({dist1=distance1, dist2=distance2}) - if distance1 and type(distance1) == "number" then - return distance1 - elseif distance2 and type(distance2) == "number" then - return distance2 - else - self:E("*****Cannot calculate distance!") - self:E({_point1,_point2}) - return -1 +--- Add an object to call back when going evasive. +-- @param #SEAD self +-- @param #table Object The object to call. Needs to have object functions as follows: +-- `:SeadSuppressionPlanned(Group, Name, SuppressionStartTime, SuppressionEndTime)` +-- `:SeadSuppressionStart(Group, Name)`, +-- `:SeadSuppressionEnd(Group, Name)`, +-- @return #SEAD self +function SEAD:AddCallBack(Object) + self:T({Class=Object.ClassName}) + self.CallBack = Object + self.UseCallBack = true + return self +end + +--- (Internal) Check if a known HARM was fired +-- @param #SEAD self +-- @param #string WeaponName +-- @return #boolean Returns true for a match +-- @return #string name Name of hit in table +function SEAD:_CheckHarms(WeaponName) + self:T( { WeaponName } ) + local hit = false + local name = "" + for _,_name in pairs (SEAD.Harms) do + if string.find(WeaponName,_name,1,true) then + hit = true + name = _name + break end + end + return hit, name +end + +--- (Internal) Return distance in meters between two coordinates or -1 on error. +-- @param #SEAD self +-- @param Core.Point#COORDINATE _point1 Coordinate one +-- @param Core.Point#COORDINATE _point2 Coordinate two +-- @return #number Distance in meters +function SEAD:_GetDistance(_point1, _point2) + self:T("_GetDistance") + if _point1 and _point2 then + local distance1 = _point1:Get2DDistance(_point2) + local distance2 = _point1:DistanceFromPointVec2(_point2) + --self:T({dist1=distance1, dist2=distance2}) + if distance1 and type(distance1) == "number" then + return distance1 + elseif distance2 and type(distance2) == "number" then + return distance2 else - self:E("******Cannot calculate distance!") + self:E("*****Cannot calculate distance!") self:E({_point1,_point2}) return -1 end + else + self:E("******Cannot calculate distance!") + self:E({_point1,_point2}) + return -1 end +end ---- Detects if an SAM site was shot with an anti radiation missile. In this case, take evasive actions based on the skill level set within the ME. --- @see SEAD --- @param #SEAD +--- (Internal) Calculate hit zone of an AGM-88 +-- @param #SEAD self +-- @param #table SEADWeapon DCS.Weapon object +-- @param Core.Point#COORDINATE pos0 Position of the plane when it fired +-- @param #number height Height when the missile was fired +-- @param Wrapper.Group#GROUP SEADGroup Attacker group +-- @param #string SEADWeaponName Weapon Name +-- @return #SEAD self +function SEAD:onafterCalculateHitZone(From,Event,To,SEADWeapon,pos0,height,SEADGroup,SEADWeaponName) + self:T("**** Calculating hit zone for " .. (SEADWeaponName or "None")) + if SEADWeapon and SEADWeapon:isExist() then + --local pos = SEADWeapon:getPoint() + + -- postion and height + local position = SEADWeapon:getPosition() + local mheight = height + -- heading + local wph = math.atan2(position.x.z, position.x.x) + if wph < 0 then + wph=wph+2*math.pi + end + wph=math.deg(wph) + + -- velocity + local wpndata = SEAD.HarmData["AGM_88"] + if string.find(SEADWeaponName,"154",1) then + wpndata = SEAD.HarmData["AGM_154"] + end + local mveloc = math.floor(wpndata[2] * 340.29) + local c1 = (2*mheight*9.81)/(mveloc^2) + local c2 = (mveloc^2) / 9.81 + local Ropt = c2 * math.sqrt(c1+1) + if height <= 5000 then + Ropt = Ropt * 0.72 + elseif height <= 7500 then + Ropt = Ropt * 0.82 + elseif height <= 10000 then + Ropt = Ropt * 0.87 + elseif height <= 12500 then + Ropt = Ropt * 0.98 + end + + -- look at a couple of zones across the trajectory + for n=1,3 do + local dist = Ropt - ((n-1)*20000) + local predpos= pos0:Translate(dist,wph) + if predpos then + + local targetzone = ZONE_RADIUS:New("Target Zone",predpos:GetVec2(),20000) + + if self.debug then + predpos:MarkToAll(string.format("height=%dm | heading=%d | velocity=%ddeg | Ropt=%dm",mheight,wph,mveloc,Ropt),false) + targetzone:DrawZone(coalition.side.BLUE,{0,0,1},0.2,nil,nil,3,true) + end + + local seadset = SET_GROUP:New():FilterPrefixes(self.SEADGroupPrefixes):FilterZones({targetzone}):FilterOnce() + local tgtcoord = targetzone:GetRandomPointVec2() + --if tgtcoord and tgtcoord.ClassName == "COORDINATE" then + --local tgtgrp = seadset:FindNearestGroupFromPointVec2(tgtcoord) + local tgtgrp = seadset:GetRandom() + local _targetgroup = nil + local _targetgroupname = "none" + local _targetskill = "Random" + if tgtgrp and tgtgrp:IsAlive() then + _targetgroup = tgtgrp + _targetgroupname = tgtgrp:GetName() -- group name + _targetskill = tgtgrp:GetUnit(1):GetSkill() + self:T("*** Found Target = ".. _targetgroupname) + self:ManageEvasion(_targetskill,_targetgroup,pos0,"AGM_88",SEADGroup, 20) + end + --end + end + end + end + return self +end + +--- (Internal) Handle Evasion +-- @param #SEAD self +-- @param #string _targetskill +-- @param Wrapper.Group#GROUP _targetgroup +-- @param Core.Point#COORDINATE SEADPlanePos +-- @param #string SEADWeaponName +-- @param Wrapper.Group#GROUP SEADGroup Attacker Group +-- @param #number timeoffset Offset for tti calc +-- @return #SEAD self +function SEAD:onafterManageEvasion(From,Event,To,_targetskill,_targetgroup,SEADPlanePos,SEADWeaponName,SEADGroup,timeoffset) + local timeoffset = timeoffset or 0 + if _targetskill == "Random" then -- when skill is random, choose a skill + local Skills = { "Average", "Good", "High", "Excellent" } + _targetskill = Skills[ math.random(1,4) ] + end + --self:T( _targetskill ) + if self.TargetSkill[_targetskill] then + local _evade = math.random (1,100) -- random number for chance of evading action + if (_evade > self.TargetSkill[_targetskill].Evade) then + self:T("*** SEAD - Evading") + -- calculate distance of attacker + local _targetpos = _targetgroup:GetCoordinate() + local _distance = self:_GetDistance(SEADPlanePos, _targetpos) + -- weapon speed + local hit, data = self:_CheckHarms(SEADWeaponName) + local wpnspeed = 666 -- ;) + local reach = 10 + if hit then + local wpndata = SEAD.HarmData[data] + reach = wpndata[1] * 1,1 + local mach = wpndata[2] + wpnspeed = math.floor(mach * 340.29) + end + -- time to impact + local _tti = math.floor(_distance / wpnspeed) - timeoffset -- estimated impact time + if _distance > 0 then + _distance = math.floor(_distance / 1000) -- km + else + _distance = 0 + end + + self:T( string.format("*** SEAD - target skill %s, distance %dkm, reach %dkm, tti %dsec", _targetskill, _distance,reach,_tti )) + + if reach >= _distance then + self:T("*** SEAD - Shot in Reach") + + local function SuppressionStart(args) + self:T(string.format("*** SEAD - %s Radar Off & Relocating",args[2])) + local grp = args[1] -- Wrapper.Group#GROUP + local name = args[2] -- #string Group Name + local attacker = args[3] -- Wrapper.Group#GROUP + if self.UseEmissionsOnOff then + grp:EnableEmission(false) + end + grp:OptionAlarmStateGreen() -- needed else we cannot move around + grp:RelocateGroundRandomInRadius(20,300,false,false,"Diamond") + if self.UseCallBack then + local object = self.CallBack + object:SeadSuppressionStart(grp,name,attacker) + end + end + + local function SuppressionStop(args) + self:T(string.format("*** SEAD - %s Radar On",args[2])) + local grp = args[1] -- Wrapper.Group#GROUP + local name = args[2] -- #string Group Nam + if self.UseEmissionsOnOff then + grp:EnableEmission(true) + end + grp:OptionAlarmStateRed() + grp:OptionEngageRange(self.EngagementRange) + self.SuppressedGroups[name] = false + if self.UseCallBack then + local object = self.CallBack + object:SeadSuppressionEnd(grp,name) + end + end + + -- randomize switch-on time + local delay = math.random(self.TargetSkill[_targetskill].DelayOn[1], self.TargetSkill[_targetskill].DelayOn[2]) + if delay > _tti then delay = delay / 2 end -- speed up + if _tti > 600 then delay = _tti - 90 end -- shot from afar, 600 is default shorad ontime + + local SuppressionStartTime = timer.getTime() + delay + local SuppressionEndTime = timer.getTime() + _tti + self.Padding + local _targetgroupname = _targetgroup:GetName() + if not self.SuppressedGroups[_targetgroupname] then + self:T(string.format("*** SEAD - %s | Parameters TTI %ds | Switch-Off in %ds",_targetgroupname,_tti,delay)) + timer.scheduleFunction(SuppressionStart,{_targetgroup,_targetgroupname, SEADGroup},SuppressionStartTime) + timer.scheduleFunction(SuppressionStop,{_targetgroup,_targetgroupname},SuppressionEndTime) + self.SuppressedGroups[_targetgroupname] = true + if self.UseCallBack then + local object = self.CallBack + object:SeadSuppressionPlanned(_targetgroup,_targetgroupname,SuppressionStartTime,SuppressionEndTime, SEADGroup) + end + end + + end + end + end + return self +end + +--- (Internal) Detects if an SAM site was shot with an anti radiation missile. In this case, take evasive actions based on the skill level set within the ME. +-- @param #SEAD self -- @param Core.Event#EVENTDATA EventData -- @return #SEAD self function SEAD:HandleEventShot( EventData ) self:T( { EventData.id } ) local SEADPlane = EventData.IniUnit -- Wrapper.Unit#UNIT + local SEADGroup = EventData.IniGroup -- Wrapper.Group#GROUP local SEADPlanePos = SEADPlane:GetCoordinate() -- Core.Point#COORDINATE local SEADUnit = EventData.IniDCSUnit local SEADUnitName = EventData.IniDCSUnitName @@ -53358,94 +58258,55 @@ function SEAD:HandleEventShot( EventData ) local _targetskill = "Random" local _targetgroupname = "none" local _target = EventData.Weapon:getTarget() -- Identify target - local _targetUnit = UNIT:Find(_target) -- Wrapper.Unit#UNIT + if not _target or self.debug then -- AGM-88 or 154 w/o target data + self:E("***** SEAD - No target data for " .. (SEADWeaponName or "None")) + if string.find(SEADWeaponName,"AGM_88",1,true) or string.find(SEADWeaponName,"AGM_154",1,true) then + self:I("**** Tracking AGM-88/154 with no target data.") + local pos0 = SEADPlane:GetCoordinate() + local fheight = SEADPlane:GetHeight() + self:__CalculateHitZone(20,SEADWeapon,pos0,fheight,SEADGroup,SEADWeaponName) + end + return self + end + local targetcat = _target:getCategory() -- Identify category + local _targetUnit = nil -- Wrapper.Unit#UNIT local _targetgroup = nil -- Wrapper.Group#GROUP - if _targetUnit and _targetUnit:IsAlive() then - _targetgroup = _targetUnit:GetGroup() - _targetgroupname = _targetgroup:GetName() -- group name - local _targetUnitName = _targetUnit:GetName() - _targetUnit:GetSkill() - _targetskill = _targetUnit:GetSkill() + self:T(string.format("*** Targetcat = %d",targetcat)) + if targetcat == Object.Category.UNIT then -- UNIT + self:T("*** Target Category UNIT") + _targetUnit = UNIT:Find(_target) -- Wrapper.Unit#UNIT + if _targetUnit and _targetUnit:IsAlive() then + _targetgroup = _targetUnit:GetGroup() + _targetgroupname = _targetgroup:GetName() -- group name + local _targetUnitName = _targetUnit:GetName() + _targetUnit:GetSkill() + _targetskill = _targetUnit:GetSkill() + end + elseif targetcat == Object.Category.STATIC then + self:T("*** Target Category STATIC") + local seadset = SET_GROUP:New():FilterPrefixes(self.SEADGroupPrefixes):FilterOnce() + local targetpoint = _target:getPoint() or {x=0,y=0,z=0} + local tgtcoord = COORDINATE:NewFromVec3(targetpoint) + local tgtgrp = seadset:FindNearestGroupFromPointVec2(tgtcoord) + if tgtgrp and tgtgrp:IsAlive() then + _targetgroup = tgtgrp + _targetgroupname = tgtgrp:GetName() -- group name + _targetskill = tgtgrp:GetUnit(1):GetSkill() + self:T("*** Found Target = ".. _targetgroupname) + end end -- see if we are shot at local SEADGroupFound = false for SEADGroupPrefixID, SEADGroupPrefix in pairs( self.SEADGroupPrefixes ) do - self:T( _targetgroupname, SEADGroupPrefix ) - if string.find( _targetgroupname, SEADGroupPrefix ) then + self:T("Target = ".. _targetgroupname .. " | Prefix = " .. SEADGroupPrefix ) + if string.find( _targetgroupname, SEADGroupPrefix,1,true ) then SEADGroupFound = true self:T( '*** SEAD - Group Match Found' ) break end end if SEADGroupFound == true then -- yes we are being attacked - if _targetskill == "Random" then -- when skill is random, choose a skill - local Skills = { "Average", "Good", "High", "Excellent" } - _targetskill = Skills[ math.random(1,4) ] - end - --self:T( _targetskill ) - if self.TargetSkill[_targetskill] then - local _evade = math.random (1,100) -- random number for chance of evading action - if (_evade > self.TargetSkill[_targetskill].Evade) then - self:T("*** SEAD - Evading") - -- calculate distance of attacker - local _targetpos = _targetgroup:GetCoordinate() - local _distance = self:_GetDistance(SEADPlanePos, _targetpos) - -- weapon speed - local hit, data = self:_CheckHarms(SEADWeaponName) - local wpnspeed = 666 -- ;) - local reach = 10 - if hit then - local wpndata = SEAD.HarmData[data] - reach = wpndata[1] * 1,1 - local mach = wpndata[2] - wpnspeed = math.floor(mach * 340.29) - end - -- time to impact - local _tti = math.floor(_distance / wpnspeed) -- estimated impact time - if _distance > 0 then - _distance = math.floor(_distance / 1000) -- km - else - _distance = 0 - end - - self:T( string.format("*** SEAD - target skill %s, distance %dkm, reach %dkm, tti %dsec", _targetskill, _distance,reach,_tti )) - - if reach >= _distance then - self:T("*** SEAD - Shot in Reach") - - local function SuppressionStart(args) - self:T(string.format("*** SEAD - %s Radar Off & Relocating",args[2])) - local grp = args[1] -- Wrapper.Group#GROUP - grp:OptionAlarmStateGreen() - grp:RelocateGroundRandomInRadius(20,300,false,false,"Diamond") - end - - local function SuppressionStop(args) - self:T(string.format("*** SEAD - %s Radar On",args[2])) - local grp = args[1] -- Wrapper.Group#GROUP - grp:OptionAlarmStateRed() - grp:OptionEngageRange(self.EngagementRange) - self.SuppressedGroups[args[2]] = false - end - - -- randomize switch-on time - local delay = math.random(self.TargetSkill[_targetskill].DelayOn[1], self.TargetSkill[_targetskill].DelayOn[2]) - if delay > _tti then delay = delay / 2 end -- speed up - if _tti > (3*delay) then delay = (_tti / 2) * 0.9 end -- shot from afar - - local SuppressionStartTime = timer.getTime() + delay - local SuppressionEndTime = timer.getTime() + _tti + self.Padding - - if not self.SuppressedGroups[_targetgroupname] then - self:T(string.format("*** SEAD - %s | Parameters TTI %ds | Switch-Off in %ds",_targetgroupname,_tti,delay)) - timer.scheduleFunction(SuppressionStart,{_targetgroup,_targetgroupname},SuppressionStartTime) - timer.scheduleFunction(SuppressionStop,{_targetgroup,_targetgroupname},SuppressionEndTime) - self.SuppressedGroups[_targetgroupname] = true - end - - end - end - end + self:ManageEvasion(_targetskill,_targetgroup,SEADPlanePos,SEADWeaponName,SEADGroup) end end return self @@ -56636,7 +61497,7 @@ end -- * `AIRBASE.Nevada.Lincoln_County` -- * `AIRBASE.Nevada.McCarran_International_Airport` -- * `AIRBASE.Nevada.Mesquite` --- * `AIRBASE.Nevada.Mina_Airport_3Q0` +-- * `AIRBASE.Nevada.Mina_Airport` -- * `AIRBASE.Nevada.Nellis_AFB` -- * `AIRBASE.Nevada.North_Las_Vegas` -- * `AIRBASE.Nevada.Pahute_Mesa_Airstrip` @@ -56871,7 +61732,7 @@ ATC_GROUND_NEVADA = { }, }, }, - [AIRBASE.Nevada.Mina_Airport_3Q0] = { + [AIRBASE.Nevada.Mina_Airport] = { PointsRunways = { [1] = { [1] = {["y"]=-290054.57371429,["x"]=-160930.02228572,}, @@ -59754,7 +64615,7 @@ do -- DETECTION_BASE -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. - -- @param Wrapper.Group#GROUP DetectionGroup The Group detecting. + -- @param Wrapper.Group#GROUP Detection The Group detecting. -- @param #number DetectionTimeStamp Time stamp of detection event. function DETECTION_BASE:onafterDetection( From, Event, To, Detection, DetectionTimeStamp ) @@ -59825,7 +64686,7 @@ do -- DETECTION_BASE local DetectedObjectVec3 = DetectedObject:getPoint() local DetectedObjectVec2 = { x = DetectedObjectVec3.x, y = DetectedObjectVec3.z } - local DetectionGroupVec3 = Detection:GetVec3() + local DetectionGroupVec3 = Detection:GetVec3() or {x=0,y=0,z=0} local DetectionGroupVec2 = { x = DetectionGroupVec3.x, y = DetectionGroupVec3.z } local Distance = ( ( DetectedObjectVec3.x - DetectionGroupVec3.x )^2 + @@ -59869,7 +64730,7 @@ do -- DETECTION_BASE if self.RejectZones then for RejectZoneID, RejectZone in pairs( self.RejectZones ) do local RejectZone = RejectZone -- Core.Zone#ZONE_BASE - if RejectZone:IsPointVec2InZone( DetectedObjectVec2 ) == true then + if RejectZone:IsVec2InZone( DetectedObjectVec2 ) == true then DetectionAccepted = false end end @@ -59914,7 +64775,7 @@ do -- DETECTION_BASE local ZoneProbability = ZoneData[2] -- #number ZoneProbability = ZoneProbability * 30 / 300 - if ZoneObject:IsPointVec2InZone( DetectedObjectVec2 ) == true then + if ZoneObject:IsVec2InZone( DetectedObjectVec2 ) == true then local Probability = math.random() -- Selects a number between 0 and 1 --self:T( { Probability, ZoneProbability } ) if Probability > ZoneProbability then @@ -61640,13 +66501,13 @@ do -- DETECTION_AREAS -- ## 4.1) Retrieve the Detected Unit Sets and Detected Zones -- -- The methods to manage the DetectedItems[].Set(s) are implemented in @{Functional.Detection#DECTECTION_BASE} and - -- the methods to manage the DetectedItems[].Zone(s) is implemented in @{Functional.Detection#DETECTION_AREAS}. + -- the methods to manage the DetectedItems[].Zone(s) are implemented in @{Functional.Detection#DETECTION_AREAS}. -- -- Retrieve the DetectedItems[].Set with the method @{Functional.Detection#DETECTION_BASE.GetDetectedSet}(). A @{Core.Set#SET_UNIT} object will be returned. -- - -- Retrieve the formed @{Zone@ZONE_UNIT}s as a result of the grouping the detected units within the DetectionZoneRange, use the method @{Functional.Detection#DETECTION_BASE.GetDetectionZones}(). - -- To understand the amount of zones created, use the method @{Functional.Detection#DETECTION_BASE.GetDetectionZoneCount}(). - -- If you want to obtain a specific zone from the DetectedZones, use the method @{Functional.Detection#DETECTION_BASE.GetDetectionZone}() with a given index. + -- Retrieve the formed @{Zone@ZONE_UNIT}s as a result of the grouping the detected units within the DetectionZoneRange, use the method @{Functional.Detection#DETECTION_AREAS.GetDetectionZones}(). + -- To understand the amount of zones created, use the method @{Functional.Detection#DETECTION_AREAS.GetDetectionZoneCount}(). + -- If you want to obtain a specific zone from the DetectedZones, use the method @{Functional.Detection#DETECTION_AREAS.GetDetectionZoneByID}() with a given index. -- -- ## 4.4) Flare or Smoke detected units -- @@ -61690,7 +66551,49 @@ do -- DETECTION_AREAS return self end - + --- Retrieve set of detected zones. + -- @param #DETECTION_AREAS self + -- @return Core.Set#SET_ZONE The @{Set} of ZONE_UNIT objects detected. + function DETECTION_AREAS:GetDetectionZones() + local zoneset = SET_ZONE:New() + for _ID,_Item in pairs (self.DetectedItems) do + local item = _Item -- #DETECTION_BASE.DetectedItem + if item.Zone then + zoneset:AddZone(item.Zone) + end + end + return zoneset + end + + --- Retrieve a specific zone by its ID (number) + -- @param #DETECTION_AREAS self + -- @param #number ID + -- @return Core.Zone#ZONE_UNIT The zone or nil if it does not exist + function DETECTION_AREAS:GetDetectionZoneByID(ID) + local zone = nil + for _ID,_Item in pairs (self.DetectedItems) do + local item = _Item -- #DETECTION_BASE.DetectedItem + if item.ID == ID then + zone = item.Zone + break + end + end + return zone + end + + --- Retrieve number of detected zones. + -- @param #DETECTION_AREAS self + -- @return #number The number of zones. + function DETECTION_AREAS:GetDetectionZoneCount() + local zoneset = 0 + for _ID,_Item in pairs (self.DetectedItems) do + if _Item.Zone then + zoneset = zoneset + 1 + end + end + return zoneset + end + --- Report summary of a detected item using a given numeric index. -- @param #DETECTION_AREAS self -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem. @@ -66094,9 +70997,15 @@ function RAT:_InitAircraft(DCSgroup) --self.aircraft.descriptors=DCSdesc -- aircraft dimensions - self.aircraft.length=DCSdesc.box.max.x - self.aircraft.height=DCSdesc.box.max.y - self.aircraft.width=DCSdesc.box.max.z + if DCSdesc.box then + self.aircraft.length=DCSdesc.box.max.x + self.aircraft.height=DCSdesc.box.max.y + self.aircraft.width=DCSdesc.box.max.z + elseif DCStype == "Mirage-F1CE" then + self.aircraft.length=16 + self.aircraft.height=5 + self.aircraft.width=9 + end self.aircraft.box=math.max(self.aircraft.length,self.aircraft.width) -- info message @@ -70211,23 +75120,23 @@ end -- * Bomb, rocket and missile impact points can be marked by smoke. -- * Direct hits on targets can trigger flares. -- * Smoke and flare colors can be adjusted for each player via radio menu. --- * Range information and weather report at the range can be reported via radio menu. +-- * Range information and weather at the range can be obtained via radio menu. -- * Persistence: Bombing range results can be saved to disk and loaded the next time the mission is started. -- * Range control voice overs (>40) for hit assessment. -- -- === -- -- ## Youtube Videos: - -- +-- -- * [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) -- * [MOOSE - On the Range - Demonstration Video](https://www.youtube.com/watch?v=kIXcxNB9_3M) --- +-- -- === -- -- ## Missions: -- -- * [MAR - On the Range - MOOSE - SC](https://www.digitalcombatsimulator.com/en/files/3317765/) by shagrat --- +-- -- === -- -- ## Sound files: [MOOSE Sound Files](https://github.com/FlightControl-Master/MOOSE_SOUND/releases) @@ -70242,11 +75151,10 @@ end -- @module Functional.Range -- @image Range.JPG -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- RANGE class -- @type RANGE -- @field #string ClassName Name of the Class. --- @field #boolean Debug If true, debug info is send as messages on the screen. +-- @field #boolean Debug If true, debug info is sent as messages on the screen. -- @field #boolean verbose Verbosity level. Higher means more output to DCS log file. -- @field #string id String id of range for output in DCS log. -- @field #string rangename Name of the range. @@ -70269,13 +75177,13 @@ end -- @field #number Tmsg Time [sec] messages to players are displayed. Default 30 sec. -- @field #string examinergroupname Name of the examiner group which should get all messages. -- @field #boolean examinerexclusive If true, only the examiner gets messages. If false, clients and examiner get messages. --- @field #number strafemaxalt Maximum altitude above ground for registering for a strafe run. Default is 914 m = 3000 ft. +-- @field #number strafemaxalt Maximum altitude in meters AGL for registering for a strafe run. Default is 914 m = 3000 ft. -- @field #number ndisplayresult Number of (player) results that a displayed. Default is 10. -- @field Utilities.Utils#SMOKECOLOR BombSmokeColor Color id used for smoking bomb targets. -- @field Utilities.Utils#SMOKECOLOR StrafeSmokeColor Color id used to smoke strafe targets. -- @field Utilities.Utils#SMOKECOLOR StrafePitSmokeColor Color id used to smoke strafe pit approach boxes. --- @field #number illuminationminalt Minimum altitude AGL in meters at which illumination bombs are fired. Default is 500 m. --- @field #number illuminationmaxalt Maximum altitude AGL in meters at which illumination bombs are fired. Default is 1000 m. +-- @field #number illuminationminalt Minimum altitude in meters AGL at which illumination bombs are fired. Default is 500 m. +-- @field #number illuminationmaxalt Maximum altitude in meters AGL at which illumination bombs are fired. Default is 1000 m. -- @field #number scorebombdistance Distance from closest target up to which bomb hits are counted. Default 1000 m. -- @field #number TdelaySmoke Time delay in seconds between impact of bomb and starting the smoke. Default 3 seconds. -- @field #boolean eventmoose If true, events are handled by MOOSE. If false, events are handled directly by DCS eventhandler. Default true. @@ -70291,10 +75199,13 @@ end -- @field #string rangecontrolrelayname Name of relay unit. -- @field #string instructorrelayname Name of relay unit. -- @field #string soundpath Path inside miz file where the sound files are located. Default is "Range Soundfiles/". +-- @field #boolean targetsheet If true, players can save their target sheets. Rangeboss will not work if targetsheets do not save. +-- @field #string targetpath Path where to save the target sheets. +-- @field #string targetprefix File prefix for target sheet files. -- @extends Core.Fsm#FSM --- *Don't only practice your art, but force your way into its secrets; art deserves that, for it and knowledge can raise man to the Divine.* - Ludwig van Beethoven --- +-- -- === -- -- ![Banner Image](..\Presentations\RANGE\RANGE_Main.png) @@ -70318,17 +75229,17 @@ end -- there should be an "On the Range" menu items in the "F10. Other..." menu. -- -- # Strafe Pits --- +-- -- Each strafe pit can consist of multiple targets. Often one finds two or three strafe targets next to each other. -- -- A strafe pit can be added to the range by the @{#RANGE.AddStrafePit}(*targetnames, boxlength, boxwidth, heading, inverseheading, goodpass, foulline*) function. -- --- * The first parameter *targetnames* defines the target or targets. This has to be given as a lua table which contains the names of @{Wrapper.Unit} or @{Static} objects defined in the mission editor. +-- * The first parameter *targetnames* defines the target or targets. This can be a single item or a Table with the name(s) of @{Wrapper.Unit} or @{Static} objects defined in the mission editor. -- * In order to perform a valid pass on the strafe pit, the pilot has to begin his run from the correct direction. Therefore, an "approach box" is defined in front --- of the strafe targets. The parameters *boxlength* and *boxwidth* define the size of the box while the parameter *heading* defines its direction. --- If the parameter *heading* is passed as **nil**, the heading is automatically taken from the heading of the first target unit as defined in the ME. --- The parameter *inverseheading* turns the heading around by 180 degrees. This is sometimes useful, since the default heading of strafe target units point in the --- wrong/opposite direction. +-- of the strafe targets. The parameters *boxlength* and *boxwidth* define the size of the box in meters, while the *heading* parameter defines the heading of the box FROM the target. +-- For example, if heading 120 is set, the approach box will start FROM the target and extend outwards on heading 120. A strafe run approach must then be flown apx. heading 300 TOWARDS the target. +-- If the parameter *heading* is passed as **nil**, the heading is automatically taken from the heading set in the ME for the first target unit. +-- * The parameter *inverseheading* turns the heading around by 180 degrees. This is useful when the default heading of strafe target units point in the wrong/opposite direction. -- * The parameter *goodpass* defines the number of hits a pilot has to achieve during a run to be judged as a "good" pass. -- * The last parameter *foulline* sets the distance from the pit targets to the foul line. Hit from closer than this line are not counted! -- @@ -70339,27 +75250,26 @@ end -- strafing pits of the range and can be adjusted by the @{#RANGE.SetMaxStrafeAlt}(maxalt) function. -- -- # Bombing targets --- +-- -- One ore multiple bombing targets can be added to the range by the @{#RANGE.AddBombingTargets}(targetnames, goodhitrange, randommove) function. -- --- * The first parameter *targetnames* has to be a lua table, which contains the names of @{Wrapper.Unit} and/or @{Static} objects defined in the mission editor. --- Note that the @{Range} logic **automatically** determines, if a name belongs to a @{Wrapper.Unit} or @{Static} object now. --- * The (optional) parameter *goodhitrange* specifies the radius around the target. If a bomb or rocket falls at a distance smaller than this number, the hit is considered to be "good". +-- * The first parameter *targetnames* defines the target or targets. This can be a single item or a Table with the name(s) of @{Wrapper.Unit} or @{Static} objects defined in the mission editor. +-- * The (optional) parameter *goodhitrange* specifies the radius in metres around the target within which a bomb/rocket hit is considered to be "good". -- * If final (optional) parameter "*randommove*" can be enabled to create moving targets. If this parameter is set to true, the units of this bombing target will randomly move within the range zone. -- Note that there might be quirks since DCS units can get stuck in buildings etc. So it might be safer to manually define a route for the units in the mission editor if moving targets are desired. -- -- ## Adding Groups --- +-- -- Another possibility to add bombing targets is the @{#RANGE.AddBombingTargetGroup}(*group, goodhitrange, randommove*) function. Here the parameter *group* is a MOOSE @{Wrapper.Group} object -- and **all** units in this group are defined as bombing targets. --- +-- -- ## Specifying Coordinates --- +-- -- It is also possible to specify coordinates rather than unit or static objects as bombing target locations. This has the advantage, that even when the unit/static object is dead, the specified -- coordinate will still be a valid impact point. This can be done via the @{#RANGE.AddBombingTargetCoordinate}(*coord*, *name*, *goodhitrange*) function. -- -- # Fine Tuning --- +-- -- Many range parameters have good default values. However, the mission designer can change these settings easily with the supplied user functions: -- -- * @{#RANGE.SetMaxStrafeAlt}() sets the max altitude for valid strafing runs. @@ -70375,60 +75285,70 @@ end -- * @{#RANGE.TrackMissilesON}() or @{#RANGE.TrackMissilesOFF}() can be used to enable/disable tracking and evaluating of all missile types a player fires. -- -- # Radio Menu --- +-- -- Each range gets a radio menu with various submenus where each player can adjust his individual settings or request information about the range or his scores. -- -- The main range menu can be found at "F10. Other..." --> "F*X*. On the Range..." --> "F1. ...". -- --- The range menu contains the following submenues: --- +-- The range menu contains the following submenus: +-- -- ![Banner Image](..\Presentations\RANGE\Menu_Main.png) -- -- * "F1. Statistics...": Range results of all players and personal stats. -- * "F2. Mark Targets": Mark range targets by smoke or flares. -- * "F3. My Settings" Personal settings. -- * "F4. Range Info": Information about the range, such as bearing and range. --- +-- -- ## F1 Statistics --- +-- -- ![Banner Image](..\Presentations\RANGE\Menu_Stats.png) --- +-- -- ## F2 Mark Targets --- +-- -- ![Banner Image](..\Presentations\RANGE\Menu_Stats.png) --- +-- -- ## F3 My Settings --- +-- -- ![Banner Image](..\Presentations\RANGE\Menu_MySettings.png) --- +-- -- ## F4 Range Info --- +-- -- ![Banner Image](..\Presentations\RANGE\Menu_RangeInfo.png) --- +-- -- # Voice Overs --- +-- -- Voice over sound files can be downloaded from the Moose Discord. Check the pinned messages in the *#func-range* channel. --- +-- -- Instructor radio will inform players when they enter or exit the range zone and provide the radio frequency of the range control for hit assessment. -- This can be enabled via the @{#RANGE.SetInstructorRadio}(*frequency*) functions, where *frequency* is the AM frequency in MHz. --- +-- -- The range control can be enabled via the @{#RANGE.SetRangeControl}(*frequency*) functions, where *frequency* is the AM frequency in MHz. --- +-- -- By default, the sound files are placed in the "Range Soundfiles/" folder inside the mission (.miz) file. Another folder can be specified via the @{#RANGE.SetSoundfilesPath}(*path*) function. --- +-- -- # Persistence --- +-- -- To automatically save bombing results to disk, use the @{#RANGE.SetAutosave}() function. Bombing results will be saved as csv file in your "Saved Games\DCS.openbeta\Logs" directory. -- Each range has a separate file, which is named "RANGE-<*RangeName*>_BombingResults.csv". --- +-- -- The next time you start the mission, these results are also automatically loaded. --- +-- -- Strafing results are currently **not** saved. +-- +-- # FSM Events +-- +-- This class creates additional events that can be used by mission designers for custom reactions +-- +-- * `EnterRange` when a player enters a range zone. See @{#RANGE.OnAfterEnterRange} +-- * `ExitRange` when a player leaves a range zone. See @{#RANGE.OnAfterExitRange} +-- * `Impact` on impact of a player's weapon on a bombing target. See @{#RANGE.OnAfterImpact} +-- * `RollingIn` when a player rolls in on a strafing target. See @{#RANGE.OnAfterRollingIn} +-- * `StrafeResult` when a player finishes a strafing run. See @{#RANGE.OnAfterStrafeResult} -- -- # Examples -- -- ## Goldwater Range --- +-- -- This example shows hot to set up the [Barry M. Goldwater range](https://en.wikipedia.org/wiki/Barry_M._Goldwater_Air_Force_Range). -- It consists of two strafe pits each has two targets plus three bombing targets. -- @@ -70447,9 +75367,9 @@ end -- -- Note that this could also be done manually by simply measuring the distance between the target and the foul line in the ME. -- GoldwaterRange:GetFoullineDistance("GWR Strafe Pit Left 1", "GWR Foul Line Left") -- --- -- Add strafe pits. Each pit (left and right) consists of two targets. --- GoldwaterRange:AddStrafePit(strafepit_left, 3000, 300, nil, true, 20, fouldist) --- GoldwaterRange:AddStrafePit(strafepit_right, nil, nil, nil, true, nil, fouldist) +-- -- Add strafe pits. Each pit (left and right) consists of two targets. Where "nil" is used as input, the default value is used. +-- GoldwaterRange:AddStrafePit(strafepit_left, 3000, 300, nil, true, 30, 500) +-- GoldwaterRange:AddStrafePit(strafepit_right, nil, nil, nil, true, nil, 500) -- -- -- Add bombing targets. A good hit is if the bomb falls less then 50 m from the target. -- GoldwaterRange:AddBombingTargets(bombtargets, 50) @@ -70459,6 +75379,7 @@ end -- -- The [476th - Air Weapons Range Objects mod](http://www.476vfightergroup.com/downloads.php?do=file&id=287) is (implicitly) used in this example. -- +-- -- # Debugging -- -- In case you have problems, it is always a good idea to have a look at your DCS log file. You find it in your "Saved Games" folder, so for example in @@ -70478,71 +75399,71 @@ end -- Note that it can happen that the RANGE radio menu is not shown. Check that the range object is defined as a **global** variable rather than a local one. -- The could avoid the lua garbage collection to accidentally/falsely deallocate the RANGE objects. -- --- --- -- @field #RANGE -RANGE={ - ClassName = "RANGE", - Debug = false, - verbose = 0, - id = nil, - rangename = nil, - location = nil, - messages = true, - rangeradius = 5000, - rangezone = nil, - strafeTargets = {}, - bombingTargets = {}, - nbombtargets = 0, - nstrafetargets = 0, - MenuAddedTo = {}, - planes = {}, - strafeStatus = {}, +RANGE = { + ClassName = "RANGE", + Debug = false, + verbose = 0, + id = nil, + rangename = nil, + location = nil, + messages = true, + rangeradius = 5000, + rangezone = nil, + strafeTargets = {}, + bombingTargets = {}, + nbombtargets = 0, + nstrafetargets = 0, + MenuAddedTo = {}, + planes = {}, + strafeStatus = {}, strafePlayerResults = {}, - bombPlayerResults = {}, - PlayerSettings = {}, - dtBombtrack = 0.005, - BombtrackThreshold = 25000, - Tmsg = 30, - examinergroupname = nil, - examinerexclusive = nil, - strafemaxalt = 914, - ndisplayresult = 10, - BombSmokeColor = SMOKECOLOR.Red, - StrafeSmokeColor = SMOKECOLOR.Green, + bombPlayerResults = {}, + PlayerSettings = {}, + dtBombtrack = 0.005, + BombtrackThreshold = 25000, + Tmsg = 30, + examinergroupname = nil, + examinerexclusive = nil, + strafemaxalt = 914, + ndisplayresult = 10, + BombSmokeColor = SMOKECOLOR.Red, + StrafeSmokeColor = SMOKECOLOR.Green, StrafePitSmokeColor = SMOKECOLOR.White, - illuminationminalt = 500, - illuminationmaxalt = 1000, - scorebombdistance = 1000, - TdelaySmoke = 3.0, - eventmoose = true, - trackbombs = true, - trackrockets = true, - trackmissiles = true, - defaultsmokebomb = true, - autosave = false, - instructorfreq = nil, - instructor = nil, - rangecontrolfreq = nil, - rangecontrol = nil, - soundpath = "Range Soundfiles/" + illuminationminalt = 500, + illuminationmaxalt = 1000, + scorebombdistance = 1000, + TdelaySmoke = 3.0, + eventmoose = true, + trackbombs = true, + trackrockets = true, + trackmissiles = true, + defaultsmokebomb = true, + autosave = false, + instructorfreq = nil, + instructor = nil, + rangecontrolfreq = nil, + rangecontrol = nil, + soundpath = "Range Soundfiles/", + targetsheet = nil, + targetpath = nil, + targetprefix = nil, } --- Default range parameters. -- @list Defaults -RANGE.Defaults={ - goodhitrange=25, - strafemaxalt=914, - dtBombtrack=0.005, - Tmsg=30, - ndisplayresult=10, - rangeradius=5000, - TdelaySmoke=3.0, - boxlength=3000, - boxwidth=300, - goodpass=20, - goodhitrange=25, - foulline=610, +RANGE.Defaults = { + goodhitrange = 25, + strafemaxalt = 914, + dtBombtrack = 0.005, + Tmsg = 30, + ndisplayresult = 10, + rangeradius = 5000, + TdelaySmoke = 3.0, + boxlength = 3000, + boxwidth = 300, + goodpass = 20, + foulline = 610 } --- Target type, i.e. unit, static, or coordinate. @@ -70550,10 +75471,10 @@ RANGE.Defaults={ -- @field #string UNIT Target is a unit. -- @field #string STATIC Target is a static. -- @field #string COORD Target is a coordinate. -RANGE.TargetType={ - UNIT="Unit", - STATIC="Static", - COORD="Coordinate", +RANGE.TargetType = { + UNIT = "Unit", + STATIC = "Static", + COORD = "Coordinate" } --- Player settings. @@ -70590,6 +75511,14 @@ RANGE.TargetType={ -- @field #number smokepoints Number of smoke points. -- @field #number heading Heading of pit. +--- Strafe status for player. +-- @type RANGE.StrafeStatus +-- @field #number hits Number of hits on target. +-- @field #number time Number of times. +-- @field #number ammo Amount of ammo. +-- @field #boolean pastfoulline If `true`, player passed foul line. Invalid pass. +-- @field #RANGE.StrafeTarget zone Strafe target. + --- Bomb target result. -- @type RANGE.BombResult -- @field #string name Name of closest target. @@ -70602,6 +75531,13 @@ RANGE.TargetType={ -- @field #number time Time via timer.getAbsTime() in seconds of impact. -- @field #string date OS date. +--- Strafe result. +-- @type RANGE.StrafeResult +-- @field #string player Player name. +-- @field #string airframe Aircraft type of player. +-- @field #number time Time via timer.getAbsTime() in seconds of impact. +-- @field #string date OS date. + --- Sound file data. -- @type RANGE.Soundfile -- @field #string filename Name of the file @@ -70654,81 +75590,81 @@ RANGE.TargetType={ -- @field #RANGE.Soundfile IREnterRange -- @field #RANGE.Soundfile IRExitRange RANGE.Sound = { - RC0={filename="RC-0.ogg", duration=0.60}, - RC1={filename="RC-1.ogg", duration=0.47}, - RC2={filename="RC-2.ogg", duration=0.43}, - RC3={filename="RC-3.ogg", duration=0.50}, - RC4={filename="RC-4.ogg", duration=0.58}, - RC5={filename="RC-5.ogg", duration=0.54}, - RC6={filename="RC-6.ogg", duration=0.61}, - RC7={filename="RC-7.ogg", duration=0.53}, - RC8={filename="RC-8.ogg", duration=0.34}, - RC9={filename="RC-9.ogg", duration=0.54}, - RCAccuracy={filename="RC-Accuracy.ogg", duration=0.67}, - RCDegrees={filename="RC-Degrees.ogg", duration=0.59}, - RCExcellentHit={filename="RC-ExcellentHit.ogg", duration=0.76}, - RCExcellentPass={filename="RC-ExcellentPass.ogg", duration=0.89}, - RCFeet={filename="RC-Feet.ogg", duration=0.49}, - RCFor={filename="RC-For.ogg", duration=0.64}, - RCGoodHit={filename="RC-GoodHit.ogg", duration=0.52}, - RCGoodPass={filename="RC-GoodPass.ogg", duration=0.62}, - RCHitsOnTarget={filename="RC-HitsOnTarget.ogg", duration=0.88}, - RCImpact={filename="RC-Impact.ogg", duration=0.61}, - RCIneffectiveHit={filename="RC-IneffectiveHit.ogg", duration=0.86}, - RCIneffectivePass={filename="RC-IneffectivePass.ogg", duration=0.99}, - RCInvalidHit={filename="RC-InvalidHit.ogg", duration=2.97}, - RCLeftStrafePitTooQuickly={filename="RC-LeftStrafePitTooQuickly.ogg", duration=3.09}, - RCPercent={filename="RC-Percent.ogg", duration=0.56}, - RCPoorHit={filename="RC-PoorHit.ogg", duration=0.54}, - RCPoorPass={filename="RC-PoorPass.ogg", duration=0.68}, - RCRollingInOnStrafeTarget={filename="RC-RollingInOnStrafeTarget.ogg", duration=1.38}, - RCTotalRoundsFired={filename="RC-TotalRoundsFired.ogg", duration=1.22}, - RCWeaponImpactedTooFar={filename="RC-WeaponImpactedTooFar.ogg", duration=3.73}, - IR0={filename="IR-0.ogg", duration=0.55}, - IR1={filename="IR-1.ogg", duration=0.41}, - IR2={filename="IR-2.ogg", duration=0.37}, - IR3={filename="IR-3.ogg", duration=0.41}, - IR4={filename="IR-4.ogg", duration=0.37}, - IR5={filename="IR-5.ogg", duration=0.43}, - IR6={filename="IR-6.ogg", duration=0.55}, - IR7={filename="IR-7.ogg", duration=0.43}, - IR8={filename="IR-8.ogg", duration=0.38}, - IR9={filename="IR-9.ogg", duration=0.55}, - IRDecimal={filename="IR-Decimal.ogg", duration=0.54}, - IRMegaHertz={filename="IR-MegaHertz.ogg", duration=0.87}, - IREnterRange={filename="IR-EnterRange.ogg", duration=4.83}, - IRExitRange={filename="IR-ExitRange.ogg", duration=3.10}, + RC0 = { filename = "RC-0.ogg", duration = 0.60 }, + RC1 = { filename = "RC-1.ogg", duration = 0.47 }, + RC2 = { filename = "RC-2.ogg", duration = 0.43 }, + RC3 = { filename = "RC-3.ogg", duration = 0.50 }, + RC4 = { filename = "RC-4.ogg", duration = 0.58 }, + RC5 = { filename = "RC-5.ogg", duration = 0.54 }, + RC6 = { filename = "RC-6.ogg", duration = 0.61 }, + RC7 = { filename = "RC-7.ogg", duration = 0.53 }, + RC8 = { filename = "RC-8.ogg", duration = 0.34 }, + RC9 = { filename = "RC-9.ogg", duration = 0.54 }, + RCAccuracy = { filename = "RC-Accuracy.ogg", duration = 0.67 }, + RCDegrees = { filename = "RC-Degrees.ogg", duration = 0.59 }, + RCExcellentHit = { filename = "RC-ExcellentHit.ogg", duration = 0.76 }, + RCExcellentPass = { filename = "RC-ExcellentPass.ogg", duration = 0.89 }, + RCFeet = { filename = "RC-Feet.ogg", duration = 0.49 }, + RCFor = { filename = "RC-For.ogg", duration = 0.64 }, + RCGoodHit = { filename = "RC-GoodHit.ogg", duration = 0.52 }, + RCGoodPass = { filename = "RC-GoodPass.ogg", duration = 0.62 }, + RCHitsOnTarget = { filename = "RC-HitsOnTarget.ogg", duration = 0.88 }, + RCImpact = { filename = "RC-Impact.ogg", duration = 0.61 }, + RCIneffectiveHit = { filename = "RC-IneffectiveHit.ogg", duration = 0.86 }, + RCIneffectivePass = { filename = "RC-IneffectivePass.ogg", duration = 0.99 }, + RCInvalidHit = { filename = "RC-InvalidHit.ogg", duration = 2.97 }, + RCLeftStrafePitTooQuickly = { filename = "RC-LeftStrafePitTooQuickly.ogg", duration = 3.09 }, + RCPercent = { filename = "RC-Percent.ogg", duration = 0.56 }, + RCPoorHit = { filename = "RC-PoorHit.ogg", duration = 0.54 }, + RCPoorPass = { filename = "RC-PoorPass.ogg", duration = 0.68 }, + RCRollingInOnStrafeTarget = { filename = "RC-RollingInOnStrafeTarget.ogg", duration = 1.38 }, + RCTotalRoundsFired = { filename = "RC-TotalRoundsFired.ogg", duration = 1.22 }, + RCWeaponImpactedTooFar = { filename = "RC-WeaponImpactedTooFar.ogg", duration = 3.73 }, + IR0 = { filename = "IR-0.ogg", duration = 0.55 }, + IR1 = { filename = "IR-1.ogg", duration = 0.41 }, + IR2 = { filename = "IR-2.ogg", duration = 0.37 }, + IR3 = { filename = "IR-3.ogg", duration = 0.41 }, + IR4 = { filename = "IR-4.ogg", duration = 0.37 }, + IR5 = { filename = "IR-5.ogg", duration = 0.43 }, + IR6 = { filename = "IR-6.ogg", duration = 0.55 }, + IR7 = { filename = "IR-7.ogg", duration = 0.43 }, + IR8 = { filename = "IR-8.ogg", duration = 0.38 }, + IR9 = { filename = "IR-9.ogg", duration = 0.55 }, + IRDecimal = { filename = "IR-Decimal.ogg", duration = 0.54 }, + IRMegaHertz = { filename = "IR-MegaHertz.ogg", duration = 0.87 }, + IREnterRange = { filename = "IR-EnterRange.ogg", duration = 4.83 }, + IRExitRange = { filename = "IR-ExitRange.ogg", duration = 3.10 }, } --- Global list of all defined range names. -- @field #table Names -RANGE.Names={} +RANGE.Names = {} --- Main radio menu on group level. -- @field #table MenuF10 Root menu table on group level. -RANGE.MenuF10={} +RANGE.MenuF10 = {} --- Main radio menu on mission level. -- @field #table MenuF10Root Root menu on mission level. -RANGE.MenuF10Root=nil +RANGE.MenuF10Root = nil --- Range script version. -- @field #string version -RANGE.version="2.3.0" +RANGE.version = "2.4.0" ---TODO list: ---TODO: Verbosity level for messages. ---TODO: Add option for default settings such as smoke off. ---TODO: Add custom weapons, which can be specified by the user. ---TODO: Check if units are still alive. ---DONE: Add statics for strafe pits. ---DONE: Add missiles. ---DONE: Convert env.info() to self:T() ---DONE: Add user functions. ---DONE: Rename private functions, i.e. start with _functionname. ---DONE: number of displayed results variable. ---DONE: Add tire option for strafe pits. ==> No really feasible since tires are very small and cannot be seen. ---DONE: Check that menu texts are short enough to be correctly displayed in VR. +-- TODO list: +-- TODO: Verbosity level for messages. +-- TODO: Add option for default settings such as smoke off. +-- TODO: Add custom weapons, which can be specified by the user. +-- TODO: Check if units are still alive. +-- DONE: Add statics for strafe pits. +-- DONE: Add missiles. +-- DONE: Convert env.info() to self:T() +-- DONE: Add user functions. +-- DONE: Rename private functions, i.e. start with _functionname. +-- DONE: number of displayed results variable. +-- DONE: Add tire option for strafe pits. ==> No really feasible since tires are very small and cannot be seen. +-- DONE: Check that menu texts are short enough to be correctly displayed in VR. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -70736,28 +75672,28 @@ RANGE.version="2.3.0" -- @param #RANGE self -- @param #string rangename Name of the range. Has to be unique. Will we used to create F10 menu items etc. -- @return #RANGE RANGE object. -function RANGE:New(rangename) - BASE:F({rangename=rangename}) +function RANGE:New( rangename ) + BASE:F( { rangename = rangename } ) -- Inherit BASE. - local self=BASE:Inherit(self, FSM:New()) -- #RANGE + local self = BASE:Inherit( self, FSM:New() ) -- #RANGE -- Get range name. - --TODO: make sure that the range name is not given twice. This would lead to problems in the F10 radio menu. - self.rangename=rangename or "Practice Range" + -- TODO: make sure that the range name is not given twice. This would lead to problems in the F10 radio menu. + self.rangename = rangename or "Practice Range" -- Log id. - self.id=string.format("RANGE %s | ", self.rangename) + self.id = string.format( "RANGE %s | ", self.rangename ) -- Debug info. - local text=string.format("Script version %s - creating new RANGE object %s.", RANGE.version, self.rangename) - self:I(self.id..text) + local text = string.format( "Script version %s - creating new RANGE object %s.", RANGE.version, self.rangename ) + self:I( self.id .. text ) -- Defaults self:SetDefaultPlayerSmokeBomb() -- Start State. - self:SetStartState("Stopped") + self:SetStartState( "Stopped" ) --- -- Add FSM transitions. @@ -70765,6 +75701,8 @@ function RANGE:New(rangename) self:AddTransition("Stopped", "Start", "Running") -- Start RANGE script. self:AddTransition("*", "Status", "*") -- Status of RANGE script. self:AddTransition("*", "Impact", "*") -- Impact of bomb/rocket/missile. + self:AddTransition("*", "RollingIn", "*") -- Player rolling in on strafe target. + self:AddTransition("*", "StrafeResult", "*") -- Strafe result of player. self:AddTransition("*", "EnterRange", "*") -- Player enters the range. self:AddTransition("*", "ExitRange", "*") -- Player leaves the range. self:AddTransition("*", "Save", "*") -- Save player results. @@ -70822,6 +75760,37 @@ function RANGE:New(rangename) -- @param #RANGE.BombResult result Data of the bombing run. -- @param #RANGE.PlayerData player Data of player settings etc. + + --- Triggers the FSM event "RollingIn". + -- @function [parent=#RANGE] RollingIn + -- @param #RANGE self + -- @param #RANGE.PlayerData player Data of player settings etc. + -- @param #RANGE.StrafeTarget target Strafe target. + + --- On after "RollingIn" event user function. Called when a player rolls in to a strafe taret. + -- @function [parent=#RANGE] OnAfterRollingIn + -- @param #RANGE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #RANGE.PlayerData player Data of player settings etc. + -- @param #RANGE.StrafeTarget target Strafe target. + + --- Triggers the FSM event "StrafeResult". + -- @function [parent=#RANGE] StrafeResult + -- @param #RANGE self + -- @param #RANGE.PlayerData player Data of player settings etc. + -- @param #RANGE.StrafeResult result Data of the strafing run. + + --- On after "StrafeResult" event user function. Called when a player finished a strafing run. + -- @function [parent=#RANGE] OnAfterStrafeResult + -- @param #RANGE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #RANGE.PlayerData player Data of player settings etc. + -- @param #RANGE.StrafeResult result Data of the strafing run. + --- Triggers the FSM event "EnterRange". -- @function [parent=#RANGE] EnterRange -- @param #RANGE self @@ -70872,136 +75841,136 @@ end function RANGE:onafterStart() -- Location/coordinate of range. - local _location=nil + local _location = nil -- Count bomb targets. - local _count=0 - for _,_target in pairs(self.bombingTargets) do - _count=_count+1 + local _count = 0 + for _, _target in pairs( self.bombingTargets ) do + _count = _count + 1 -- Get range location. - if _location==nil then - _location=self:_GetBombTargetCoordinate(_target) + if _location == nil then + _location = self:_GetBombTargetCoordinate( _target ) end end - self.nbombtargets=_count + self.nbombtargets = _count -- Count strafing targets. - _count=0 - for _,_target in pairs(self.strafeTargets) do - _count=_count+1 + _count = 0 + for _, _target in pairs( self.strafeTargets ) do + _count = _count + 1 - for _,_unit in pairs(_target.targets) do - if _location==nil then - _location=_unit:GetCoordinate() + for _, _unit in pairs( _target.targets ) do + if _location == nil then + _location = _unit:GetCoordinate() end end end - self.nstrafetargets=_count + self.nstrafetargets = _count -- Location of the range. We simply take the first unit/target we find if it was not explicitly specified by the user. - if self.location==nil then - self.location=_location + if self.location == nil then + self.location = _location end - if self.location==nil then - local text=string.format("ERROR! No range location found. Number of strafe targets = %d. Number of bomb targets = %d.", self.nstrafetargets, self.nbombtargets) - self:E(self.id..text) + if self.location == nil then + local text = string.format( "ERROR! No range location found. Number of strafe targets = %d. Number of bomb targets = %d.", self.nstrafetargets, self.nbombtargets ) + self:E( self.id .. text ) return end -- Define a MOOSE zone of the range. - if self.rangezone==nil then - self.rangezone=ZONE_RADIUS:New(self.rangename, {x=self.location.x, y=self.location.z}, self.rangeradius) + if self.rangezone == nil then + self.rangezone = ZONE_RADIUS:New( self.rangename, { x = self.location.x, y = self.location.z }, self.rangeradius ) end -- Starting range. - local text=string.format("Starting RANGE %s. Number of strafe targets = %d. Number of bomb targets = %d.", self.rangename, self.nstrafetargets, self.nbombtargets) - self:I(self.id..text) + local text = string.format( "Starting RANGE %s. Number of strafe targets = %d. Number of bomb targets = %d.", self.rangename, self.nstrafetargets, self.nbombtargets ) + self:I( self.id .. text ) -- Event handling. if self.eventmoose then -- Events are handled my MOOSE. - self:T(self.id.."Events are handled by MOOSE.") - self:HandleEvent(EVENTS.Birth) - self:HandleEvent(EVENTS.Hit) - self:HandleEvent(EVENTS.Shot) + self:T( self.id .. "Events are handled by MOOSE." ) + self:HandleEvent( EVENTS.Birth ) + self:HandleEvent( EVENTS.Hit ) + self:HandleEvent( EVENTS.Shot ) else -- Events are handled directly by DCS. - self:T(self.id.."Events are handled directly by DCS.") - world.addEventHandler(self) + self:T( self.id .. "Events are handled directly by DCS." ) + world.addEventHandler( self ) end -- Make bomb target move randomly within the range zone. - for _,_target in pairs(self.bombingTargets) do + for _, _target in pairs( self.bombingTargets ) do -- Check if it is a static object. - --local _static=self:_CheckStatic(_target.target:GetName()) - local _static=_target.type==RANGE.TargetType.STATIC + -- local _static=self:_CheckStatic(_target.target:GetName()) + local _static = _target.type == RANGE.TargetType.STATIC - if _target.move and _static==false and _target.speed>1 then - local unit=_target.target --Wrapper.Unit#UNIT - _target.target:PatrolZones({self.rangezone}, _target.speed*0.75, "Off road") + if _target.move and _static == false and _target.speed > 1 then + local unit = _target.target -- Wrapper.Unit#UNIT + _target.target:PatrolZones( { self.rangezone }, _target.speed * 0.75, "Off road" ) end end - + -- Init range control. if self.rangecontrolfreq then - + -- Radio queue. - self.rangecontrol=RADIOQUEUE:New(self.rangecontrolfreq, nil, self.rangename) - self.rangecontrol.schedonce=true - + self.rangecontrol = RADIOQUEUE:New( self.rangecontrolfreq, nil, self.rangename ) + self.rangecontrol.schedonce = true + -- Init numbers. - self.rangecontrol:SetDigit(0, RANGE.Sound.RC0.filename, RANGE.Sound.RC0.duration, self.soundpath) - self.rangecontrol:SetDigit(1, RANGE.Sound.RC1.filename, RANGE.Sound.RC1.duration, self.soundpath) - self.rangecontrol:SetDigit(2, RANGE.Sound.RC2.filename, RANGE.Sound.RC2.duration, self.soundpath) - self.rangecontrol:SetDigit(3, RANGE.Sound.RC3.filename, RANGE.Sound.RC3.duration, self.soundpath) - self.rangecontrol:SetDigit(4, RANGE.Sound.RC4.filename, RANGE.Sound.RC4.duration, self.soundpath) - self.rangecontrol:SetDigit(5, RANGE.Sound.RC5.filename, RANGE.Sound.RC5.duration, self.soundpath) - self.rangecontrol:SetDigit(6, RANGE.Sound.RC6.filename, RANGE.Sound.RC6.duration, self.soundpath) - self.rangecontrol:SetDigit(7, RANGE.Sound.RC7.filename, RANGE.Sound.RC7.duration, self.soundpath) - self.rangecontrol:SetDigit(8, RANGE.Sound.RC8.filename, RANGE.Sound.RC8.duration, self.soundpath) - self.rangecontrol:SetDigit(9, RANGE.Sound.RC9.filename, RANGE.Sound.RC9.duration, self.soundpath) - + self.rangecontrol:SetDigit( 0, RANGE.Sound.RC0.filename, RANGE.Sound.RC0.duration, self.soundpath ) + self.rangecontrol:SetDigit( 1, RANGE.Sound.RC1.filename, RANGE.Sound.RC1.duration, self.soundpath ) + self.rangecontrol:SetDigit( 2, RANGE.Sound.RC2.filename, RANGE.Sound.RC2.duration, self.soundpath ) + self.rangecontrol:SetDigit( 3, RANGE.Sound.RC3.filename, RANGE.Sound.RC3.duration, self.soundpath ) + self.rangecontrol:SetDigit( 4, RANGE.Sound.RC4.filename, RANGE.Sound.RC4.duration, self.soundpath ) + self.rangecontrol:SetDigit( 5, RANGE.Sound.RC5.filename, RANGE.Sound.RC5.duration, self.soundpath ) + self.rangecontrol:SetDigit( 6, RANGE.Sound.RC6.filename, RANGE.Sound.RC6.duration, self.soundpath ) + self.rangecontrol:SetDigit( 7, RANGE.Sound.RC7.filename, RANGE.Sound.RC7.duration, self.soundpath ) + self.rangecontrol:SetDigit( 8, RANGE.Sound.RC8.filename, RANGE.Sound.RC8.duration, self.soundpath ) + self.rangecontrol:SetDigit( 9, RANGE.Sound.RC9.filename, RANGE.Sound.RC9.duration, self.soundpath ) + -- Set location where the messages are transmitted from. - self.rangecontrol:SetSenderCoordinate(self.location) - self.rangecontrol:SetSenderUnitName(self.rangecontrolrelayname) - + self.rangecontrol:SetSenderCoordinate( self.location ) + self.rangecontrol:SetSenderUnitName( self.rangecontrolrelayname ) + -- Start range control radio queue. - self.rangecontrol:Start(1, 0.1) + self.rangecontrol:Start( 1, 0.1 ) -- Init range control. if self.instructorfreq then - + -- Radio queue. - self.instructor=RADIOQUEUE:New(self.instructorfreq, nil, self.rangename) - self.instructor.schedonce=true - + self.instructor = RADIOQUEUE:New( self.instructorfreq, nil, self.rangename ) + self.instructor.schedonce = true + -- Init numbers. - self.instructor:SetDigit(0, RANGE.Sound.IR0.filename, RANGE.Sound.IR0.duration, self.soundpath) - self.instructor:SetDigit(1, RANGE.Sound.IR1.filename, RANGE.Sound.IR1.duration, self.soundpath) - self.instructor:SetDigit(2, RANGE.Sound.IR2.filename, RANGE.Sound.IR2.duration, self.soundpath) - self.instructor:SetDigit(3, RANGE.Sound.IR3.filename, RANGE.Sound.IR3.duration, self.soundpath) - self.instructor:SetDigit(4, RANGE.Sound.IR4.filename, RANGE.Sound.IR4.duration, self.soundpath) - self.instructor:SetDigit(5, RANGE.Sound.IR5.filename, RANGE.Sound.IR5.duration, self.soundpath) - self.instructor:SetDigit(6, RANGE.Sound.IR6.filename, RANGE.Sound.IR6.duration, self.soundpath) - self.instructor:SetDigit(7, RANGE.Sound.IR7.filename, RANGE.Sound.IR7.duration, self.soundpath) - self.instructor:SetDigit(8, RANGE.Sound.IR8.filename, RANGE.Sound.IR8.duration, self.soundpath) - self.instructor:SetDigit(9, RANGE.Sound.IR9.filename, RANGE.Sound.IR9.duration, self.soundpath) - + self.instructor:SetDigit( 0, RANGE.Sound.IR0.filename, RANGE.Sound.IR0.duration, self.soundpath ) + self.instructor:SetDigit( 1, RANGE.Sound.IR1.filename, RANGE.Sound.IR1.duration, self.soundpath ) + self.instructor:SetDigit( 2, RANGE.Sound.IR2.filename, RANGE.Sound.IR2.duration, self.soundpath ) + self.instructor:SetDigit( 3, RANGE.Sound.IR3.filename, RANGE.Sound.IR3.duration, self.soundpath ) + self.instructor:SetDigit( 4, RANGE.Sound.IR4.filename, RANGE.Sound.IR4.duration, self.soundpath ) + self.instructor:SetDigit( 5, RANGE.Sound.IR5.filename, RANGE.Sound.IR5.duration, self.soundpath ) + self.instructor:SetDigit( 6, RANGE.Sound.IR6.filename, RANGE.Sound.IR6.duration, self.soundpath ) + self.instructor:SetDigit( 7, RANGE.Sound.IR7.filename, RANGE.Sound.IR7.duration, self.soundpath ) + self.instructor:SetDigit( 8, RANGE.Sound.IR8.filename, RANGE.Sound.IR8.duration, self.soundpath ) + self.instructor:SetDigit( 9, RANGE.Sound.IR9.filename, RANGE.Sound.IR9.duration, self.soundpath ) + -- Set location where the messages are transmitted from. - self.instructor:SetSenderCoordinate(self.location) - self.instructor:SetSenderUnitName(self.instructorrelayname) - + self.instructor:SetSenderCoordinate( self.location ) + self.instructor:SetSenderUnitName( self.instructorrelayname ) + -- Start instructor radio queue. - self.instructor:Start(1, 0.1) - + self.instructor:Start( 1, 0.1 ) + end - + end - + -- Load prev results. if self.autosave then self:Load() @@ -71013,10 +75982,10 @@ function RANGE:onafterStart() self:_SmokeBombTargets() self:_SmokeStrafeTargets() self:_SmokeStrafeTargetBoxes() - self.rangezone:SmokeZone(SMOKECOLOR.White) + self.rangezone:SmokeZone( SMOKECOLOR.White ) end - self:__Status(-60) + self:__Status( -60 ) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -71025,10 +75994,10 @@ end --- Set maximal strafing altitude. Player entering a strafe pit above that altitude are not registered for a valid pass. -- @param #RANGE self --- @param #number maxalt Maximum altitude AGL in meters. Default is 914 m= 3000 ft. +-- @param #number maxalt Maximum altitude in meters AGL. Default is 914 m = 3000 ft. -- @return #RANGE self -function RANGE:SetMaxStrafeAlt(maxalt) - self.strafemaxalt=maxalt or RANGE.Defaults.strafemaxalt +function RANGE:SetMaxStrafeAlt( maxalt ) + self.strafemaxalt = maxalt or RANGE.Defaults.strafemaxalt return self end @@ -71036,8 +76005,8 @@ end -- @param #RANGE self -- @param #number dt Time interval in seconds. Default is 0.005 s. -- @return #RANGE self -function RANGE:SetBombtrackTimestep(dt) - self.dtBombtrack=dt or RANGE.Defaults.dtBombtrack +function RANGE:SetBombtrackTimestep( dt ) + self.dtBombtrack = dt or RANGE.Defaults.dtBombtrack return self end @@ -71045,8 +76014,8 @@ end -- @param #RANGE self -- @param #number time Time in seconds. Default is 30 s. -- @return #RANGE self -function RANGE:SetMessageTimeDuration(time) - self.Tmsg=time or RANGE.Defaults.Tmsg +function RANGE:SetMessageTimeDuration( time ) + self.Tmsg = time or RANGE.Defaults.Tmsg return self end @@ -71054,7 +76023,7 @@ end -- @param #RANGE self -- @return #RANGE self function RANGE:SetAutosaveOn() - self.autosave=true + self.autosave = true return self end @@ -71062,7 +76031,23 @@ end -- @param #RANGE self -- @return #RANGE self function RANGE:SetAutosaveOff() - self.autosave=false + self.autosave = false + return self +end + +--- Enable saving of player's target sheets and specify an optional directory path. +-- @param #RANGE self +-- @param #string path (Optional) Path where to save the target sheets. +-- @param #string prefix (Optional) Prefix for target sheet files. File name will be saved as *prefix_aircrafttype-0001.csv*, *prefix_aircrafttype-0002.csv*, etc. +-- @return #RANGE self +function RANGE:SetTargetSheet( path, prefix ) + if io then + self.targetsheet = true + self.targetpath = path + self.targetprefix = prefix + else + self:E( self.lid .. "ERROR: io is not desanitized. Cannot save target sheet." ) + end return self end @@ -71071,9 +76056,9 @@ end -- @param #string examinergroupname Name of the group of the examiner. -- @param #boolean exclusively If true, messages are send exclusively to the examiner, i.e. not to the clients. -- @return #RANGE self -function RANGE:SetMessageToExaminer(examinergroupname, exclusively) - self.examinergroupname=examinergroupname - self.examinerexclusive=exclusively +function RANGE:SetMessageToExaminer( examinergroupname, exclusively ) + self.examinergroupname = examinergroupname + self.examinerexclusive = exclusively return self end @@ -71081,8 +76066,8 @@ end -- @param #RANGE self -- @param #number nmax Number of results. Default is 10. -- @return #RANGE self -function RANGE:SetDisplayedMaxPlayerResults(nmax) - self.ndisplayresult=nmax or RANGE.Defaults.ndisplayresult +function RANGE:SetDisplayedMaxPlayerResults( nmax ) + self.ndisplayresult = nmax or RANGE.Defaults.ndisplayresult return self end @@ -71090,8 +76075,8 @@ end -- @param #RANGE self -- @param #number radius Radius in km. Default 5 km. -- @return #RANGE self -function RANGE:SetRangeRadius(radius) - self.rangeradius=radius*1000 or RANGE.Defaults.rangeradius +function RANGE:SetRangeRadius( radius ) + self.rangeradius = radius * 1000 or RANGE.Defaults.rangeradius return self end @@ -71099,11 +76084,11 @@ end -- @param #RANGE self -- @param #boolean switch If true nor nil default is to smoke impact points of bombs. -- @return #RANGE self -function RANGE:SetDefaultPlayerSmokeBomb(switch) - if switch==true or switch==nil then - self.defaultsmokebomb=true +function RANGE:SetDefaultPlayerSmokeBomb( switch ) + if switch == true or switch == nil then + self.defaultsmokebomb = true else - self.defaultsmokebomb=false + self.defaultsmokebomb = false end return self end @@ -71112,8 +76097,8 @@ end -- @param #RANGE self -- @param #number distance Threshold distance in km. Default 25 km. -- @return #RANGE self -function RANGE:SetBombtrackThreshold(distance) - self.BombtrackThreshold=(distance or 25)*1000 +function RANGE:SetBombtrackThreshold( distance ) + self.BombtrackThreshold = (distance or 25) * 1000 return self end @@ -71122,8 +76107,8 @@ end -- @param #RANGE self -- @param Core.Point#COORDINATE coordinate Coordinate of the range. -- @return #RANGE self -function RANGE:SetRangeLocation(coordinate) - self.location=coordinate +function RANGE:SetRangeLocation( coordinate ) + self.location = coordinate return self end @@ -71132,8 +76117,8 @@ end -- @param #RANGE self -- @param Core.Zone#ZONE zone MOOSE zone defining the range perimeters. -- @return #RANGE self -function RANGE:SetRangeZone(zone) - self.rangezone=zone +function RANGE:SetRangeZone( zone ) + self.rangezone = zone return self end @@ -71141,8 +76126,8 @@ end -- @param #RANGE self -- @param Utilities.Utils#SMOKECOLOR colorid Color id. Default SMOKECOLOR.Red. -- @return #RANGE self -function RANGE:SetBombTargetSmokeColor(colorid) - self.BombSmokeColor=colorid or SMOKECOLOR.Red +function RANGE:SetBombTargetSmokeColor( colorid ) + self.BombSmokeColor = colorid or SMOKECOLOR.Red return self end @@ -71150,8 +76135,8 @@ end -- @param #RANGE self -- @param #number distance Distance in meters. Default 1000 m. -- @return #RANGE self -function RANGE:SetScoreBombDistance(distance) - self.scorebombdistance=distance or 1000 +function RANGE:SetScoreBombDistance( distance ) + self.scorebombdistance = distance or 1000 return self end @@ -71159,8 +76144,8 @@ end -- @param #RANGE self -- @param Utilities.Utils#SMOKECOLOR colorid Color id. Default SMOKECOLOR.Green. -- @return #RANGE self -function RANGE:SetStrafeTargetSmokeColor(colorid) - self.StrafeSmokeColor=colorid or SMOKECOLOR.Green +function RANGE:SetStrafeTargetSmokeColor( colorid ) + self.StrafeSmokeColor = colorid or SMOKECOLOR.Green return self end @@ -71168,8 +76153,8 @@ end -- @param #RANGE self -- @param Utilities.Utils#SMOKECOLOR colorid Color id. Default SMOKECOLOR.White. -- @return #RANGE self -function RANGE:SetStrafePitSmokeColor(colorid) - self.StrafePitSmokeColor=colorid or SMOKECOLOR.White +function RANGE:SetStrafePitSmokeColor( colorid ) + self.StrafePitSmokeColor = colorid or SMOKECOLOR.White return self end @@ -71177,8 +76162,8 @@ end -- @param #RANGE self -- @param #number delay Time delay in seconds. Default is 3 seconds. -- @return #RANGE self -function RANGE:SetSmokeTimeDelay(delay) - self.TdelaySmoke=delay or RANGE.Defaults.TdelaySmoke +function RANGE:SetSmokeTimeDelay( delay ) + self.TdelaySmoke = delay or RANGE.Defaults.TdelaySmoke return self end @@ -71186,7 +76171,7 @@ end -- @param #RANGE self -- @return #RANGE self function RANGE:DebugON() - self.Debug=true + self.Debug = true return self end @@ -71194,7 +76179,7 @@ end -- @param #RANGE self -- @return #RANGE self function RANGE:DebugOFF() - self.Debug=false + self.Debug = false return self end @@ -71202,7 +76187,7 @@ end -- @param #RANGE self -- @return #RANGE self function RANGE:SetMessagesOFF() - self.messages=false + self.messages = false return self end @@ -71210,16 +76195,15 @@ end -- @param #RANGE self -- @return #RANGE self function RANGE:SetMessagesON() - self.messages=true + self.messages = true return self end - --- Enables tracking of all bomb types. Note that this is the default setting. -- @param #RANGE self -- @return #RANGE self function RANGE:TrackBombsON() - self.trackbombs=true + self.trackbombs = true return self end @@ -71227,7 +76211,7 @@ end -- @param #RANGE self -- @return #RANGE self function RANGE:TrackBombsOFF() - self.trackbombs=false + self.trackbombs = false return self end @@ -71235,7 +76219,7 @@ end -- @param #RANGE self -- @return #RANGE self function RANGE:TrackRocketsON() - self.trackrockets=true + self.trackrockets = true return self end @@ -71243,7 +76227,7 @@ end -- @param #RANGE self -- @return #RANGE self function RANGE:TrackRocketsOFF() - self.trackrockets=false + self.trackrockets = false return self end @@ -71251,7 +76235,7 @@ end -- @param #RANGE self -- @return #RANGE self function RANGE:TrackMissilesON() - self.trackmissiles=true + self.trackmissiles = true return self end @@ -71259,19 +76243,18 @@ end -- @param #RANGE self -- @return #RANGE self function RANGE:TrackMissilesOFF() - self.trackmissiles=false + self.trackmissiles = false return self end - --- Enable range control and set frequency. -- @param #RANGE self -- @param #number frequency Frequency in MHz. Default 256 MHz. -- @param #string relayunitname Name of the unit used for transmission. -- @return #RANGE self -function RANGE:SetRangeControl(frequency, relayunitname) - self.rangecontrolfreq=frequency or 256 - self.rangecontrolrelayname=relayunitname +function RANGE:SetRangeControl( frequency, relayunitname ) + self.rangecontrolfreq = frequency or 256 + self.rangecontrolrelayname = relayunitname return self end @@ -71280,163 +76263,162 @@ end -- @param #number frequency Frequency in MHz. Default 305 MHz. -- @param #string relayunitname Name of the unit used for transmission. -- @return #RANGE self -function RANGE:SetInstructorRadio(frequency, relayunitname) - self.instructorfreq=frequency or 305 - self.instructorrelayname=relayunitname +function RANGE:SetInstructorRadio( frequency, relayunitname ) + self.instructorfreq = frequency or 305 + self.instructorrelayname = relayunitname return self end --- Set sound files folder within miz file. -- @param #RANGE self --- @param #string path Path for sound files. Default "ATIS Soundfiles/". Mind the slash "/" at the end! +-- @param #string path Path for sound files. Default "Range Soundfiles/". Mind the slash "/" at the end! -- @return #RANGE self -function RANGE:SetSoundfilesPath(path) - self.soundpath=tostring(path or "Range Soundfiles/") - self:I(self.id..string.format("Setting sound files path to %s", self.soundpath)) +function RANGE:SetSoundfilesPath( path ) + self.soundpath = tostring( path or "Range Soundfiles/" ) + self:I( self.id .. string.format( "Setting sound files path to %s", self.soundpath ) ) return self end --- Add new strafe pit. For a strafe pit, hits from guns are counted. One pit can consist of several units. --- Note, an approach is only valid, if the player enters via a zone in front of the pit, which defined by boxlength and boxheading. +-- A strafe run approach is only valid if the player enters via a zone in front of the pit, which is defined by boxlength, boxwidth, and heading. -- Furthermore, the player must not be too high and fly in the direction of the pit to make a valid target apporoach. -- @param #RANGE self --- @param #table targetnames Table of unit or static names defining the strafe targets. The first target in the list determines the approach zone (heading and box). +-- @param #table targetnames Single or multiple (Table) unit or static names defining the strafe targets. The first target in the list determines the approach box origin (heading and box). -- @param #number boxlength (Optional) Length of the approach box in meters. Default is 3000 m. -- @param #number boxwidth (Optional) Width of the approach box in meters. Default is 300 m. --- @param #number heading (Optional) Approach heading in Degrees. Default is heading of the unit as defined in the mission editor. --- @param #boolean inverseheading (Optional) Take inverse heading (heading --> heading - 180 Degrees). Default is false. +-- @param #number heading (Optional) Approach box heading in degrees (originating FROM the target). Default is the heading set in the ME for the first target unit +-- @param #boolean inverseheading (Optional) Use inverse heading (heading --> heading - 180 Degrees). Default is false. -- @param #number goodpass (Optional) Number of hits for a "good" strafing pass. Default is 20. --- @param #number foulline (Optional) Foul line distance. Hits from closer than this distance are not counted. Default 610 m = 2000 ft. Set to 0 for no foul line. +-- @param #number foulline (Optional) Foul line distance. Hits from closer than this distance are not counted. Default is 610 m = 2000 ft. Set to 0 for no foul line. -- @return #RANGE self -function RANGE:AddStrafePit(targetnames, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) - self:F({targetnames=targetnames, boxlength=boxlength, boxwidth=boxwidth, heading=heading, inverseheading=inverseheading, goodpass=goodpass, foulline=foulline}) +function RANGE:AddStrafePit( targetnames, boxlength, boxwidth, heading, inverseheading, goodpass, foulline ) + self:F( { targetnames = targetnames, boxlength = boxlength, boxwidth = boxwidth, heading = heading, inverseheading = inverseheading, goodpass = goodpass, foulline = foulline } ) -- Create table if necessary. - if type(targetnames) ~= "table" then - targetnames={targetnames} + if type( targetnames ) ~= "table" then + targetnames = { targetnames } end -- Make targets - local _targets={} - local center=nil --Wrapper.Unit#UNIT - local ntargets=0 + local _targets = {} + local center = nil -- Wrapper.Unit#UNIT + local ntargets = 0 - for _i,_name in ipairs(targetnames) do + for _i, _name in ipairs( targetnames ) do -- Check if we have a static or unit object. - local _isstatic=self:_CheckStatic(_name) + local _isstatic = self:_CheckStatic( _name ) - local unit=nil - if _isstatic==true then + local unit = nil + if _isstatic == true then -- Add static object. - self:T(self.id..string.format("Adding STATIC object %s as strafe target #%d.", _name, _i)) - unit=STATIC:FindByName(_name, false) + self:T( self.id .. string.format( "Adding STATIC object %s as strafe target #%d.", _name, _i ) ) + unit = STATIC:FindByName( _name, false ) - elseif _isstatic==false then + elseif _isstatic == false then -- Add unit object. - self:T(self.id..string.format("Adding UNIT object %s as strafe target #%d.", _name, _i)) - unit=UNIT:FindByName(_name) + self:T( self.id .. string.format( "Adding UNIT object %s as strafe target #%d.", _name, _i ) ) + unit = UNIT:FindByName( _name ) else -- Neither unit nor static object with this name could be found. - local text=string.format("ERROR! Could not find ANY strafe target object with name %s.", _name) - self:E(self.id..text) + local text = string.format( "ERROR! Could not find ANY strafe target object with name %s.", _name ) + self:E( self.id .. text ) end -- Add object to targets. if unit then - table.insert(_targets, unit) + table.insert( _targets, unit ) -- Define center as the first unit we find - if center==nil then - center=unit + if center == nil then + center = unit end - ntargets=ntargets+1 + ntargets = ntargets + 1 end end -- Check if at least one target could be found. - if ntargets==0 then - local text=string.format("ERROR! No strafe target could be found when calling RANGE:AddStrafePit() for range %s", self.rangename) - self:E(self.id..text) + if ntargets == 0 then + local text = string.format( "ERROR! No strafe target could be found when calling RANGE:AddStrafePit() for range %s", self.rangename ) + self:E( self.id .. text ) return end -- Approach box dimensions. - local l=boxlength or RANGE.Defaults.boxlength - local w=(boxwidth or RANGE.Defaults.boxwidth)/2 + local l = boxlength or RANGE.Defaults.boxlength + local w = (boxwidth or RANGE.Defaults.boxwidth) / 2 -- Heading: either manually entered or automatically taken from unit heading. - local heading=heading or center:GetHeading() + local heading = heading or center:GetHeading() -- Invert the heading since some units point in the "wrong" direction. In particular the strafe pit from 476th range objects. if inverseheading ~= nil then if inverseheading then - heading=heading-180 + heading = heading - 180 end end - if heading<0 then - heading=heading+360 + if heading < 0 then + heading = heading + 360 end - if heading>360 then - heading=heading-360 + if heading > 360 then + heading = heading - 360 end -- Number of hits called a "good" pass. - goodpass=goodpass or RANGE.Defaults.goodpass + goodpass = goodpass or RANGE.Defaults.goodpass -- Foule line distance. - foulline=foulline or RANGE.Defaults.foulline + foulline = foulline or RANGE.Defaults.foulline -- Coordinate of the range. - local Ccenter=center:GetCoordinate() + local Ccenter = center:GetCoordinate() -- Name of the target defined as its unit name. - local _name=center:GetName() + local _name = center:GetName() -- Points defining the approach area. - local p={} - p[#p+1]=Ccenter:Translate( w, heading+90) - p[#p+1]= p[#p]:Translate( l, heading) - p[#p+1]= p[#p]:Translate(2*w, heading-90) - p[#p+1]= p[#p]:Translate( -l, heading) + local p = {} + p[#p + 1] = Ccenter:Translate( w, heading + 90 ) + p[#p + 1] = p[#p]:Translate( l, heading ) + p[#p + 1] = p[#p]:Translate( 2 * w, heading - 90 ) + p[#p + 1] = p[#p]:Translate( -l, heading ) - local pv2={} - for i,p in ipairs(p) do - pv2[i]={x=p.x, y=p.z} + local pv2 = {} + for i, p in ipairs( p ) do + pv2[i] = { x = p.x, y = p.z } end -- Create polygon zone. - local _polygon=ZONE_POLYGON_BASE:New(_name, pv2) + local _polygon = ZONE_POLYGON_BASE:New( _name, pv2 ) -- Create tires - --_polygon:BoundZone() + -- _polygon:BoundZone() - local st={} --#RANGE.StrafeTarget - st.name=_name - st.polygon=_polygon - st.coordinate=Ccenter - st.goodPass=goodpass - st.targets=_targets - st.foulline=foulline - st.smokepoints=p - st.heading=heading + local st = {} -- #RANGE.StrafeTarget + st.name = _name + st.polygon = _polygon + st.coordinate = Ccenter + st.goodPass = goodpass + st.targets = _targets + st.foulline = foulline + st.smokepoints = p + st.heading = heading -- Add zone to table. - table.insert(self.strafeTargets, st) + table.insert( self.strafeTargets, st ) -- Debug info - local text=string.format("Adding new strafe target %s with %d targets: heading = %03d, box_L = %.1f, box_W = %.1f, goodpass = %d, foul line = %.1f", _name, ntargets, heading, l, w, goodpass, foulline) - self:T(self.id..text) + local text = string.format( "Adding new strafe target %s with %d targets: heading = %03d, box_L = %.1f, box_W = %.1f, goodpass = %d, foul line = %.1f", _name, ntargets, heading, l, w, goodpass, foulline ) + self:T( self.id .. text ) return self end - --- Add all units of a group as one new strafe target pit. -- For a strafe pit, hits from guns are counted. One pit can consist of several units. -- Note, an approach is only valid, if the player enters via a zone in front of the pit, which defined by boxlength and boxheading. @@ -71450,29 +76432,29 @@ end -- @param #number goodpass (Optional) Number of hits for a "good" strafing pass. Default is 20. -- @param #number foulline (Optional) Foul line distance. Hits from closer than this distance are not counted. Default 610 m = 2000 ft. Set to 0 for no foul line. -- @return #RANGE self -function RANGE:AddStrafePitGroup(group, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) - self:F({group=group, boxlength=boxlength, boxwidth=boxwidth, heading=heading, inverseheading=inverseheading, goodpass=goodpass, foulline=foulline}) +function RANGE:AddStrafePitGroup( group, boxlength, boxwidth, heading, inverseheading, goodpass, foulline ) + self:F( { group = group, boxlength = boxlength, boxwidth = boxwidth, heading = heading, inverseheading = inverseheading, goodpass = goodpass, foulline = foulline } ) if group and group:IsAlive() then -- Get units of group. - local _units=group:GetUnits() + local _units = group:GetUnits() -- Make table of unit names. - local _names={} - for _,_unit in ipairs(_units) do + local _names = {} + for _, _unit in ipairs( _units ) do - local _unit=_unit --Wrapper.Unit#UNIT + local _unit = _unit -- Wrapper.Unit#UNIT if _unit and _unit:IsAlive() then - local _name=_unit:GetName() - table.insert(_names,_name) + local _name = _unit:GetName() + table.insert( _names, _name ) end end -- Add strafe pit. - self:AddStrafePit(_names, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) + self:AddStrafePit( _names, boxlength, boxwidth, heading, inverseheading, goodpass, foulline ) end return self @@ -71480,36 +76462,36 @@ end --- Add bombing target(s) to range. -- @param #RANGE self --- @param #table targetnames Table containing names of unit or static objects serving as bomb targets. +-- @param #table targetnames Single or multiple (Table) names of unit or static objects serving as bomb targets. -- @param #number goodhitrange (Optional) Max distance from target unit (in meters) which is considered as a good hit. Default is 25 m. -- @param #boolean randommove If true, unit will move randomly within the range. Default is false. -- @return #RANGE self -function RANGE:AddBombingTargets(targetnames, goodhitrange, randommove) - self:F({targetnames=targetnames, goodhitrange=goodhitrange, randommove=randommove}) +function RANGE:AddBombingTargets( targetnames, goodhitrange, randommove ) + self:F( { targetnames = targetnames, goodhitrange = goodhitrange, randommove = randommove } ) -- Create a table if necessary. - if type(targetnames) ~= "table" then - targetnames={targetnames} + if type( targetnames ) ~= "table" then + targetnames = { targetnames } end -- Default range is 25 m. - goodhitrange=goodhitrange or RANGE.Defaults.goodhitrange + goodhitrange = goodhitrange or RANGE.Defaults.goodhitrange - for _,name in pairs(targetnames) do + for _, name in pairs( targetnames ) do -- Check if we have a static or unit object. - local _isstatic=self:_CheckStatic(name) + local _isstatic = self:_CheckStatic( name ) - if _isstatic==true then - local _static=STATIC:FindByName(name) - self:T2(self.id..string.format("Adding static bombing target %s with hit range %d.", name, goodhitrange, false)) - self:AddBombingTargetUnit(_static, goodhitrange) - elseif _isstatic==false then - local _unit=UNIT:FindByName(name) - self:T2(self.id..string.format("Adding unit bombing target %s with hit range %d.", name, goodhitrange, randommove)) - self:AddBombingTargetUnit(_unit, goodhitrange) + if _isstatic == true then + local _static = STATIC:FindByName( name ) + self:T2( self.id .. string.format( "Adding static bombing target %s with hit range %d.", name, goodhitrange, false ) ) + self:AddBombingTargetUnit( _static, goodhitrange ) + elseif _isstatic == false then + local _unit = UNIT:FindByName( name ) + self:T2( self.id .. string.format( "Adding unit bombing target %s with hit range %d.", name, goodhitrange, randommove ) ) + self:AddBombingTargetUnit( _unit, goodhitrange ) else - self:E(self.id..string.format("ERROR! Could not find bombing target %s.", name)) + self:E( self.id .. string.format( "ERROR! Could not find bombing target %s.", name ) ) end end @@ -71523,77 +76505,76 @@ end -- @param #number goodhitrange Max distance from unit which is considered as a good hit. -- @param #boolean randommove If true, unit will move randomly within the range. Default is false. -- @return #RANGE self -function RANGE:AddBombingTargetUnit(unit, goodhitrange, randommove) - self:F({unit=unit, goodhitrange=goodhitrange, randommove=randommove}) +function RANGE:AddBombingTargetUnit( unit, goodhitrange, randommove ) + self:F( { unit = unit, goodhitrange = goodhitrange, randommove = randommove } ) -- Get name of positionable. - local name=unit:GetName() + local name = unit:GetName() -- Check if we have a static or unit object. - local _isstatic=self:_CheckStatic(name) + local _isstatic = self:_CheckStatic( name ) -- Default range is 25 m. - goodhitrange=goodhitrange or RANGE.Defaults.goodhitrange + goodhitrange = goodhitrange or RANGE.Defaults.goodhitrange -- Set randommove to false if it was not specified. - if randommove==nil or _isstatic==true then - randommove=false + if randommove == nil or _isstatic == true then + randommove = false end -- Debug or error output. - if _isstatic==true then - self:I(self.id..string.format("Adding STATIC bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring(randommove))) - elseif _isstatic==false then - self:I(self.id..string.format("Adding UNIT bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring(randommove))) + if _isstatic == true then + self:I( self.id .. string.format( "Adding STATIC bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring( randommove ) ) ) + elseif _isstatic == false then + self:I( self.id .. string.format( "Adding UNIT bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring( randommove ) ) ) else - self:E(self.id..string.format("ERROR! No bombing target with name %s could be found. Carefully check all UNIT and STATIC names defined in the mission editor!", name)) + self:E( self.id .. string.format( "ERROR! No bombing target with name %s could be found. Carefully check all UNIT and STATIC names defined in the mission editor!", name ) ) end -- Get max speed of unit in km/h. - local speed=0 - if _isstatic==false then - speed=self:_GetSpeed(unit) + local speed = 0 + if _isstatic == false then + speed = self:_GetSpeed( unit ) end - local target={} --#RANGE.BombTarget - target.name=name - target.target=unit - target.goodhitrange=goodhitrange - target.move=randommove - target.speed=speed - target.coordinate=unit:GetCoordinate() + local target = {} -- #RANGE.BombTarget + target.name = name + target.target = unit + target.goodhitrange = goodhitrange + target.move = randommove + target.speed = speed + target.coordinate = unit:GetCoordinate() if _isstatic then - target.type=RANGE.TargetType.STATIC + target.type = RANGE.TargetType.STATIC else - target.type=RANGE.TargetType.UNIT + target.type = RANGE.TargetType.UNIT end -- Insert target to table. - table.insert(self.bombingTargets, target) + table.insert( self.bombingTargets, target ) return self end - --- Add a coordinate of a bombing target. This -- @param #RANGE self -- @param Core.Point#COORDINATE coord The coordinate. -- @param #string name Name of target. -- @param #number goodhitrange Max distance from unit which is considered as a good hit. -- @return #RANGE self -function RANGE:AddBombingTargetCoordinate(coord, name, goodhitrange) +function RANGE:AddBombingTargetCoordinate( coord, name, goodhitrange ) - local target={} --#RANGE.BombTarget - target.name=name or "Bomb Target" - target.target=nil - target.goodhitrange=goodhitrange or RANGE.Defaults.goodhitrange - target.move=false - target.speed=0 - target.coordinate=coord - target.type=RANGE.TargetType.COORD + local target = {} -- #RANGE.BombTarget + target.name = name or "Bomb Target" + target.target = nil + target.goodhitrange = goodhitrange or RANGE.Defaults.goodhitrange + target.move = false + target.speed = 0 + target.coordinate = coord + target.type = RANGE.TargetType.COORD -- Insert target to table. - table.insert(self.bombingTargets, target) + table.insert( self.bombingTargets, target ) return self end @@ -71604,16 +76585,16 @@ end -- @param #number goodhitrange Max distance from unit which is considered as a good hit. -- @param #boolean randommove If true, unit will move randomly within the range. Default is false. -- @return #RANGE self -function RANGE:AddBombingTargetGroup(group, goodhitrange, randommove) - self:F({group=group, goodhitrange=goodhitrange, randommove=randommove}) +function RANGE:AddBombingTargetGroup( group, goodhitrange, randommove ) + self:F( { group = group, goodhitrange = goodhitrange, randommove = randommove } ) if group then - local _units=group:GetUnits() + local _units = group:GetUnits() - for _,_unit in pairs(_units) do + for _, _unit in pairs( _units ) do if _unit and _unit:IsAlive() then - self:AddBombingTargetUnit(_unit, goodhitrange, randommove) + self:AddBombingTargetUnit( _unit, goodhitrange, randommove ) end end end @@ -71626,42 +76607,42 @@ end -- @param #string namepit Name of the strafe pit target object. -- @param #string namefoulline Name of the fould line distance marker object. -- @return #number Foul line distance in meters. -function RANGE:GetFoullineDistance(namepit, namefoulline) - self:F({namepit=namepit, namefoulline=namefoulline}) +function RANGE:GetFoullineDistance( namepit, namefoulline ) + self:F( { namepit = namepit, namefoulline = namefoulline } ) -- Check if we have units or statics. - local _staticpit=self:_CheckStatic(namepit) - local _staticfoul=self:_CheckStatic(namefoulline) + local _staticpit = self:_CheckStatic( namepit ) + local _staticfoul = self:_CheckStatic( namefoulline ) -- Get the unit or static pit object. - local pit=nil - if _staticpit==true then - pit=STATIC:FindByName(namepit, false) - elseif _staticpit==false then - pit=UNIT:FindByName(namepit) + local pit = nil + if _staticpit == true then + pit = STATIC:FindByName( namepit, false ) + elseif _staticpit == false then + pit = UNIT:FindByName( namepit ) else - self:E(self.id..string.format("ERROR! Pit object %s could not be found in GetFoullineDistance function. Check the name in the ME.", namepit)) + self:E( self.id .. string.format( "ERROR! Pit object %s could not be found in GetFoullineDistance function. Check the name in the ME.", namepit ) ) end -- Get the unit or static foul line object. - local foul=nil - if _staticfoul==true then - foul=STATIC:FindByName(namefoulline, false) - elseif _staticfoul==false then - foul=UNIT:FindByName(namefoulline) + local foul = nil + if _staticfoul == true then + foul = STATIC:FindByName( namefoulline, false ) + elseif _staticfoul == false then + foul = UNIT:FindByName( namefoulline ) else - self:E(self.id..string.format("ERROR! Foul line object %s could not be found in GetFoullineDistance function. Check the name in the ME.", namefoulline)) + self:E( self.id .. string.format( "ERROR! Foul line object %s could not be found in GetFoullineDistance function. Check the name in the ME.", namefoulline ) ) end -- Get the distance between the two objects. - local fouldist=0 - if pit~=nil and foul~=nil then - fouldist=pit:GetCoordinate():Get2DDistance(foul:GetCoordinate()) + local fouldist = 0 + if pit ~= nil and foul ~= nil then + fouldist = pit:GetCoordinate():Get2DDistance( foul:GetCoordinate() ) else - self:E(self.id..string.format("ERROR! Foul line distance could not be determined. Check pit object name %s and foul line object name %s in the ME.", namepit, namefoulline)) + self:E( self.id .. string.format( "ERROR! Foul line distance could not be determined. Check pit object name %s and foul line object name %s in the ME.", namepit, namefoulline ) ) end - self:T(self.id..string.format("Foul line distance = %.1f m.", fouldist)) + self:T( self.id .. string.format( "Foul line distance = %.1f m.", fouldist ) ) return fouldist end @@ -71672,120 +76653,117 @@ end --- General event handler. -- @param #RANGE self -- @param #table Event DCS event table. -function RANGE:onEvent(Event) - self:F3(Event) +function RANGE:onEvent( Event ) + self:F3( Event ) if Event == nil or Event.initiator == nil then - self:T3("Skipping onEvent. Event or Event.initiator unknown.") + self:T3( "Skipping onEvent. Event or Event.initiator unknown." ) return true end - if Unit.getByName(Event.initiator:getName()) == nil then - self:T3("Skipping onEvent. Initiator unit name unknown.") + if Unit.getByName( Event.initiator:getName() ) == nil then + self:T3( "Skipping onEvent. Initiator unit name unknown." ) return true end local DCSiniunit = Event.initiator local DCStgtunit = Event.target - local DCSweapon = Event.weapon + local DCSweapon = Event.weapon - local EventData={} - local _playerunit=nil - local _playername=nil + local EventData = {} + local _playerunit = nil + local _playername = nil if Event.initiator then - EventData.IniUnitName = Event.initiator:getName() - EventData.IniDCSGroup = Event.initiator:getGroup() + EventData.IniUnitName = Event.initiator:getName() + EventData.IniDCSGroup = Event.initiator:getGroup() EventData.IniGroupName = Event.initiator:getGroup():getName() -- Get player unit and name. This returns nil,nil if the event was not fired by a player unit. And these are the only events we are interested in. - _playerunit, _playername = self:_GetPlayerUnitAndName(EventData.IniUnitName) + _playerunit, _playername = self:_GetPlayerUnitAndName( EventData.IniUnitName ) end if Event.target then - EventData.TgtUnitName = Event.target:getName() - EventData.TgtUnit = UNIT:FindByName(EventData.TgtUnitName) + EventData.TgtUnitName = Event.target:getName() + EventData.TgtUnit = UNIT:FindByName( EventData.TgtUnitName ) end if Event.weapon then - EventData.Weapon = Event.weapon - EventData.weapon = Event.weapon + EventData.Weapon = Event.weapon + EventData.weapon = Event.weapon EventData.WeaponTypeName = Event.weapon:getTypeName() end -- Event info. - self:T3(self.id..string.format("EVENT: Event in onEvent with ID = %s", tostring(Event.id))) - self:T3(self.id..string.format("EVENT: Ini unit = %s" , tostring(EventData.IniUnitName))) - self:T3(self.id..string.format("EVENT: Ini group = %s" , tostring(EventData.IniGroupName))) - self:T3(self.id..string.format("EVENT: Ini player = %s" , tostring(_playername))) - self:T3(self.id..string.format("EVENT: Tgt unit = %s" , tostring(EventData.TgtUnitName))) - self:T3(self.id..string.format("EVENT: Wpn type = %s" , tostring(EventData.WeaponTypeName))) + self:T3( self.id .. string.format( "EVENT: Event in onEvent with ID = %s", tostring( Event.id ) ) ) + self:T3( self.id .. string.format( "EVENT: Ini unit = %s", tostring( EventData.IniUnitName ) ) ) + self:T3( self.id .. string.format( "EVENT: Ini group = %s", tostring( EventData.IniGroupName ) ) ) + self:T3( self.id .. string.format( "EVENT: Ini player = %s", tostring( _playername ) ) ) + self:T3( self.id .. string.format( "EVENT: Tgt unit = %s", tostring( EventData.TgtUnitName ) ) ) + self:T3( self.id .. string.format( "EVENT: Wpn type = %s", tostring( EventData.WeaponTypeName ) ) ) -- Call event Birth function. - if Event.id==world.event.S_EVENT_BIRTH and _playername then - self:OnEventBirth(EventData) + if Event.id == world.event.S_EVENT_BIRTH and _playername then + self:OnEventBirth( EventData ) end -- Call event Shot function. - if Event.id==world.event.S_EVENT_SHOT and _playername and Event.weapon then - self:OnEventShot(EventData) + if Event.id == world.event.S_EVENT_SHOT and _playername and Event.weapon then + self:OnEventShot( EventData ) end -- Call event Hit function. - if Event.id==world.event.S_EVENT_HIT and _playername and DCStgtunit then - self:OnEventHit(EventData) + if Event.id == world.event.S_EVENT_HIT and _playername and DCStgtunit then + self:OnEventHit( EventData ) end end - --- Range event handler for event birth. -- @param #RANGE self -- @param Core.Event#EVENTDATA EventData -function RANGE:OnEventBirth(EventData) - self:F({eventbirth = EventData}) +function RANGE:OnEventBirth( EventData ) + self:F( { eventbirth = EventData } ) - local _unitName=EventData.IniUnitName - local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + local _unitName = EventData.IniUnitName + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) - self:T3(self.id.."BIRTH: unit = "..tostring(EventData.IniUnitName)) - self:T3(self.id.."BIRTH: group = "..tostring(EventData.IniGroupName)) - self:T3(self.id.."BIRTH: player = "..tostring(_playername)) + self:T3( self.id .. "BIRTH: unit = " .. tostring( EventData.IniUnitName ) ) + self:T3( self.id .. "BIRTH: group = " .. tostring( EventData.IniGroupName ) ) + self:T3( self.id .. "BIRTH: player = " .. tostring( _playername ) ) if _unit and _playername then - local _uid=_unit:GetID() - local _group=_unit:GetGroup() - local _gid=_group:GetID() - local _callsign=_unit:GetCallsign() + local _uid = _unit:GetID() + local _group = _unit:GetGroup() + local _gid = _group:GetID() + local _callsign = _unit:GetCallsign() -- Debug output. - local text=string.format("Player %s, callsign %s entered unit %s (UID %d) of group %s (GID %d)", _playername, _callsign, _unitName, _uid, _group:GetName(), _gid) - self:T(self.id..text) + local text = string.format( "Player %s, callsign %s entered unit %s (UID %d) of group %s (GID %d)", _playername, _callsign, _unitName, _uid, _group:GetName(), _gid ) + self:T( self.id .. text ) -- Reset current strafe status. self.strafeStatus[_uid] = nil -- Add Menu commands after a delay of 0.1 seconds. - --SCHEDULER:New(nil, self._AddF10Commands, {self,_unitName}, 0.1) - self:ScheduleOnce(0.1, self._AddF10Commands, self, _unitName) + self:ScheduleOnce( 0.1, self._AddF10Commands, self, _unitName ) -- By default, some bomb impact points and do not flare each hit on target. - self.PlayerSettings[_playername]={} --#RANGE.PlayerData - self.PlayerSettings[_playername].smokebombimpact=self.defaultsmokebomb - self.PlayerSettings[_playername].flaredirecthits=false - self.PlayerSettings[_playername].smokecolor=SMOKECOLOR.Blue - self.PlayerSettings[_playername].flarecolor=FLARECOLOR.Red - self.PlayerSettings[_playername].delaysmoke=true - self.PlayerSettings[_playername].messages=true - self.PlayerSettings[_playername].client=CLIENT:FindByName(_unitName, nil, true) - self.PlayerSettings[_playername].unitname=_unitName - self.PlayerSettings[_playername].playername=_playername - self.PlayerSettings[_playername].airframe=EventData.IniUnit:GetTypeName() - self.PlayerSettings[_playername].inzone=false + self.PlayerSettings[_playername] = {} -- #RANGE.PlayerData + self.PlayerSettings[_playername].smokebombimpact = self.defaultsmokebomb + self.PlayerSettings[_playername].flaredirecthits = false + self.PlayerSettings[_playername].smokecolor = SMOKECOLOR.Blue + self.PlayerSettings[_playername].flarecolor = FLARECOLOR.Red + self.PlayerSettings[_playername].delaysmoke = true + self.PlayerSettings[_playername].messages = true + self.PlayerSettings[_playername].client = CLIENT:FindByName( _unitName, nil, true ) + self.PlayerSettings[_playername].unitname = _unitName + self.PlayerSettings[_playername].playername = _playername + self.PlayerSettings[_playername].airframe = EventData.IniUnit:GetTypeName() + self.PlayerSettings[_playername].inzone = false -- Start check in zone timer. if self.planes[_uid] ~= true then - --SCHEDULER:New(nil, self._CheckInZone, {self, EventData.IniUnitName}, 1, 1) - self.timerCheckZone=TIMER:New(self._CheckInZone, self, EventData.IniUnitName):Start(1, 1) + self.timerCheckZone = TIMER:New( self._CheckInZone, self, EventData.IniUnitName ):Start( 1, 1 ) self.planes[_uid] = true end @@ -71795,18 +76773,18 @@ end --- Range event handler for event hit. -- @param #RANGE self -- @param Core.Event#EVENTDATA EventData -function RANGE:OnEventHit(EventData) - self:F({eventhit = EventData}) +function RANGE:OnEventHit( EventData ) + self:F( { eventhit = EventData } ) -- Debug info. - self:T3(self.id.."HIT: Ini unit = "..tostring(EventData.IniUnitName)) - self:T3(self.id.."HIT: Ini group = "..tostring(EventData.IniGroupName)) - self:T3(self.id.."HIT: Tgt target = "..tostring(EventData.TgtUnitName)) + self:T3( self.id .. "HIT: Ini unit = " .. tostring( EventData.IniUnitName ) ) + self:T3( self.id .. "HIT: Ini group = " .. tostring( EventData.IniGroupName ) ) + self:T3( self.id .. "HIT: Tgt target = " .. tostring( EventData.TgtUnitName ) ) -- Player info local _unitName = EventData.IniUnitName - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) - if _unit==nil or _playername==nil then + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) + if _unit == nil or _playername == nil then return end @@ -71814,11 +76792,11 @@ function RANGE:OnEventHit(EventData) local _unitID = _unit:GetID() -- Target - local target = EventData.TgtUnit + local target = EventData.TgtUnit local targetname = EventData.TgtUnitName -- Current strafe target of player. - local _currentTarget = self.strafeStatus[_unitID] + local _currentTarget = self.strafeStatus[_unitID] --#RANGE.StrafeStatus -- Player has rolled in on a strafing target. if _currentTarget and target:IsAlive() then @@ -71827,30 +76805,31 @@ function RANGE:OnEventHit(EventData) local targetPos = target:GetCoordinate() -- Loop over valid targets for this run. - for _,_target in pairs(_currentTarget.zone.targets) do + for _, _target in pairs( _currentTarget.zone.targets ) do -- Check the the target is the same that was actually hit. - if _target and _target:IsAlive() and _target:GetName() == targetname then + if _target and _target:IsAlive() and _target:GetName() == targetname then -- Get distance between player and target. - local dist=playerPos:Get2DDistance(targetPos) + local dist = playerPos:Get2DDistance( targetPos ) if dist > _currentTarget.zone.foulline then -- Increase hit counter of this run. - _currentTarget.hits = _currentTarget.hits + 1 + _currentTarget.hits = _currentTarget.hits + 1 -- Flare target. if _unit and _playername and self.PlayerSettings[_playername].flaredirecthits then - targetPos:Flare(self.PlayerSettings[_playername].flarecolor) + targetPos:Flare( self.PlayerSettings[_playername].flarecolor ) end else -- Too close to the target. - if _currentTarget.pastfoulline==false and _unit and _playername then - local _d=_currentTarget.zone.foulline - local text=string.format("%s, Invalid hit!\nYou already passed foul line distance of %d m for target %s.", self:_myname(_unitName), _d, targetname) - self:_DisplayMessageToGroup(_unit, text) - self:T2(self.id..text) - _currentTarget.pastfoulline=true + if _currentTarget.pastfoulline == false and _unit and _playername then + local _d = _currentTarget.zone.foulline + local text = string.format( "%s, Invalid hit!\nYou already passed foul line distance of %d m for target %s.", self:_myname( _unitName ), _d, targetname ) + self:_DisplayMessageToGroup( _unit, text ) + self:T2( self.id .. text ) + _currentTarget.pastfoulline = true + invalidStrafe = true -- Rangeboss Edit end end @@ -71859,9 +76838,9 @@ function RANGE:OnEventHit(EventData) end -- Bombing Targets - for _,_bombtarget in pairs(self.bombingTargets) do + for _, _bombtarget in pairs( self.bombingTargets ) do - local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE + local _target = _bombtarget.target -- Wrapper.Positionable#POSITIONABLE -- Check if one of the bomb targets was hit. if _target and _target:IsAlive() and _bombtarget.name == targetname then @@ -71870,11 +76849,11 @@ function RANGE:OnEventHit(EventData) -- Flare target. if self.PlayerSettings[_playername].flaredirecthits then - + -- Position of target. local targetPos = _target:GetCoordinate() - - targetPos:Flare(self.PlayerSettings[_playername].flarecolor) + + targetPos:Flare( self.PlayerSettings[_playername].flarecolor ) end end @@ -71885,40 +76864,40 @@ end --- Range event handler for event shot (when a unit releases a rocket or bomb (but not a fast firing gun). -- @param #RANGE self -- @param Core.Event#EVENTDATA EventData -function RANGE:OnEventShot(EventData) - self:F({eventshot = EventData}) +function RANGE:OnEventShot( EventData ) + self:F( { eventshot = EventData } ) -- Nil checks. - if EventData.Weapon==nil then + if EventData.Weapon == nil then return end - if EventData.IniDCSUnit==nil then + if EventData.IniDCSUnit == nil then return end -- Weapon data. - local _weapon = EventData.Weapon:getTypeName() -- should be the same as Event.WeaponTypeName - local _weaponStrArray = UTILS.Split(_weapon,"%.") + local _weapon = EventData.Weapon:getTypeName() -- should be the same as Event.WeaponTypeName + local _weaponStrArray = UTILS.Split( _weapon, "%." ) local _weaponName = _weaponStrArray[#_weaponStrArray] -- Weapon descriptor. - local desc=EventData.Weapon:getDesc() + local desc = EventData.Weapon:getDesc() -- Weapon category: 0=SHELL, 1=MISSILE, 2=ROCKET, 3=BOMB (Weapon.Category.X) - local weaponcategory=desc.category + local weaponcategory = desc.category -- Debug info. - self:T(self.id.."EVENT SHOT: Range "..self.rangename) - self:T(self.id.."EVENT SHOT: Ini unit = "..EventData.IniUnitName) - self:T(self.id.."EVENT SHOT: Ini group = "..EventData.IniGroupName) - self:T(self.id.."EVENT SHOT: Weapon type = ".._weapon) - self:T(self.id.."EVENT SHOT: Weapon name = ".._weaponName) - self:T(self.id.."EVENT SHOT: Weapon cate = "..weaponcategory) + self:T( self.id .. "EVENT SHOT: Range " .. self.rangename ) + self:T( self.id .. "EVENT SHOT: Ini unit = " .. EventData.IniUnitName ) + self:T( self.id .. "EVENT SHOT: Ini group = " .. EventData.IniGroupName ) + self:T( self.id .. "EVENT SHOT: Weapon type = " .. _weapon ) + self:T( self.id .. "EVENT SHOT: Weapon name = " .. _weaponName ) + self:T( self.id .. "EVENT SHOT: Weapon cate = " .. weaponcategory ) -- Tracking conditions for bombs, rockets and missiles. - local _bombs = weaponcategory==Weapon.Category.BOMB --string.match(_weapon, "weapons.bombs") - local _rockets = weaponcategory==Weapon.Category.ROCKET --string.match(_weapon, "weapons.nurs") - local _missiles = weaponcategory==Weapon.Category.MISSILE --string.match(_weapon, "weapons.missiles") or _viggen + local _bombs = weaponcategory == Weapon.Category.BOMB -- string.match(_weapon, "weapons.bombs") + local _rockets = weaponcategory == Weapon.Category.ROCKET -- string.match(_weapon, "weapons.nurs") + local _missiles = weaponcategory == Weapon.Category.MISSILE -- string.match(_weapon, "weapons.missiles") or _viggen -- Check if any condition applies here. local _track = (_bombs and self.trackbombs) or (_rockets and self.trackrockets) or (_missiles and self.trackmissiles) @@ -71927,39 +76906,38 @@ function RANGE:OnEventShot(EventData) local _unitName = EventData.IniUnitName -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) -- Set this to larger value than the threshold. - local dPR=self.BombtrackThreshold*2 + local dPR = self.BombtrackThreshold * 2 -- Distance player to range. if _unit and _playername then - dPR=_unit:GetCoordinate():Get2DDistance(self.location) - self:T(self.id..string.format("Range %s, player %s, player-range distance = %d km.", self.rangename, _playername, dPR/1000)) + dPR = _unit:GetCoordinate():Get2DDistance( self.location ) + self:T( self.id .. string.format( "Range %s, player %s, player-range distance = %d km.", self.rangename, _playername, dPR / 1000 ) ) end -- Only track if distance player to range is < 25 km. Also check that a player shot. No need to track AI weapons. - if _track and dPR<=self.BombtrackThreshold and _unit and _playername then + if _track and dPR <= self.BombtrackThreshold and _unit and _playername then -- Player data. - local playerData=self.PlayerSettings[_playername] --#RANGE.PlayerData + local playerData = self.PlayerSettings[_playername] -- #RANGE.PlayerData -- Tracking info and init of last bomb position. - self:T(self.id..string.format("RANGE %s: Tracking %s - %s.", self.rangename, _weapon, EventData.weapon:getName())) + self:T( self.id .. string.format( "RANGE %s: Tracking %s - %s.", self.rangename, _weapon, EventData.weapon:getName() ) ) -- Init bomb position. - local _lastBombPos = {x=0,y=0,z=0} --DCS#Vec3 + local _lastBombPos = { x = 0, y = 0, z = 0 } -- DCS#Vec3 -- Function monitoring the position of a bomb until impact. - local function trackBomb(_ordnance) + local function trackBomb( _ordnance ) -- When the pcall returns a failure the weapon has hit. - local _status,_bombPos = pcall( - function() + local _status, _bombPos = pcall( function() return _ordnance:getPoint() - end) + end ) - self:T2(self.id..string.format("Range %s: Bomb still in air: %s", self.rangename, tostring(_status))) + self:T2( self.id .. string.format( "Range %s: Bomb still in air: %s", self.rangename, tostring( _status ) ) ) if _status then ---------------------------- @@ -71967,7 +76945,7 @@ function RANGE:OnEventShot(EventData) ---------------------------- -- Remember this position. - _lastBombPos = {x = _bombPos.x, y = _bombPos.y, z= _bombPos.z } + _lastBombPos = { x = _bombPos.x, y = _bombPos.y, z = _bombPos.z } -- Check again in ~0.005 seconds ==> 200 checks per second. return timer.getTime() + self.dtBombtrack @@ -71979,55 +76957,57 @@ function RANGE:OnEventShot(EventData) ----------------------------- -- Get closet target to last position. - local _closetTarget=nil --#RANGE.BombTarget - local _distance=nil - local _closeCoord=nil - local _hitquality="POOR" + local _closetTarget = nil -- #RANGE.BombTarget + local _distance = nil + local _closeCoord = nil + local _hitquality = "POOR" -- Get callsign. - local _callsign=self:_myname(_unitName) + local _callsign = self:_myname( _unitName ) -- Coordinate of impact point. - local impactcoord=COORDINATE:NewFromVec3(_lastBombPos) + local impactcoord = COORDINATE:NewFromVec3( _lastBombPos ) -- Check if impact happened in range zone. - local insidezone=self.rangezone:IsCoordinateInZone(impactcoord) + local insidezone = self.rangezone:IsCoordinateInZone( impactcoord ) -- Impact point of bomb. if self.Debug then - impactcoord:MarkToAll("Bomb impact point") + impactcoord:MarkToAll( "Bomb impact point" ) end -- Smoke impact point of bomb. if playerData.smokebombimpact and insidezone then if playerData.delaysmoke then - timer.scheduleFunction(self._DelayedSmoke, {coord=impactcoord, color=playerData.smokecolor}, timer.getTime() + self.TdelaySmoke) + timer.scheduleFunction( self._DelayedSmoke, { coord = impactcoord, color = playerData.smokecolor }, timer.getTime() + self.TdelaySmoke ) else - impactcoord:Smoke(playerData.smokecolor) + impactcoord:Smoke( playerData.smokecolor ) end end -- Loop over defined bombing targets. - for _,_bombtarget in pairs(self.bombingTargets) do + for _, _bombtarget in pairs( self.bombingTargets ) do -- Get target coordinate. - local targetcoord=self:_GetBombTargetCoordinate(_bombtarget) + local targetcoord = self:_GetBombTargetCoordinate( _bombtarget ) if targetcoord then -- Distance between bomb and target. - local _temp = impactcoord:Get2DDistance(targetcoord) + local _temp = impactcoord:Get2DDistance( targetcoord ) -- Find closest target to last known position of the bomb. if _distance == nil or _temp < _distance then _distance = _temp _closetTarget = _bombtarget - _closeCoord=targetcoord - if _distance <= 0.5*_bombtarget.goodhitrange then + _closeCoord = targetcoord + if _distance <= 1.53 then -- Rangeboss Edit + _hitquality = "SHACK" -- Rangeboss Edit + elseif _distance <= 0.5 * _bombtarget.goodhitrange then -- Rangeboss Edit _hitquality = "EXCELLENT" elseif _distance <= _bombtarget.goodhitrange then _hitquality = "GOOD" - elseif _distance <= 2*_bombtarget.goodhitrange then + elseif _distance <= 2 * _bombtarget.goodhitrange then _hitquality = "INEFFECTIVE" else _hitquality = "POOR" @@ -72041,44 +77021,48 @@ function RANGE:OnEventShot(EventData) if _distance and _distance <= self.scorebombdistance then -- Init bomb player results. if not self.bombPlayerResults[_playername] then - self.bombPlayerResults[_playername]={} + self.bombPlayerResults[_playername] = {} end -- Local results. - local _results=self.bombPlayerResults[_playername] + local _results = self.bombPlayerResults[_playername] - local result={} --#RANGE.BombResult - result.name=_closetTarget.name or "unknown" - result.distance=_distance - result.radial=_closeCoord:HeadingTo(impactcoord) - result.weapon=_weaponName or "unknown" - result.quality=_hitquality - result.player=playerData.playername - result.time=timer.getAbsTime() - result.airframe=playerData.airframe + local result = {} -- #RANGE.BombResult + result.name = _closetTarget.name or "unknown" + result.distance = _distance + result.radial = _closeCoord:HeadingTo( impactcoord ) + result.weapon = _weaponName or "unknown" + result.quality = _hitquality + result.player = playerData.playername + result.time = timer.getAbsTime() + result.airframe = playerData.airframe + result.roundsFired = 0 -- Rangeboss Edit + result.roundsHit = 0 -- Rangeboss Edit + result.roundsQuality = "N/A" -- Rangeboss Edit + result.rangename = self.rangename -- Add to table. - table.insert(_results, result) + table.insert( _results, result ) -- Call impact. - self:Impact(result, playerData) + self:Impact( result, playerData ) elseif insidezone then -- Send message. - local _message=string.format("%s, weapon impacted too far from nearest range target (>%.1f km). No score!", _callsign, self.scorebombdistance/1000) - self:_DisplayMessageToGroup(_unit, _message, nil, false) - + local _message = string.format( "%s, weapon impacted too far from nearest range target (>%.1f km). No score!", _callsign, self.scorebombdistance / 1000 ) + self:_DisplayMessageToGroup( _unit, _message, nil, false ) + if self.rangecontrol then - self.rangecontrol:NewTransmission(RANGE.Sound.RCWeaponImpactedTooFar.filename, RANGE.Sound.RCWeaponImpactedTooFar.duration, self.soundpath, nil, nil, _message, self.subduration) + self.rangecontrol:NewTransmission( RANGE.Sound.RCWeaponImpactedTooFar.filename, RANGE.Sound.RCWeaponImpactedTooFar.duration, self.soundpath, nil, nil, _message, self.subduration ) end else - self:T(self.id.."Weapon impacted outside range zone.") + self:T( self.id .. "Weapon impacted outside range zone." ) end - --Terminate the timer - self:T(self.id..string.format("Range %s, player %s: Terminating bomb track timer.", self.rangename, _playername)) + -- Terminate the timer + self:T( self.id .. string.format( "Range %s, player %s: Terminating bomb track timer.", self.rangename, _playername ) ) return nil end -- _status check @@ -72086,10 +77070,10 @@ function RANGE:OnEventShot(EventData) end -- end function trackBomb -- Weapon is not yet "alife" just yet. Start timer in one second. - self:T(self.id..string.format("Range %s, player %s: Tracking of weapon starts in 0.1 seconds.", self.rangename, _playername)) - timer.scheduleFunction(trackBomb, EventData.weapon, timer.getTime()+0.1) + self:T( self.id .. string.format( "Range %s, player %s: Tracking of weapon starts in 0.1 seconds.", self.rangename, _playername ) ) + timer.scheduleFunction( trackBomb, EventData.weapon, timer.getTime() + 0.1 ) - end --if _track (string.match) and player-range distance < threshold. + end -- if _track (string.match) and player-range distance < threshold. end @@ -72102,47 +77086,46 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function RANGE:onafterStatus(From, Event, To) +function RANGE:onafterStatus( From, Event, To ) - if self.verbose>0 then + if self.verbose > 0 then - local fsmstate=self:GetState() - - local text=string.format("Range status: %s", fsmstate) - - if self.instructor then - local alive="N/A" + local fsmstate = self:GetState() + + local text = string.format( "Range status: %s", fsmstate ) + + if self.instructor then + local alive = "N/A" if self.instructorrelayname then - local relay=UNIT:FindByName(self.instructorrelayname) + local relay = UNIT:FindByName( self.instructorrelayname ) if relay then - alive=tostring(relay:IsAlive()) + alive = tostring( relay:IsAlive() ) end end - text=text..string.format(", Instructor %.3f MHz (Relay=%s alive=%s)", self.instructorfreq, tostring(self.instructorrelayname), alive) + text = text .. string.format( ", Instructor %.3f MHz (Relay=%s alive=%s)", self.instructorfreq, tostring( self.instructorrelayname ), alive ) end - - if self.rangecontrol then - local alive="N/A" + + if self.rangecontrol then + local alive = "N/A" if self.rangecontrolrelayname then - local relay=UNIT:FindByName(self.rangecontrolrelayname) + local relay = UNIT:FindByName( self.rangecontrolrelayname ) if relay then - alive=tostring(relay:IsAlive()) + alive = tostring( relay:IsAlive() ) end end - text=text..string.format(", Control %.3f MHz (Relay=%s alive=%s)", self.rangecontrolfreq, tostring(self.rangecontrolrelayname), alive) + text = text .. string.format( ", Control %.3f MHz (Relay=%s alive=%s)", self.rangecontrolfreq, tostring( self.rangecontrolrelayname ), alive ) end - - + -- Check range status. - self:I(self.id..text) - + self:I( self.id .. text ) + end -- Check player status. self:_CheckPlayers() -- Check back in ~10 seconds. - self:__Status(-10) + self:__Status( -10 ) end --- Function called after player enters the range zone. @@ -72151,23 +77134,23 @@ end -- @param #string Event Event. -- @param #string To To state. -- @param #RANGE.PlayerData player Player data. -function RANGE:onafterEnterRange(From, Event, To, player) - +function RANGE:onafterEnterRange( From, Event, To, player ) + if self.instructor and self.rangecontrol then - + -- Range control radio frequency split. - local RF=UTILS.Split(string.format("%.3f", self.rangecontrolfreq), ".") - + local RF = UTILS.Split( string.format( "%.3f", self.rangecontrolfreq ), "." ) + -- Radio message that player entered the range - self.instructor:NewTransmission(RANGE.Sound.IREnterRange.filename, RANGE.Sound.IREnterRange.duration, self.soundpath) - self.instructor:Number2Transmission(RF[1]) - if tonumber(RF[2])>0 then - self.instructor:NewTransmission(RANGE.Sound.IRDecimal.filename, RANGE.Sound.IRDecimal.duration, self.soundpath) - self.instructor:Number2Transmission(RF[2]) + self.instructor:NewTransmission( RANGE.Sound.IREnterRange.filename, RANGE.Sound.IREnterRange.duration, self.soundpath ) + self.instructor:Number2Transmission( RF[1] ) + if tonumber( RF[2] ) > 0 then + self.instructor:NewTransmission( RANGE.Sound.IRDecimal.filename, RANGE.Sound.IRDecimal.duration, self.soundpath ) + self.instructor:Number2Transmission( RF[2] ) end - self.instructor:NewTransmission(RANGE.Sound.IRMegaHertz.filename, RANGE.Sound.IRMegaHertz.duration, self.soundpath) + self.instructor:NewTransmission( RANGE.Sound.IRMegaHertz.filename, RANGE.Sound.IRMegaHertz.duration, self.soundpath ) end - + end --- Function called after player leaves the range zone. @@ -72176,15 +77159,14 @@ end -- @param #string Event Event. -- @param #string To To state. -- @param #RANGE.PlayerData player Player data. -function RANGE:onafterExitRange(From, Event, To, player) +function RANGE:onafterExitRange( From, Event, To, player ) if self.instructor then - self.instructor:NewTransmission(RANGE.Sound.IRExitRange.filename, RANGE.Sound.IRExitRange.duration, self.soundpath) + self.instructor:NewTransmission( RANGE.Sound.IRExitRange.filename, RANGE.Sound.IRExitRange.duration, self.soundpath ) end end - --- Function called after bomb impact on range. -- @param #RANGE self -- @param #string From From state. @@ -72192,49 +77174,49 @@ end -- @param #string To To state. -- @param #RANGE.BombResult result Result of bomb impact. -- @param #RANGE.PlayerData player Player data table. -function RANGE:onafterImpact(From, Event, To, result, player) +function RANGE:onafterImpact( From, Event, To, result, player ) -- Only display target name if there is more than one bomb target. - local targetname=nil - if #self.bombingTargets>1 then - local targetname=result.name + local targetname = nil + if #self.bombingTargets > 1 then + local targetname = result.name end -- Send message to player. - local text=string.format("%s, impact %03d° for %d ft", player.playername, result.radial, UTILS.MetersToFeet(result.distance)) + local text = string.format( "%s, impact %03d° for %d ft", player.playername, result.radial, UTILS.MetersToFeet( result.distance ) ) if targetname then - text=text..string.format(" from bulls of target %s.") + text = text .. string.format( " from bulls of target %s." ) else - text=text.."." + text = text .. "." end - text=text..string.format(" %s hit.", result.quality) - + text = text .. string.format( " %s hit.", result.quality ) + if self.rangecontrol then - self.rangecontrol:NewTransmission(RANGE.Sound.RCImpact.filename, RANGE.Sound.RCImpact.duration, self.soundpath, nil, nil, text, self.subduration) - self.rangecontrol:Number2Transmission(string.format("%03d", result.radial), nil, 0.1) - self.rangecontrol:NewTransmission(RANGE.Sound.RCDegrees.filename, RANGE.Sound.RCDegrees.duration, self.soundpath) - self.rangecontrol:NewTransmission(RANGE.Sound.RCFor.filename, RANGE.Sound.RCFor.duration, self.soundpath) - self.rangecontrol:Number2Transmission(string.format("%d", UTILS.MetersToFeet(result.distance))) - self.rangecontrol:NewTransmission(RANGE.Sound.RCFeet.filename, RANGE.Sound.RCFeet.duration, self.soundpath) - if result.quality=="POOR" then - self.rangecontrol:NewTransmission(RANGE.Sound.RCPoorHit.filename, RANGE.Sound.RCPoorHit.duration, self.soundpath, nil, 0.5) - elseif result.quality=="INEFFECTIVE" then - self.rangecontrol:NewTransmission(RANGE.Sound.RCIneffectiveHit.filename, RANGE.Sound.RCIneffectiveHit.duration, self.soundpath, nil, 0.5) - elseif result.quality=="GOOD" then - self.rangecontrol:NewTransmission(RANGE.Sound.RCGoodHit.filename, RANGE.Sound.RCGoodHit.duration, self.soundpath, nil, 0.5) - elseif result.quality=="EXCELLENT" then - self.rangecontrol:NewTransmission(RANGE.Sound.RCExcellentHit.filename, RANGE.Sound.RCExcellentHit.duration, self.soundpath, nil, 0.5) + self.rangecontrol:NewTransmission( RANGE.Sound.RCImpact.filename, RANGE.Sound.RCImpact.duration, self.soundpath, nil, nil, text, self.subduration ) + self.rangecontrol:Number2Transmission( string.format( "%03d", result.radial ), nil, 0.1 ) + self.rangecontrol:NewTransmission( RANGE.Sound.RCDegrees.filename, RANGE.Sound.RCDegrees.duration, self.soundpath ) + self.rangecontrol:NewTransmission( RANGE.Sound.RCFor.filename, RANGE.Sound.RCFor.duration, self.soundpath ) + self.rangecontrol:Number2Transmission( string.format( "%d", UTILS.MetersToFeet( result.distance ) ) ) + self.rangecontrol:NewTransmission( RANGE.Sound.RCFeet.filename, RANGE.Sound.RCFeet.duration, self.soundpath ) + if result.quality == "POOR" then + self.rangecontrol:NewTransmission( RANGE.Sound.RCPoorHit.filename, RANGE.Sound.RCPoorHit.duration, self.soundpath, nil, 0.5 ) + elseif result.quality == "INEFFECTIVE" then + self.rangecontrol:NewTransmission( RANGE.Sound.RCIneffectiveHit.filename, RANGE.Sound.RCIneffectiveHit.duration, self.soundpath, nil, 0.5 ) + elseif result.quality == "GOOD" then + self.rangecontrol:NewTransmission( RANGE.Sound.RCGoodHit.filename, RANGE.Sound.RCGoodHit.duration, self.soundpath, nil, 0.5 ) + elseif result.quality == "EXCELLENT" then + self.rangecontrol:NewTransmission( RANGE.Sound.RCExcellentHit.filename, RANGE.Sound.RCExcellentHit.duration, self.soundpath, nil, 0.5 ) end - - end + + end -- Unit. - local unit=UNIT:FindByName(player.unitname) + local unit = UNIT:FindByName( player.unitname ) -- Send message. - self:_DisplayMessageToGroup(unit, text, nil, true) - self:T(self.id..text) - + self:_DisplayMessageToGroup( unit, text, nil, true ) + self:T( self.id .. text ) + -- Save results. if self.autosave then self:Save() @@ -72247,11 +77229,11 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function RANGE:onbeforeSave(From, Event, To) +function RANGE:onbeforeSave( From, Event, To ) if io and lfs then return true else - self:E(self.id..string.format("WARNING: io and/or lfs not desanitized. Cannot save player results.")) + self:E( self.id .. string.format( "WARNING: io and/or lfs not desanitized. Cannot save player results." ) ) return false end end @@ -72261,50 +77243,50 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function RANGE:onafterSave(From, Event, To) +function RANGE:onafterSave( From, Event, To ) - local function _savefile(filename, data) - local f=io.open(filename, "wb") + local function _savefile( filename, data ) + local f = io.open( filename, "wb" ) if f then - f:write(data) + f:write( data ) f:close() - self:I(self.id..string.format("Saving player results to file %s", tostring(filename))) + self:I( self.id .. string.format( "Saving player results to file %s", tostring( filename ) ) ) else - self:E(self.id..string.format("ERROR: Could not save results to file %s", tostring(filename))) + self:E( self.id .. string.format( "ERROR: Could not save results to file %s", tostring( filename ) ) ) end end -- Path. - local path=lfs.writedir()..[[Logs\]] + local path = lfs.writedir() .. [[Logs\]] -- Set file name. - local filename=path..string.format("RANGE-%s_BombingResults.csv", self.rangename) + local filename = path .. string.format( "RANGE-%s_BombingResults.csv", self.rangename ) -- Header line. - local scores="Name,Pass,Target,Distance,Radial,Quality,Weapon,Airframe,Mission Time" + local scores = "Name,Pass,Target,Distance,Radial,Quality,Weapon,Airframe,Mission Time" -- Loop over all players. - for playername,results in pairs(self.bombPlayerResults) do + for playername, results in pairs( self.bombPlayerResults ) do -- Loop over player grades table. - for i,_result in pairs(results) do - local result=_result --#RANGE.BombResult - local distance=result.distance - local weapon=result.weapon - local target=result.name - local radial=result.radial - local quality=result.quality - local time=UTILS.SecondsToClock(result.time) - local airframe=result.airframe - local date="n/a" + for i, _result in pairs( results ) do + local result = _result -- #RANGE.BombResult + local distance = result.distance + local weapon = result.weapon + local target = result.name + local radial = result.radial + local quality = result.quality + local time = UTILS.SecondsToClock( result.time ) + local airframe = result.airframe + local date = "n/a" if os then - date=os.date() + date = os.date() end - scores=scores..string.format("\n%s,%d,%s,%.2f,%03d,%s,%s,%s,%s,%s", playername, i, target, distance, radial, quality, weapon, airframe, time, date) + scores = scores .. string.format( "\n%s,%d,%s,%.2f,%03d,%s,%s,%s,%s,%s", playername, i, target, distance, radial, quality, weapon, airframe, time, date ) end end - _savefile(filename, scores) + _savefile( filename, scores ) end --- Function called before save event. Checks that io and lfs are desanitized. @@ -72312,11 +77294,11 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function RANGE:onbeforeLoad(From, Event, To) +function RANGE:onbeforeLoad( From, Event, To ) if io and lfs then return true else - self:E(self.id..string.format("WARNING: io and/or lfs not desanitized. Cannot load player results.")) + self:E( self.id .. string.format( "WARNING: io and/or lfs not desanitized. Cannot load player results." ) ) return false end end @@ -72326,128 +77308,198 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function RANGE:onafterLoad(From, Event, To) +function RANGE:onafterLoad( From, Event, To ) --- Function that load data from a file. - local function _loadfile(filename) - local f=io.open(filename, "rb") + local function _loadfile( filename ) + local f = io.open( filename, "rb" ) if f then - --self:I(self.id..string.format("Loading player results from file %s", tostring(filename))) - local data=f:read("*all") + -- self:I(self.id..string.format("Loading player results from file %s", tostring(filename))) + local data = f:read( "*all" ) f:close() return data else - self:E(self.id..string.format("WARNING: Could not load player results from file %s. File might not exist just yet.", tostring(filename))) + self:E( self.id .. string.format( "WARNING: Could not load player results from file %s. File might not exist just yet.", tostring( filename ) ) ) return nil end end -- Path in DCS log file. - local path=lfs.writedir()..[[Logs\]] + local path = lfs.writedir() .. [[Logs\]] -- Set file name. - local filename=path..string.format("RANGE-%s_BombingResults.csv", self.rangename) + local filename = path .. string.format( "RANGE-%s_BombingResults.csv", self.rangename ) -- Info message. - local text=string.format("Loading player bomb results from file %s", filename) - self:I(self.id..text) + local text = string.format( "Loading player bomb results from file %s", filename ) + self:I( self.id .. text ) -- Load asset data from file. - local data=_loadfile(filename) + local data = _loadfile( filename ) if data then -- Split by line break. - local results=UTILS.Split(data,"\n") + local results = UTILS.Split( data, "\n" ) -- Remove first header line. - table.remove(results, 1) + table.remove( results, 1 ) -- Init player scores table. - self.bombPlayerResults={} + self.bombPlayerResults = {} -- Loop over all lines. - for _,_result in pairs(results) do + for _, _result in pairs( results ) do -- Parameters are separated by commata. - local resultdata=UTILS.Split(_result, ",") + local resultdata = UTILS.Split( _result, "," ) -- Grade table - local result={} --#RANGE.BombResult + local result = {} -- #RANGE.BombResult -- Player name. - local playername=resultdata[1] - result.player=playername + local playername = resultdata[1] + result.player = playername -- Results data. - result.name=tostring(resultdata[3]) - result.distance=tonumber(resultdata[4]) - result.radial=tonumber(resultdata[5]) - result.quality=tostring(resultdata[6]) - result.weapon=tostring(resultdata[7]) - result.airframe=tostring(resultdata[8]) - result.time=UTILS.ClockToSeconds(resultdata[9] or "00:00:00") - result.date=resultdata[10] or "n/a" + result.name = tostring( resultdata[3] ) + result.distance = tonumber( resultdata[4] ) + result.radial = tonumber( resultdata[5] ) + result.quality = tostring( resultdata[6] ) + result.weapon = tostring( resultdata[7] ) + result.airframe = tostring( resultdata[8] ) + result.time = UTILS.ClockToSeconds( resultdata[9] or "00:00:00" ) + result.date = resultdata[10] or "n/a" -- Create player array if necessary. - self.bombPlayerResults[playername]=self.bombPlayerResults[playername] or {} + self.bombPlayerResults[playername] = self.bombPlayerResults[playername] or {} -- Add result to table. - table.insert(self.bombPlayerResults[playername], result) + table.insert( self.bombPlayerResults[playername], result ) end end end +--- Save target sheet. +-- @param #RANGE self +-- @param #string _playername Player name. +-- @param #RANGE.StrafeResult result Results table. +function RANGE:_SaveTargetSheet( _playername, result ) -- RangeBoss Specific Function + + --- Function that saves data to file + local function _savefile( filename, data ) + local f = io.open( filename, "wb" ) + if f then + f:write( data ) + f:close() + else + env.info( "RANGEBOSS EDIT - could not save target sheet to file" ) + -- self:E(self.lid..string.format("ERROR: could not save target sheet to file %s.\nFile may contain invalid characters.", tostring(filename))) + end + end + + -- Set path or default. + local path = self.targetpath + if lfs then + path = path or lfs.writedir() .. [[Logs\]] + end + + -- Create unused file name. + local filename = nil + for i = 1, 9999 do + + -- Create file name + if self.targetprefix then + filename = string.format( "%s_%s-%04d.csv", self.targetprefix, result.airframe, i ) + else + local name = UTILS.ReplaceIllegalCharacters( _playername, "_" ) + filename = string.format( "RANGERESULTS-%s_Targetsheet-%s-%04d.csv", self.rangename, name, i ) + end + + -- Set path. + if path ~= nil then + filename = path .. "\\" .. filename + end + + -- Check if file exists. + local _exists = UTILS.FileExists( filename ) + if not _exists then + break + end + end + + -- Header line + local data = "Name,Target,Rounds Fired,Rounds Hit,Rounds Quality,Airframe,Mission Time,OS Time\n" + + local target = result.name + local airframe = result.airframe + local roundsFired = result.roundsFired + local roundsHit = result.roundsHit + local strafeResult = result.roundsQuality + local time = UTILS.SecondsToClock( result.time ) + local date = "n/a" + if os then + date = os.date() + end + data = data .. string.format( "%s,%s,%d,%d,%s,%s,%s,%s", _playername, target, roundsFired, roundsHit, strafeResult, airframe, time, date ) + + -- Save file. + _savefile( filename, data ) +end + ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Display Messages ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Start smoking a coordinate with a delay. -- @param #table _args Argements passed. -function RANGE._DelayedSmoke(_args) - trigger.action.smoke(_args.coord:GetVec3(), _args.color) +function RANGE._DelayedSmoke( _args ) + trigger.action.smoke( _args.coord:GetVec3(), _args.color ) end --- Display top 10 stafing results of a specific player. -- @param #RANGE self -- @param #string _unitName Name of the player unit. -function RANGE:_DisplayMyStrafePitResults(_unitName) - self:F(_unitName) +function RANGE:_DisplayMyStrafePitResults( _unitName ) + self:F( _unitName ) -- Get player unit and name - local _unit,_playername = self:_GetPlayerUnitAndName(_unitName) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) if _unit and _playername then -- Message header. - local _message = string.format("My Top %d Strafe Pit Results:\n", self.ndisplayresult) + local _message = string.format( "My Top %d Strafe Pit Results:\n", self.ndisplayresult ) -- Get player results. - local _results = self.strafePlayerResults[_playername] + local _results = self.strafePlayerResults[_playername] -- Create message. if _results == nil then - -- No score yet. - _message = string.format("%s: No Score yet.", _playername) + -- No score yet. + _message = string.format( "%s: No Score yet.", _playername ) else -- Sort results table wrt number of hits. - local _sort = function( a,b ) return a.hits > b.hits end - table.sort(_results,_sort) + local _sort = function( a, b ) + return a.hits > b.hits + end + table.sort( _results, _sort ) -- Prepare message of best results. local _bestMsg = "" local _count = 1 -- Loop over results - for _,_result in pairs(_results) do + for _, _result in pairs( _results ) do + local result=_result --#RANGE.StrafeResult -- Message text. - _message = _message..string.format("\n[%d] Hits %d - %s - %s", _count, _result.hits, _result.zone.name, _result.text) + _message = _message .. string.format( "\n[%d] Hits %d - %s - %s", _count, result.roundsHit, result.name, result.roundsQuality ) -- Best result. if _bestMsg == "" then - _bestMsg = string.format("Hits %d - %s - %s", _result.hits, _result.zone.name, _result.text) + _bestMsg = string.format( "Hits %d - %s - %s", _result.hits, _result.zone.name, _result.text ) end -- 10 runs @@ -72456,26 +77508,26 @@ function RANGE:_DisplayMyStrafePitResults(_unitName) end -- Increase counter - _count = _count+1 + _count = _count + 1 end -- Message text. - _message = _message .."\n\nBEST: ".._bestMsg + _message = _message .. "\n\nBEST: " .. _bestMsg end -- Send message to group. - self:_DisplayMessageToGroup(_unit, _message, nil, true, true) + self:_DisplayMessageToGroup( _unit, _message, nil, true, true ) end end --- Display top 10 strafing results of all players. -- @param #RANGE self -- @param #string _unitName Name fo the player unit. -function RANGE:_DisplayStrafePitResults(_unitName) - self:F(_unitName) +function RANGE:_DisplayStrafePitResults( _unitName ) + self:F( _unitName ) -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) -- Check if we have a unit which is a player. if _unit and _playername then @@ -72484,14 +77536,14 @@ function RANGE:_DisplayStrafePitResults(_unitName) local _playerResults = {} -- Message text. - local _message = string.format("Strafe Pit Results - Top %d Players:\n", self.ndisplayresult) + local _message = string.format( "Strafe Pit Results - Top %d Players:\n", self.ndisplayresult ) -- Loop over player results. - for _playerName,_results in pairs(self.strafePlayerResults) do + for _playerName, _results in pairs( self.strafePlayerResults ) do -- Get the best result of the player. local _best = nil - for _,_result in pairs(_results) do + for _, _result in pairs( _results ) do if _best == nil or _result.hits > _best.hits then _best = _result end @@ -72499,226 +77551,252 @@ function RANGE:_DisplayStrafePitResults(_unitName) -- Add best result to table. if _best ~= nil then - local text=string.format("%s: Hits %i - %s - %s", _playerName, _best.hits, _best.zone.name, _best.text) - table.insert(_playerResults,{msg = text, hits = _best.hits}) + local text = string.format( "%s: Hits %i - %s - %s", _playerName, _best.hits, _best.zone.name, _best.text ) + table.insert( _playerResults, { msg = text, hits = _best.hits } ) end end - --Sort list! - local _sort = function( a,b ) return a.hits > b.hits end - table.sort(_playerResults,_sort) + -- Sort list! + local _sort = function( a, b ) + return a.hits > b.hits + end + table.sort( _playerResults, _sort ) -- Add top 10 results. - for _i = 1, math.min(#_playerResults, self.ndisplayresult) do - _message = _message..string.format("\n[%d] %s", _i, _playerResults[_i].msg) + for _i = 1, math.min( #_playerResults, self.ndisplayresult ) do + _message = _message .. string.format( "\n[%d] %s", _i, _playerResults[_i].msg ) end -- In case there are no scores yet. - if #_playerResults<1 then - _message = _message.."No player scored yet." + if #_playerResults < 1 then + _message = _message .. "No player scored yet." end -- Send message. - self:_DisplayMessageToGroup(_unit, _message, nil, true, true) + self:_DisplayMessageToGroup( _unit, _message, nil, true, true ) end end --- Display top 10 bombing run results of specific player. -- @param #RANGE self -- @param #string _unitName Name of the player unit. -function RANGE:_DisplayMyBombingResults(_unitName) - self:F(_unitName) +function RANGE:_DisplayMyBombingResults( _unitName ) + self:F( _unitName ) -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) if _unit and _playername then -- Init message. - local _message = string.format("My Top %d Bombing Results:\n", self.ndisplayresult) + local _message = string.format( "My Top %d Bombing Results:\n", self.ndisplayresult ) -- Results from player. local _results = self.bombPlayerResults[_playername] -- No score so far. if _results == nil then - _message = _playername..": No Score yet." + _message = _playername .. ": No Score yet." else -- Sort results wrt to distance. - local _sort = function( a,b ) return a.distance < b.distance end - table.sort(_results,_sort) + local _sort = function( a, b ) + return a.distance < b.distance + end + table.sort( _results, _sort ) -- Loop over results. local _bestMsg = "" - for i,_result in pairs(_results) do - local result=_result --#RANGE.BombResult + for i, _result in pairs( _results ) do + local result = _result -- #RANGE.BombResult -- Message with name, weapon and distance. - _message = _message.."\n"..string.format("[%d] %d m %03d° - %s - %s - %s hit", i, result.distance, result.radial, result.name, result.weapon, result.quality) + _message = _message .. "\n" .. string.format( "[%d] %d m %03d° - %s - %s - %s hit", i, result.distance, result.radial, result.name, result.weapon, result.quality ) -- Store best/first result. if _bestMsg == "" then - _bestMsg = string.format("%d m %03d° - %s - %s - %s hit", result.distance, result.radial, result.name, result.weapon, result.quality) + _bestMsg = string.format( "%d m %03d° - %s - %s - %s hit", result.distance, result.radial, result.name, result.weapon, result.quality ) end -- Best 10 runs only. - if i==self.ndisplayresult then + if i == self.ndisplayresult then break end end -- Message. - _message = _message .."\n\nBEST: ".._bestMsg + _message = _message .. "\n\nBEST: " .. _bestMsg end -- Send message. - self:_DisplayMessageToGroup(_unit, _message, nil, true, true) + self:_DisplayMessageToGroup( _unit, _message, nil, true, true ) end end --- Display best bombing results of top 10 players. -- @param #RANGE self -- @param #string _unitName Name of player unit. -function RANGE:_DisplayBombingResults(_unitName) - self:F(_unitName) +function RANGE:_DisplayBombingResults( _unitName ) + self:F( _unitName ) -- Results table. local _playerResults = {} -- Get player unit and name. - local _unit, _player = self:_GetPlayerUnitAndName(_unitName) + local _unit, _player = self:_GetPlayerUnitAndName( _unitName ) -- Check if we have a unit with a player. if _unit and _player then -- Message header. - local _message = string.format("Bombing Results - Top %d Players:\n", self.ndisplayresult) + local _message = string.format( "Bombing Results - Top %d Players:\n", self.ndisplayresult ) -- Loop over players. - for _playerName,_results in pairs(self.bombPlayerResults) do + for _playerName, _results in pairs( self.bombPlayerResults ) do -- Find best result of player. local _best = nil - for _,_result in pairs(_results) do + for _, _result in pairs( _results ) do if _best == nil or _result.distance < _best.distance then - _best = _result + _best = _result end end -- Put best result of player into table. if _best ~= nil then - local bestres=string.format("%s: %d m - %s - %s - %s hit", _playerName, _best.distance, _best.name, _best.weapon, _best.quality) - table.insert(_playerResults, {msg = bestres, distance = _best.distance}) + local bestres = string.format( "%s: %d m - %s - %s - %s hit", _playerName, _best.distance, _best.name, _best.weapon, _best.quality ) + table.insert( _playerResults, { msg = bestres, distance = _best.distance } ) end end -- Sort list of player results. - local _sort = function( a,b ) return a.distance < b.distance end - table.sort(_playerResults,_sort) + local _sort = function( a, b ) + return a.distance < b.distance + end + table.sort( _playerResults, _sort ) -- Loop over player results. - for _i = 1, math.min(#_playerResults, self.ndisplayresult) do - _message = _message..string.format("\n[%d] %s", _i, _playerResults[_i].msg) + for _i = 1, math.min( #_playerResults, self.ndisplayresult ) do + _message = _message .. string.format( "\n[%d] %s", _i, _playerResults[_i].msg ) end -- In case there are no scores yet. - if #_playerResults<1 then - _message = _message.."No player scored yet." + if #_playerResults < 1 then + _message = _message .. "No player scored yet." end -- Send message. - self:_DisplayMessageToGroup(_unit, _message, nil, true, true) + self:_DisplayMessageToGroup( _unit, _message, nil, true, true ) end end --- Report information like bearing and range from player unit to range. -- @param #RANGE self -- @param #string _unitname Name of the player unit. -function RANGE:_DisplayRangeInfo(_unitname) - self:F(_unitname) +function RANGE:_DisplayRangeInfo( _unitname ) + self:F( _unitname ) -- Get player unit and player name. - local unit, playername = self:_GetPlayerUnitAndName(_unitname) + local unit, playername = self:_GetPlayerUnitAndName( _unitname ) -- Check if we have a player. if unit and playername then -- Message text. - local text="" + local text = "" -- Current coordinates. - local coord=unit:GetCoordinate() + local coord = unit:GetCoordinate() if self.location then - local settings=_DATABASE:GetPlayerSettings(playername) or _SETTINGS --Core.Settings#SETTINGS + local settings = _DATABASE:GetPlayerSettings( playername ) or _SETTINGS -- Core.Settings#SETTINGS -- Direction vector from current position (coord) to target (position). - local position=self.location --Core.Point#COORDINATE - local bulls=position:ToStringBULLS(unit:GetCoalition(), settings) - local lldms=position:ToStringLLDMS(settings) - local llddm=position:ToStringLLDDM(settings) - local rangealt=position:GetLandHeight() - local vec3=coord:GetDirectionVec3(position) - local angle=coord:GetAngleDegrees(vec3) - local range=coord:Get2DDistance(position) + local position = self.location -- Core.Point#COORDINATE + local bulls = position:ToStringBULLS( unit:GetCoalition(), settings ) + local lldms = position:ToStringLLDMS( settings ) + local llddm = position:ToStringLLDDM( settings ) + local rangealt = position:GetLandHeight() + local vec3 = coord:GetDirectionVec3( position ) + local angle = coord:GetAngleDegrees( vec3 ) + local range = coord:Get2DDistance( position ) -- Bearing string. - local Bs=string.format('%03d°', angle) + local Bs = string.format( '%03d°', angle ) local texthit if self.PlayerSettings[playername].flaredirecthits then - texthit=string.format("Flare direct hits: ON (flare color %s)\n", self:_flarecolor2text(self.PlayerSettings[playername].flarecolor)) + texthit = string.format( "Flare direct hits: ON (flare color %s)\n", self:_flarecolor2text( self.PlayerSettings[playername].flarecolor ) ) else - texthit=string.format("Flare direct hits: OFF\n") + texthit = string.format( "Flare direct hits: OFF\n" ) end local textbomb if self.PlayerSettings[playername].smokebombimpact then - textbomb=string.format("Smoke bomb impact points: ON (smoke color %s)\n", self:_smokecolor2text(self.PlayerSettings[playername].smokecolor)) + textbomb = string.format( "Smoke bomb impact points: ON (smoke color %s)\n", self:_smokecolor2text( self.PlayerSettings[playername].smokecolor ) ) else - textbomb=string.format("Smoke bomb impact points: OFF\n") + textbomb = string.format( "Smoke bomb impact points: OFF\n" ) end local textdelay if self.PlayerSettings[playername].delaysmoke then - textdelay=string.format("Smoke bomb delay: ON (delay %.1f seconds)", self.TdelaySmoke) + textdelay = string.format( "Smoke bomb delay: ON (delay %.1f seconds)", self.TdelaySmoke ) else - textdelay=string.format("Smoke bomb delay: OFF") + textdelay = string.format( "Smoke bomb delay: OFF" ) end -- Player unit settings. - local trange=string.format("%.1f km", range/1000) - local trangealt=string.format("%d m", rangealt) - local tstrafemaxalt=string.format("%d m", self.strafemaxalt) + local trange = string.format( "%.1f km", range / 1000 ) + local trangealt = string.format( "%d m", rangealt ) + local tstrafemaxalt = string.format( "%d m", self.strafemaxalt ) if settings:IsImperial() then - trange=string.format("%.1f NM", UTILS.MetersToNM(range)) - trangealt=string.format("%d feet", UTILS.MetersToFeet(rangealt)) - tstrafemaxalt=string.format("%d feet", UTILS.MetersToFeet(self.strafemaxalt)) + trange = string.format( "%.1f NM", UTILS.MetersToNM( range ) ) + trangealt = string.format( "%d feet", UTILS.MetersToFeet( rangealt ) ) + tstrafemaxalt = string.format( "%d feet", UTILS.MetersToFeet( self.strafemaxalt ) ) end -- Message. - text=text..string.format("Information on %s:\n", self.rangename) - text=text..string.format("-------------------------------------------------------\n") - text=text..string.format("Bearing %s, Range %s\n", Bs, trange) - text=text..string.format("%s\n", bulls) - text=text..string.format("%s\n", lldms) - text=text..string.format("%s\n", llddm) - text=text..string.format("Altitude ASL: %s\n", trangealt) - 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) - text=text..texthit - text=text..textbomb - text=text..textdelay + text = text .. string.format( "Information on %s:\n", self.rangename ) + text = text .. string.format( "-------------------------------------------------------\n" ) + text = text .. string.format( "Bearing %s, Range %s\n", Bs, trange ) + text = text .. string.format( "%s\n", bulls ) + text = text .. string.format( "%s\n", lldms ) + text = text .. string.format( "%s\n", llddm ) + text = text .. string.format( "Altitude ASL: %s\n", trangealt ) + 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 -- Send message to player group. - self:_DisplayMessageToGroup(unit, text, nil, true, true) + self:_DisplayMessageToGroup( unit, text, nil, true, true ) -- Debug output. - self:T2(self.id..text) + self:T2( self.id .. text ) end end end @@ -72726,150 +77804,148 @@ end --- Display bombing target locations to player. -- @param #RANGE self -- @param #string _unitname Name of the player unit. -function RANGE:_DisplayBombTargets(_unitname) - self:F(_unitname) +function RANGE:_DisplayBombTargets( _unitname ) + self:F( _unitname ) -- Get player unit and player name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitname) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitname ) -- Check if we have a player. if _unit and _playername then -- Player settings. - local _settings=_DATABASE:GetPlayerSettings(_playername) or _SETTINGS --Core.Settings#SETTINGS + local _settings = _DATABASE:GetPlayerSettings( _playername ) or _SETTINGS -- Core.Settings#SETTINGS -- Message text. - local _text="Bomb Target Locations:" + local _text = "Bomb Target Locations:" - for _,_bombtarget in pairs(self.bombingTargets) do - local bombtarget=_bombtarget --#RANGE.BombTarget + for _, _bombtarget in pairs( self.bombingTargets ) do + local bombtarget = _bombtarget -- #RANGE.BombTarget -- Coordinate of bombtarget. - local coord=self:_GetBombTargetCoordinate(bombtarget) + local coord = self:_GetBombTargetCoordinate( bombtarget ) if coord then - + -- Get elevation - local elevation=coord:GetLandHeight() - local eltxt=string.format("%d m", elevation) + local elevation = coord:GetLandHeight() + local eltxt = string.format( "%d m", elevation ) if not _settings:IsMetric() then - elevation=UTILS.MetersToFeet(elevation) - eltxt=string.format("%d ft", elevation) + elevation = UTILS.MetersToFeet( elevation ) + eltxt = string.format( "%d ft", elevation ) end - local ca2g=coord:ToStringA2G(_unit,_settings) - _text=_text..string.format("\n- %s:\n%s @ %s", bombtarget.name or "unknown", ca2g, eltxt) + local ca2g = coord:ToStringA2G( _unit, _settings ) + _text = _text .. string.format( "\n- %s:\n%s @ %s", bombtarget.name or "unknown", ca2g, eltxt ) end end - self:_DisplayMessageToGroup(_unit,_text, 60, true, true) + self:_DisplayMessageToGroup( _unit, _text, 120, true, true ) end end --- Display pit location and heading to player. -- @param #RANGE self -- @param #string _unitname Name of the player unit. -function RANGE:_DisplayStrafePits(_unitname) - self:F(_unitname) +function RANGE:_DisplayStrafePits( _unitname ) + self:F( _unitname ) -- Get player unit and player name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitname) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitname ) -- Check if we have a player. if _unit and _playername then -- Player settings. - local _settings=_DATABASE:GetPlayerSettings(_playername) or _SETTINGS --Core.Settings#SETTINGS + local _settings = _DATABASE:GetPlayerSettings( _playername ) or _SETTINGS -- Core.Settings#SETTINGS -- Message text. - local _text="Strafe Target Locations:" + local _text = "Strafe Target Locations:" - for _,_strafepit in pairs(self.strafeTargets) do - local _target=_strafepit --Wrapper.Positionable#POSITIONABLE + for _, _strafepit in pairs( self.strafeTargets ) do + local _target = _strafepit -- Wrapper.Positionable#POSITIONABLE -- Pit parameters. - local coord=_strafepit.coordinate --Core.Point#COORDINATE - local heading=_strafepit.heading + local coord = _strafepit.coordinate -- Core.Point#COORDINATE + local heading = _strafepit.heading -- Turn heading around ==> approach heading. - if heading>180 then - heading=heading-180 + if heading > 180 then + heading = heading - 180 else - heading=heading+180 + heading = heading + 180 end - local mycoord=coord:ToStringA2G(_unit, _settings) - _text=_text..string.format("\n- %s: heading %03d°\n%s",_strafepit.name, heading, mycoord) + local mycoord = coord:ToStringA2G( _unit, _settings ) + _text = _text .. string.format( "\n- %s: heading %03d°\n%s", _strafepit.name, heading, mycoord ) end - self:_DisplayMessageToGroup(_unit,_text, nil, true, true) + self:_DisplayMessageToGroup( _unit, _text, nil, true, true ) end end - --- Report weather conditions at range. Temperature, QFE pressure and wind data. -- @param #RANGE self -- @param #string _unitname Name of the player unit. -function RANGE:_DisplayRangeWeather(_unitname) - self:F(_unitname) +function RANGE:_DisplayRangeWeather( _unitname ) + self:F( _unitname ) -- Get player unit and player name. - local unit, playername = self:_GetPlayerUnitAndName(_unitname) + local unit, playername = self:_GetPlayerUnitAndName( _unitname ) -- Check if we have a player. if unit and playername then -- Message text. - local text="" + local text = "" -- Current coordinates. - local coord=unit:GetCoordinate() + local coord = unit:GetCoordinate() if self.location then -- Get atmospheric data at range location. - local position=self.location --Core.Point#COORDINATE - local T=position:GetTemperature() - local P=position:GetPressure() - local Wd,Ws=position:GetWind() + local position = self.location -- Core.Point#COORDINATE + local T = position:GetTemperature() + local P = position:GetPressure() + local Wd, Ws = position:GetWind() -- Get Beaufort wind scale. - local Bn,Bd=UTILS.BeaufortScale(Ws) + local Bn, Bd = UTILS.BeaufortScale( Ws ) - local WD=string.format('%03d°', Wd) - local Ts=string.format("%d°C",T) + local WD = string.format( '%03d°', Wd ) + local Ts = string.format( "%d°C", T ) - local hPa2inHg=0.0295299830714 - local hPa2mmHg=0.7500615613030 + local hPa2inHg = 0.0295299830714 + local hPa2mmHg = 0.7500615613030 - local settings=_DATABASE:GetPlayerSettings(playername) or _SETTINGS --Core.Settings#SETTINGS - local tT=string.format("%d°C",T) - local tW=string.format("%.1f m/s", Ws) - local tP=string.format("%.1f mmHg", P*hPa2mmHg) + local settings = _DATABASE:GetPlayerSettings( playername ) or _SETTINGS -- Core.Settings#SETTINGS + local tT = string.format( "%d°C", T ) + local tW = string.format( "%.1f m/s", Ws ) + local tP = string.format( "%.1f mmHg", P * hPa2mmHg ) if settings:IsImperial() then - --tT=string.format("%d°F", UTILS.CelciusToFarenheit(T)) - tW=string.format("%.1f knots", UTILS.MpsToKnots(Ws)) - tP=string.format("%.2f inHg", P*hPa2inHg) + -- tT=string.format("%d°F", UTILS.CelsiusToFahrenheit(T)) + tW = string.format( "%.1f knots", UTILS.MpsToKnots( Ws ) ) + tP = string.format( "%.2f inHg", P * hPa2inHg ) end - -- Message text. - text=text..string.format("Weather Report at %s:\n", self.rangename) - text=text..string.format("--------------------------------------------------\n") - text=text..string.format("Temperature %s\n", tT) - text=text..string.format("Wind from %s at %s (%s)\n", WD, tW, Bd) - text=text..string.format("QFE %.1f hPa = %s", P, tP) + text = text .. string.format( "Weather Report at %s:\n", self.rangename ) + text = text .. string.format( "--------------------------------------------------\n" ) + text = text .. string.format( "Temperature %s\n", tT ) + text = text .. string.format( "Wind from %s at %s (%s)\n", WD, tW, Bd ) + text = text .. string.format( "QFE %.1f hPa = %s", P, tP ) else - text=string.format("No range location defined for range %s.", self.rangename) + text = string.format( "No range location defined for range %s.", self.rangename ) end -- Send message to player group. - self:_DisplayMessageToGroup(unit, text, nil, true, true) + self:_DisplayMessageToGroup( unit, text, nil, true, true ) -- Debug output. - self:T2(self.id..text) + self:T2( self.id .. text ) else - self:T(self.id..string.format("ERROR! Could not find player unit in RangeInfo! Name = %s", _unitname)) + self:T( self.id .. string.format( "ERROR! Could not find player unit in RangeInfo! Name = %s", _unitname ) ) end end @@ -72881,23 +77957,23 @@ end -- @param #string _unitName Name of player unit. function RANGE:_CheckPlayers() - for playername,_playersettings in pairs(self.PlayerSettings) do - local playersettings=_playersettings --#RANGE.PlayerData + for playername, _playersettings in pairs( self.PlayerSettings ) do + local playersettings = _playersettings -- #RANGE.PlayerData - local unitname=playersettings.unitname - local unit=UNIT:FindByName(unitname) + local unitname = playersettings.unitname + local unit = UNIT:FindByName( unitname ) if unit and unit:IsAlive() then - if unit:IsInZone(self.rangezone) then + if unit:IsInZone( self.rangezone ) then ------------------------------ -- Player INSIDE Range Zone -- ------------------------------ if not playersettings.inzone then - playersettings.inzone=true - self:EnterRange(playersettings) + playersettings.inzone = true + self:EnterRange( playersettings ) end else @@ -72906,9 +77982,9 @@ function RANGE:_CheckPlayers() -- Player OUTSIDE Range Zone -- ------------------------------- - if playersettings.inzone==true then - playersettings.inzone=false - self:ExitRange(playersettings) + if playersettings.inzone == true then + playersettings.inzone = false + self:ExitRange( playersettings ) end end @@ -72920,64 +77996,67 @@ end --- Check if player is inside a strafing zone. If he is, we start looking for hits. If he was and left the zone again, the result is stored. -- @param #RANGE self -- @param #string _unitName Name of player unit. -function RANGE:_CheckInZone(_unitName) - self:F2(_unitName) +function RANGE:_CheckInZone( _unitName ) + self:F2( _unitName ) -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) + local unitheading = 0 -- RangeBoss if _unit and _playername then - + + -- Player data. + local playerData=self.PlayerSettings[_playername] -- #RANGE.PlayerData + --- Function to check if unit is in zone and facing in the right direction and is below the max alt. - local function checkme(targetheading, _zone) - local zone=_zone --Core.Zone#ZONE + local function checkme( targetheading, _zone ) + local zone = _zone -- Core.Zone#ZONE -- Heading check. - local unitheading = _unit:GetHeading() - local pitheading = targetheading-180 - local deltaheading = unitheading-pitheading - local towardspit = math.abs(deltaheading)<=90 or math.abs(deltaheading-360)<=90 - + local unitheading = _unit:GetHeading() + local pitheading = targetheading - 180 + local deltaheading = unitheading - pitheading + local towardspit = math.abs( deltaheading ) <= 90 or math.abs( deltaheading - 360 ) <= 90 + if towardspit then - - local vec3=_unit:GetVec3() - local vec2={x=vec3.x, y=vec3.z} --DCS#Vec2 - local landheight=land.getHeight(vec2) - local unitalt=vec3.y-landheight - - if unitalt<=self.strafemaxalt then - local unitinzone=zone:IsVec2InZone(vec2) + + local vec3 = _unit:GetVec3() + local vec2 = { x = vec3.x, y = vec3.z } -- DCS#Vec2 + local landheight = land.getHeight( vec2 ) + local unitalt = vec3.y - landheight + + if unitalt <= self.strafemaxalt then + local unitinzone = zone:IsVec2InZone( vec2 ) return unitinzone end end - + return false end -- Current position of player unit. - local _unitID = _unit:GetID() + local _unitID = _unit:GetID() -- Currently strafing? (strafeStatus is nil if not) - local _currentStrafeRun = self.strafeStatus[_unitID] + local _currentStrafeRun = self.strafeStatus[_unitID] --#RANGE.StrafeStatus - if _currentStrafeRun then -- player has already registered for a strafing run. + if _currentStrafeRun then -- player has already registered for a strafing run. -- Get the current approach zone and check if player is inside. - local zone=_currentStrafeRun.zone.polygon --Core.Zone#ZONE_POLYGON_BASE + local zone = _currentStrafeRun.zone.polygon -- Core.Zone#ZONE_POLYGON_BASE -- Check if unit in zone and facing the right direction. - local unitinzone=checkme(_currentStrafeRun.zone.heading, zone) + local unitinzone = checkme( _currentStrafeRun.zone.heading, zone ) -- Check if player is in strafe zone and below max alt. if unitinzone then - -- Still in zone, keep counting hits. Increase counter. - _currentStrafeRun.time = _currentStrafeRun.time+1 + _currentStrafeRun.time = _currentStrafeRun.time + 1 else -- Increase counter - _currentStrafeRun.time = _currentStrafeRun.time+1 + _currentStrafeRun.time = _currentStrafeRun.time + 1 if _currentStrafeRun.time <= 3 then @@ -72985,24 +78064,26 @@ function RANGE:_CheckInZone(_unitName) self.strafeStatus[_unitID] = nil -- Message text. - local _msg = string.format("%s left strafing zone %s too quickly. No Score.", _playername, _currentStrafeRun.zone.name) + local _msg = string.format( "%s left strafing zone %s too quickly. No Score.", _playername, _currentStrafeRun.zone.name ) -- Send message. - self:_DisplayMessageToGroup(_unit, _msg, nil, true) - + self:_DisplayMessageToGroup( _unit, _msg, nil, true ) + if self.rangecontrol then - self.rangecontrol:NewTransmission(RANGE.Sound.RCLeftStrafePitTooQuickly.filename, RANGE.Sound.RCLeftStrafePitTooQuickly.duration, self.soundpath) + self.rangecontrol:NewTransmission( RANGE.Sound.RCLeftStrafePitTooQuickly.filename, RANGE.Sound.RCLeftStrafePitTooQuickly.duration, self.soundpath ) end else -- Get current ammo. - local _ammo=self:_GetAmmo(_unitName) + local _ammo = self:_GetAmmo( _unitName ) -- Result. - local _result = self.strafeStatus[_unitID] - local _sound = nil --#RANGE.Soundfile - + local _result = self.strafeStatus[_unitID] --#RANGE.StrafeStatus + + local _sound = nil -- #RANGE.Soundfile + + --[[ --RangeBoss commented out in order to implement strafe quality based on accuracy percentage, not the number of rounds on target -- Judge this pass. Text is displayed on summary. if _result.hits >= _result.zone.goodPass*2 then _result.text = "EXCELLENT PASS" @@ -73017,37 +78098,85 @@ function RANGE:_CheckInZone(_unitName) _result.text = "POOR PASS" _sound=RANGE.Sound.RCPoorPass end - + ]] + -- Calculate accuracy of run. Number of hits wrt number of rounds fired. - local shots=_result.ammo-_ammo - local accur=0 - if shots>0 then - accur=_result.hits/shots*100 - if accur > 100 then accur = 100 end + local shots = _result.ammo - _ammo + local accur = 0 + if shots > 0 then + accur = _result.hits / shots * 100 + if accur > 100 then + accur = 100 + end + end + + -- Results text and sound message. + local resulttext="" + if _result.pastfoulline == true then -- + resulttext = "* INVALID - PASSED FOUL LINE *" + _sound = RANGE.Sound.RCPoorPass -- + else + if accur >= 90 then + resulttext = "DEADEYE PASS" + _sound = RANGE.Sound.RCExcellentPass + elseif accur >= 75 then + resulttext = "EXCELLENT PASS" + _sound = RANGE.Sound.RCExcellentPass + elseif accur >= 50 then + resulttext = "GOOD PASS" + _sound = RANGE.Sound.RCGoodPass + elseif accur >= 25 then + resulttext = "INEFFECTIVE PASS" + _sound = RANGE.Sound.RCIneffectivePass + else + resulttext = "POOR PASS" + _sound = RANGE.Sound.RCPoorPass + end end -- Message text. - local _text=string.format("%s, hits on target %s: %d", self:_myname(_unitName), _result.zone.name, _result.hits) + local _text = string.format( "%s, hits on target %s: %d", self:_myname( _unitName ), _result.zone.name, _result.hits ) if shots and accur then - _text=_text..string.format("\nTotal rounds fired %d. Accuracy %.1f %%.", shots, accur) + _text = _text .. string.format( "\nTotal rounds fired %d. Accuracy %.1f %%.", shots, accur ) end - _text=_text..string.format("\n%s", _result.text) + _text = _text .. string.format( "\n%s", resulttext ) -- Send message. - self:_DisplayMessageToGroup(_unit, _text) + self:_DisplayMessageToGroup( _unit, _text ) + -- Strafe result. + local result = {} -- #RANGE.StrafeResult + result.player=_playername + result.name=_result.zone.name or "unknown" + result.time = timer.getAbsTime() + result.roundsFired = shots + result.roundsHit = _result.hits + result.roundsQuality = resulttext + result.strafeAccuracy = accur + result.rangename = self.rangename + result.airframe=playerData.airframe + result.invalid = _result.pastfoulline + + -- Griger Results. + self:StrafeResult(playerData, result) + + -- Save trap sheet. + if playerData and playerData.targeton and self.targetsheet then + self:_SaveTargetSheet( _playername, result ) + end + -- Voice over. - if self.rangecontrol then - self.rangecontrol:NewTransmission(RANGE.Sound.RCHitsOnTarget.filename, RANGE.Sound.RCHitsOnTarget.duration, self.soundpath) - self.rangecontrol:Number2Transmission(string.format("%d", _result.hits)) + if self.rangecontrol then + self.rangecontrol:NewTransmission( RANGE.Sound.RCHitsOnTarget.filename, RANGE.Sound.RCHitsOnTarget.duration, self.soundpath ) + self.rangecontrol:Number2Transmission( string.format( "%d", _result.hits ) ) if shots and accur then - self.rangecontrol:NewTransmission(RANGE.Sound.RCTotalRoundsFired.filename, RANGE.Sound.RCTotalRoundsFired.duration, self.soundpath, nil, 0.2) - self.rangecontrol:Number2Transmission(string.format("%d", shots), nil, 0.2) - self.rangecontrol:NewTransmission(RANGE.Sound.RCAccuracy.filename, RANGE.Sound.RCAccuracy.duration, self.soundpath, nil, 0.2) - self.rangecontrol:Number2Transmission(string.format("%d", UTILS.Round(accur, 0))) - self.rangecontrol:NewTransmission(RANGE.Sound.RCPercent.filename, RANGE.Sound.RCPercent.duration, self.soundpath) + self.rangecontrol:NewTransmission( RANGE.Sound.RCTotalRoundsFired.filename, RANGE.Sound.RCTotalRoundsFired.duration, self.soundpath, nil, 0.2 ) + self.rangecontrol:Number2Transmission( string.format( "%d", shots ), nil, 0.2 ) + self.rangecontrol:NewTransmission( RANGE.Sound.RCAccuracy.filename, RANGE.Sound.RCAccuracy.duration, self.soundpath, nil, 0.2 ) + self.rangecontrol:Number2Transmission( string.format( "%d", UTILS.Round( accur, 0 ) ) ) + self.rangecontrol:NewTransmission( RANGE.Sound.RCPercent.filename, RANGE.Sound.RCPercent.duration, self.soundpath ) end - self.rangecontrol:NewTransmission(_sound.filename, _sound.duration, self.soundpath, nil, 0.5) + self.rangecontrol:NewTransmission( _sound.filename, _sound.duration, self.soundpath, nil, 0.5 ) end -- Set strafe status to nil. @@ -73055,7 +78184,7 @@ function RANGE:_CheckInZone(_unitName) -- Save stats so the player can retrieve them. local _stats = self.strafePlayerResults[_playername] or {} - table.insert(_stats, _result) + table.insert( _stats, result ) self.strafePlayerResults[_playername] = _stats end @@ -73064,32 +78193,36 @@ function RANGE:_CheckInZone(_unitName) else -- Check to see if we're in any of the strafing zones (first time). - for _,_targetZone in pairs(self.strafeTargets) do + for _, _targetZone in pairs( self.strafeTargets ) do + local target=_targetZone --#RANGE.StrafeTarget -- Get the current approach zone and check if player is inside. - local zone=_targetZone.polygon --Core.Zone#ZONE_POLYGON_BASE + local zone = target.polygon -- Core.Zone#ZONE_POLYGON_BASE -- Check if unit in zone and facing the right direction. - local unitinzone=checkme(_targetZone.heading, zone) + local unitinzone = checkme( target.heading, zone ) -- Player is inside zone. if unitinzone then -- Get ammo at the beginning of the run. - local _ammo=self:_GetAmmo(_unitName) + local _ammo = self:_GetAmmo( _unitName ) -- Init strafe status for this player. - self.strafeStatus[_unitID] = {hits = 0, zone = _targetZone, time = 1, ammo=_ammo, pastfoulline=false} + self.strafeStatus[_unitID] = { hits = 0, zone = target, time = 1, ammo = _ammo, pastfoulline = false } -- Rolling in! - local _msg=string.format("%s, rolling in on strafe pit %s.", self:_myname(_unitName), _targetZone.name) - + local _msg = string.format( "%s, rolling in on strafe pit %s.", self:_myname( _unitName ), target.name ) + if self.rangecontrol then - self.rangecontrol:NewTransmission(RANGE.Sound.RCRollingInOnStrafeTarget.filename, RANGE.Sound.RCRollingInOnStrafeTarget.duration, self.soundpath) - end + self.rangecontrol:NewTransmission( RANGE.Sound.RCRollingInOnStrafeTarget.filename, RANGE.Sound.RCRollingInOnStrafeTarget.duration, self.soundpath ) + end -- Send message. - self:_DisplayMessageToGroup(_unit, _msg, 10, true) + self:_DisplayMessageToGroup( _unit, _msg, 10, true ) + + -- Trigger event that player is rolling in. + self:RollingIn(playerData, target) -- We found our player. Skip remaining checks. break @@ -73109,18 +78242,18 @@ end --- Add menu commands for player. -- @param #RANGE self -- @param #string _unitName Name of player unit. -function RANGE:_AddF10Commands(_unitName) - self:F(_unitName) +function RANGE:_AddF10Commands( _unitName ) + self:F( _unitName ) -- Get player unit and name. - local _unit, playername = self:_GetPlayerUnitAndName(_unitName) + local _unit, playername = self:_GetPlayerUnitAndName( _unitName ) -- Check for player unit. if _unit and playername then -- Get group and ID. - local group=_unit:GetGroup() - local _gid=group:GetID() + local group = _unit:GetGroup() + local _gid = group:GetID() if group and _gid then @@ -73130,7 +78263,7 @@ function RANGE:_AddF10Commands(_unitName) self.MenuAddedTo[_gid] = true -- Range root menu path. - local _rangePath=nil + local _rangePath = nil if RANGE.MenuF10Root then @@ -73138,7 +78271,8 @@ function RANGE:_AddF10Commands(_unitName) -- MISSION LEVEL -- ------------------- - _rangePath = missionCommands.addSubMenuForGroup(_gid, self.rangename, RANGE.MenuF10Root) + -- _rangePath = missionCommands.addSubMenuForGroup(_gid, self.rangename, RANGE.MenuF10Root) + _rangePath = MENU_GROUP:New( group, "On the Range" ) else @@ -73148,61 +78282,63 @@ function RANGE:_AddF10Commands(_unitName) -- Main F10 menu: F10/On the Range// if RANGE.MenuF10[_gid] == nil then - RANGE.MenuF10[_gid]=missionCommands.addSubMenuForGroup(_gid, "On the Range") + -- RANGE.MenuF10[_gid]=missionCommands.addSubMenuForGroup(_gid, "On the Range") + RANGE.MenuF10[_gid] = MENU_GROUP:New( group, "On the Range" ) end - _rangePath = missionCommands.addSubMenuForGroup(_gid, self.rangename, RANGE.MenuF10[_gid]) - + -- _rangePath = missionCommands.addSubMenuForGroup(_gid, self.rangename, RANGE.MenuF10[_gid]) + _rangePath = MENU_GROUP:New( group, self.rangename, RANGE.MenuF10[_gid] ) end + local _statsPath = MENU_GROUP:New( group, "Statistics", _rangePath ) + local _markPath = MENU_GROUP:New( group, "Mark Targets", _rangePath ) + local _settingsPath = MENU_GROUP:New( group, "My Settings", _rangePath ) + local _infoPath = MENU_GROUP:New( group, "Range Info", _rangePath ) - local _statsPath = missionCommands.addSubMenuForGroup(_gid, "Statistics", _rangePath) - local _markPath = missionCommands.addSubMenuForGroup(_gid, "Mark Targets", _rangePath) - local _settingsPath = missionCommands.addSubMenuForGroup(_gid, "My Settings", _rangePath) - local _infoPath = missionCommands.addSubMenuForGroup(_gid, "Range Info", _rangePath) -- F10/On the Range//My Settings/ - local _mysmokePath = missionCommands.addSubMenuForGroup(_gid, "Smoke Color", _settingsPath) - local _myflarePath = missionCommands.addSubMenuForGroup(_gid, "Flare Color", _settingsPath) + local _mysmokePath = MENU_GROUP:New( group, "Smoke Color", _settingsPath ) + local _myflarePath = MENU_GROUP:New( group, "Flare Color", _settingsPath ) -- F10/On the Range//Mark Targets/ - missionCommands.addCommandForGroup(_gid, "Mark On Map", _markPath, self._MarkTargetsOnMap, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Illuminate Range", _markPath, self._IlluminateBombTargets, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Smoke Strafe Pits", _markPath, self._SmokeStrafeTargetBoxes, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Smoke Strafe Tgts", _markPath, self._SmokeStrafeTargets, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Smoke Bomb Tgts", _markPath, self._SmokeBombTargets, self, _unitName) + local _MoMap = MENU_GROUP_COMMAND:New( group, "Mark On Map", _markPath, self._MarkTargetsOnMap, self, _unitName ) + local _IllRng = MENU_GROUP_COMMAND:New( group, "Illuminate Range", _markPath, self._IlluminateBombTargets, self, _unitName ) + local _SSpit = MENU_GROUP_COMMAND:New( group, "Smoke Strafe Pits", _markPath, self._SmokeStrafeTargetBoxes, self, _unitName ) + local _SStgts = MENU_GROUP_COMMAND:New( group, "Smoke Strafe Tgts", _markPath, self._SmokeStrafeTargets, self, _unitName ) + local _SBtgts = MENU_GROUP_COMMAND:New( group, "Smoke Bomb Tgts", _markPath, self._SmokeBombTargets, self, _unitName ) -- F10/On the Range//Stats/ - missionCommands.addCommandForGroup(_gid, "All Strafe Results", _statsPath, self._DisplayStrafePitResults, self, _unitName) - missionCommands.addCommandForGroup(_gid, "All Bombing Results", _statsPath, self._DisplayBombingResults, self, _unitName) - missionCommands.addCommandForGroup(_gid, "My Strafe Results", _statsPath, self._DisplayMyStrafePitResults, self, _unitName) - missionCommands.addCommandForGroup(_gid, "My Bomb Results", _statsPath, self._DisplayMyBombingResults, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Reset All Stats", _statsPath, self._ResetRangeStats, self, _unitName) + local _AllSR = MENU_GROUP_COMMAND:New( group, "All Strafe Results", _statsPath, self._DisplayStrafePitResults, self, _unitName ) + local _AllBR = MENU_GROUP_COMMAND:New( group, "All Bombing Results", _statsPath, self._DisplayBombingResults, self, _unitName ) + local _MySR = MENU_GROUP_COMMAND:New( group, "My Strafe Results", _statsPath, self._DisplayMyStrafePitResults, self, _unitName ) + local _MyBR = MENU_GROUP_COMMAND:New( group, "My Bomb Results", _statsPath, self._DisplayMyBombingResults, self, _unitName ) + local _ResetST = MENU_GROUP_COMMAND:New( group, "Reset All Stats", _statsPath, self._ResetRangeStats, self, _unitName ) -- F10/On the Range//My Settings/Smoke Color/ - missionCommands.addCommandForGroup(_gid, "Blue Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Blue) - missionCommands.addCommandForGroup(_gid, "Green Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Green) - missionCommands.addCommandForGroup(_gid, "Orange Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Orange) - missionCommands.addCommandForGroup(_gid, "Red Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Red) - missionCommands.addCommandForGroup(_gid, "White Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.White) + local _BlueSM = MENU_GROUP_COMMAND:New( group, "Blue Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Blue ) + local _GrSM = MENU_GROUP_COMMAND:New( group, "Green Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Green ) + local _OrSM = MENU_GROUP_COMMAND:New( group, "Orange Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Orange ) + local _ReSM = MENU_GROUP_COMMAND:New( group, "Red Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Red ) + local _WhSm = MENU_GROUP_COMMAND:New( group, "White Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.White ) -- F10/On the Range//My Settings/Flare Color/ - missionCommands.addCommandForGroup(_gid, "Green Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.Green) - missionCommands.addCommandForGroup(_gid, "Red Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.Red) - missionCommands.addCommandForGroup(_gid, "White Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.White) - missionCommands.addCommandForGroup(_gid, "Yellow Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.Yellow) + local _GrFl = MENU_GROUP_COMMAND:New( group, "Green Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.Green ) + local _ReFl = MENU_GROUP_COMMAND:New( group, "Red Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.Red ) + local _WhFl = MENU_GROUP_COMMAND:New( group, "White Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.White ) + local _YeFl = MENU_GROUP_COMMAND:New( group, "Yellow Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.Yellow ) -- F10/On the Range//My Settings/ - missionCommands.addCommandForGroup(_gid, "Smoke Delay On/Off", _settingsPath, self._SmokeBombDelayOnOff, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Smoke Impact On/Off", _settingsPath, self._SmokeBombImpactOnOff, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Flare Hits On/Off", _settingsPath, self._FlareDirectHitsOnOff, self, _unitName) - missionCommands.addCommandForGroup(_gid, "All Messages On/Off", _settingsPath, self._MessagesToPlayerOnOff, self, _unitName) + local _SmDe = MENU_GROUP_COMMAND:New( group, "Smoke Delay On/Off", _settingsPath, self._SmokeBombDelayOnOff, self, _unitName ) + local _SmIm = MENU_GROUP_COMMAND:New( group, "Smoke Impact On/Off", _settingsPath, self._SmokeBombImpactOnOff, self, _unitName ) + local _FlHi = MENU_GROUP_COMMAND:New( group, "Flare Hits On/Off", _settingsPath, self._FlareDirectHitsOnOff, self, _unitName ) + local _AlMeA = MENU_GROUP_COMMAND:New( group, "All Messages On/Off", _settingsPath, self._MessagesToPlayerOnOff, self, _unitName ) + local _TrpSh = MENU_GROUP_COMMAND:New( group, "Targetsheet On/Off", _settingsPath, self._TargetsheetOnOff, self, _unitName ) -- F10/On the Range//Range Information - missionCommands.addCommandForGroup(_gid, "General Info", _infoPath, self._DisplayRangeInfo, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Weather Report", _infoPath, self._DisplayRangeWeather, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Bombing Targets", _infoPath, self._DisplayBombTargets, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Strafe Pits", _infoPath, self._DisplayStrafePits, self, _unitName) + local _WeIn = MENU_GROUP_COMMAND:New( group, "General Info", _infoPath, self._DisplayRangeInfo, self, _unitName ) + local _WeRe = MENU_GROUP_COMMAND:New( group, "Weather Report", _infoPath, self._DisplayRangeWeather, self, _unitName ) + local _BoTgtgs = MENU_GROUP_COMMAND:New( group, "Bombing Targets", _infoPath, self._DisplayBombTargets, self, _unitName ) + local _StrPits = MENU_GROUP_COMMAND:New( group, "Strafe Pits", _infoPath, self._DisplayStrafePits, self, _unitName ):Refresh() end else - self:E(self.id.."Could not find group or group ID in AddF10Menu() function. Unit name: ".._unitName) + self:E( self.id .. "Could not find group or group ID in AddF10Menu() function. Unit name: " .. _unitName ) end else - self:E(self.id.."Player unit does not exist in AddF10Menu() function. Unit name: ".._unitName) + self:E( self.id .. "Player unit does not exist in AddF10Menu() function. Unit name: " .. _unitName ) end end @@ -73215,80 +78351,79 @@ end -- @param #RANGE self -- @param #RANGE.BombTarget target Bomb target data. -- @return Core.Point#COORDINATE Target coordinate. -function RANGE:_GetBombTargetCoordinate(target) +function RANGE:_GetBombTargetCoordinate( target ) - local coord=nil --Core.Point#COORDINATE + local coord = nil -- Core.Point#COORDINATE - if target.type==RANGE.TargetType.UNIT then + if target.type == RANGE.TargetType.UNIT then if not target.move then -- Target should not move. - coord=target.coordinate + coord = target.coordinate else -- Moving target. Check if alive and get current position if target.target and target.target:IsAlive() then - coord=target.target:GetCoordinate() + coord = target.target:GetCoordinate() end end - elseif target.type==RANGE.TargetType.STATIC then + elseif target.type == RANGE.TargetType.STATIC then -- Static targets dont move. - coord=target.coordinate + coord = target.coordinate - elseif target.type==RANGE.TargetType.COORD then + elseif target.type == RANGE.TargetType.COORD then -- Coordinates dont move. - coord=target.coordinate + coord = target.coordinate else - self:E(self.id.."ERROR: Unknown target type.") + self:E( self.id .. "ERROR: Unknown target type." ) end return coord end - --- Get the number of shells a unit currently has. -- @param #RANGE self -- @param #string unitname Name of the player unit. -- @return Number of shells left -function RANGE:_GetAmmo(unitname) - self:F2(unitname) +function RANGE:_GetAmmo( unitname ) + self:F2( unitname ) -- Init counter. - local ammo=0 + local ammo = 0 - local unit, playername = self:_GetPlayerUnitAndName(unitname) + local unit, playername = self:_GetPlayerUnitAndName( unitname ) if unit and playername then - local has_ammo=false + local has_ammo = false - local ammotable=unit:GetAmmo() - self:T2({ammotable=ammotable}) + local ammotable = unit:GetAmmo() + self:T2( { ammotable = ammotable } ) if ammotable ~= nil then - local weapons=#ammotable - self:T2(self.id..string.format("Number of weapons %d.", weapons)) + local weapons = #ammotable + self:T2( self.id .. string.format( "Number of weapons %d.", weapons ) ) - for w=1,weapons do + for w = 1, weapons do - local Nammo=ammotable[w]["count"] - local Tammo=ammotable[w]["desc"]["typeName"] + local Nammo = ammotable[w]["count"] + local Tammo = ammotable[w]["desc"]["typeName"] -- We are specifically looking for shells here. - if string.match(Tammo, "shell") then + if string.match( Tammo, "shell" ) then -- Add up all shells - ammo=ammo+Nammo + ammo = ammo + Nammo - local text=string.format("Player %s has %d rounds ammo of type %s", playername, Nammo, Tammo) - self:T(self.id..text) + local text = string.format( "Player %s has %d rounds ammo of type %s", playername, Nammo, Tammo ) + self:T( self.id .. text ) else - local text=string.format("Player %s has %d ammo of type %s", playername, Nammo, Tammo) - self:T(self.id..text) + local text = string.format( "Player %s has %d ammo of type %s", playername, Nammo, Tammo ) + self:T( self.id .. text ) end end end @@ -73300,46 +78435,46 @@ end --- Mark targets on F10 map. -- @param #RANGE self -- @param #string _unitName Name of the player unit. -function RANGE:_MarkTargetsOnMap(_unitName) - self:F(_unitName) +function RANGE:_MarkTargetsOnMap( _unitName ) + self:F( _unitName ) -- Get group. - local group=nil --Wrapper.Group#GROUP + local group = nil -- Wrapper.Group#GROUP if _unitName then - group=UNIT:FindByName(_unitName):GetGroup() + group = UNIT:FindByName( _unitName ):GetGroup() end -- Mark bomb targets. - for _,_bombtarget in pairs(self.bombingTargets) do - local bombtarget=_bombtarget --#RANGE.BombTarget - local coord=self:_GetBombTargetCoordinate(_bombtarget) + for _, _bombtarget in pairs( self.bombingTargets ) do + local bombtarget = _bombtarget -- #RANGE.BombTarget + local coord = self:_GetBombTargetCoordinate( _bombtarget ) if group then - coord:MarkToGroup(string.format("Bomb target %s:\n%s\n%s", bombtarget.name, coord:ToStringLLDMS(), coord:ToStringBULLS(group:GetCoalition())), group) + coord:MarkToGroup( string.format( "Bomb target %s:\n%s\n%s", bombtarget.name, coord:ToStringLLDMS(), coord:ToStringBULLS( group:GetCoalition() ) ), group ) else - coord:MarkToAll(string.format("Bomb target %s", bombtarget.name)) + coord:MarkToAll( string.format( "Bomb target %s", bombtarget.name ) ) end end -- Mark strafe targets. - for _,_strafepit in pairs(self.strafeTargets) do - for _,_target in pairs(_strafepit.targets) do - local _target=_target --Wrapper.Positionable#POSITIONABLE + for _, _strafepit in pairs( self.strafeTargets ) do + for _, _target in pairs( _strafepit.targets ) do + local _target = _target -- Wrapper.Positionable#POSITIONABLE if _target and _target:IsAlive() then - local coord=_target:GetCoordinate() --Core.Point#COORDINATE + local coord = _target:GetCoordinate() -- Core.Point#COORDINATE if group then - --coord:MarkToGroup("Strafe target ".._target:GetName(), group) - coord:MarkToGroup(string.format("Strafe target %s:\n%s\n%s", _target:GetName(), coord:ToStringLLDMS(), coord:ToStringBULLS(group:GetCoalition())), group) + -- coord:MarkToGroup("Strafe target ".._target:GetName(), group) + coord:MarkToGroup( string.format( "Strafe target %s:\n%s\n%s", _target:GetName(), coord:ToStringLLDMS(), coord:ToStringBULLS( group:GetCoalition() ) ), group ) else - coord:MarkToAll("Strafe target ".._target:GetName()) + coord:MarkToAll( "Strafe target " .. _target:GetName() ) end end end end if _unitName then - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) - local text=string.format("%s, %s, range targets are now marked on F10 map.", self.rangename, _playername) - self:_DisplayMessageToGroup(_unit, text, 5) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) + local text = string.format( "%s, %s, range targets are now marked on F10 map.", self.rangename, _playername ) + self:_DisplayMessageToGroup( _unit, text, 5 ) end end @@ -73347,67 +78482,67 @@ end --- Illuminate targets. Fires illumination bombs at one random bomb and one random strafe target at a random altitude between 400 and 800 m. -- @param #RANGE self -- @param #string _unitName (Optional) Name of the player unit. -function RANGE:_IlluminateBombTargets(_unitName) - self:F(_unitName) +function RANGE:_IlluminateBombTargets( _unitName ) + self:F( _unitName ) -- All bombing target coordinates. - local bomb={} + local bomb = {} - for _,_bombtarget in pairs(self.bombingTargets) do - local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE - local coord=self:_GetBombTargetCoordinate(_bombtarget) + for _, _bombtarget in pairs( self.bombingTargets ) do + local _target = _bombtarget.target -- Wrapper.Positionable#POSITIONABLE + local coord = self:_GetBombTargetCoordinate( _bombtarget ) if coord then - table.insert(bomb, coord) + table.insert( bomb, coord ) end end - if #bomb>0 then - local coord=bomb[math.random(#bomb)] --Core.Point#COORDINATE - local c=COORDINATE:New(coord.x,coord.y+math.random(self.illuminationminalt,self.illuminationmaxalt),coord.z) + if #bomb > 0 then + local coord = bomb[math.random( #bomb )] -- Core.Point#COORDINATE + local c = COORDINATE:New( coord.x, coord.y + math.random( self.illuminationminalt, self.illuminationmaxalt ), coord.z ) c:IlluminationBomb() end -- All strafe target coordinates. - local strafe={} + local strafe = {} - for _,_strafepit in pairs(self.strafeTargets) do - for _,_target in pairs(_strafepit.targets) do - local _target=_target --Wrapper.Positionable#POSITIONABLE + for _, _strafepit in pairs( self.strafeTargets ) do + for _, _target in pairs( _strafepit.targets ) do + local _target = _target -- Wrapper.Positionable#POSITIONABLE if _target and _target:IsAlive() then - local coord=_target:GetCoordinate() --Core.Point#COORDINATE - table.insert(strafe, coord) + local coord = _target:GetCoordinate() -- Core.Point#COORDINATE + table.insert( strafe, coord ) end end end -- Pick a random strafe target. - if #strafe>0 then - local coord=strafe[math.random(#strafe)] --Core.Point#COORDINATE - local c=COORDINATE:New(coord.x,coord.y+math.random(self.illuminationminalt,self.illuminationmaxalt),coord.z) + if #strafe > 0 then + local coord = strafe[math.random( #strafe )] -- Core.Point#COORDINATE + local c = COORDINATE:New( coord.x, coord.y + math.random( self.illuminationminalt, self.illuminationmaxalt ), coord.z ) c:IlluminationBomb() end if _unitName then - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) - local text=string.format("%s, %s, range targets are illuminated.", self.rangename, _playername) - self:_DisplayMessageToGroup(_unit, text, 5) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) + local text = string.format( "%s, %s, range targets are illuminated.", self.rangename, _playername ) + self:_DisplayMessageToGroup( _unit, text, 5 ) end end --- Reset player statistics. -- @param #RANGE self -- @param #string _unitName Name of the player unit. -function RANGE:_ResetRangeStats(_unitName) - self:F(_unitName) +function RANGE:_ResetRangeStats( _unitName ) + self:F( _unitName ) -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) if _unit and _playername then self.strafePlayerResults[_playername] = nil self.bombPlayerResults[_playername] = nil - local text=string.format("%s, %s, your range stats were cleared.", self.rangename, _playername) - self:DisplayMessageToGroup(_unit, text, 5, false, true) + local text = string.format( "%s, %s, your range stats were cleared.", self.rangename, _playername ) + self:DisplayMessageToGroup( _unit, text, 5, false, true ) end end @@ -73418,19 +78553,19 @@ end -- @param #number _time Duration how long the message is displayed. -- @param #boolean _clear Clear up old messages. -- @param #boolean display If true, display message regardless of player setting "Messages Off". -function RANGE:_DisplayMessageToGroup(_unit, _text, _time, _clear, display) - self:F({unit=_unit, text=_text, time=_time, clear=_clear}) +function RANGE:_DisplayMessageToGroup( _unit, _text, _time, _clear, display ) + self:F( { unit = _unit, text = _text, time = _time, clear = _clear } ) -- Defaults - _time=_time or self.Tmsg - if _clear==nil or _clear==false then - _clear=false + _time = _time or self.Tmsg + if _clear == nil or _clear == false then + _clear = false else - _clear=true + _clear = true end -- Messages globally disabled. - if self.messages==false then + if self.messages == false then return end @@ -73438,22 +78573,22 @@ function RANGE:_DisplayMessageToGroup(_unit, _text, _time, _clear, display) if _unit and _unit:IsAlive() then -- Group ID. - local _gid=_unit:GetGroup():GetID() + local _gid = _unit:GetGroup():GetID() -- Get playername and player settings - local _, playername=self:_GetPlayerUnitAndName(_unit:GetName()) - local playermessage=self.PlayerSettings[playername].messages + local _, playername = self:_GetPlayerUnitAndName( _unit:GetName() ) + local playermessage = self.PlayerSettings[playername].messages -- Send message to player if messages enabled and not only for the examiner. - if _gid and (playermessage==true or display) and (not self.examinerexclusive) then - trigger.action.outTextForGroup(_gid, _text, _time, _clear) + if _gid and (playermessage == true or display) and (not self.examinerexclusive) then + trigger.action.outTextForGroup( _gid, _text, _time, _clear ) end -- Send message to examiner. - if self.examinergroupname~=nil then - local _examinerid=GROUP:FindByName(self.examinergroupname):GetID() + if self.examinergroupname ~= nil then + local _examinerid = GROUP:FindByName( self.examinergroupname ):GetID() if _examinerid then - trigger.action.outTextForGroup(_examinerid, _text, _time, _clear) + trigger.action.outTextForGroup( _examinerid, _text, _time, _clear ) end end end @@ -73463,20 +78598,20 @@ end --- Toggle status of smoking bomb impact points. -- @param #RANGE self -- @param #string unitname Name of the player unit. -function RANGE:_SmokeBombImpactOnOff(unitname) - self:F(unitname) +function RANGE:_SmokeBombImpactOnOff( unitname ) + self:F( unitname ) - local unit, playername = self:_GetPlayerUnitAndName(unitname) + local unit, playername = self:_GetPlayerUnitAndName( unitname ) if unit and playername then local text - if self.PlayerSettings[playername].smokebombimpact==true then - self.PlayerSettings[playername].smokebombimpact=false - text=string.format("%s, %s, smoking impact points of bombs is now OFF.", self.rangename, playername) + if self.PlayerSettings[playername].smokebombimpact == true then + self.PlayerSettings[playername].smokebombimpact = false + text = string.format( "%s, %s, smoking impact points of bombs is now OFF.", self.rangename, playername ) else - self.PlayerSettigs[playername].smokebombimpact=true - text=string.format("%s, %s, smoking impact points of bombs is now ON.", self.rangename, playername) + self.PlayerSettigs[playername].smokebombimpact = true + text = string.format( "%s, %s, smoking impact points of bombs is now ON.", self.rangename, playername ) end - self:_DisplayMessageToGroup(unit, text, 5, false, true) + self:_DisplayMessageToGroup( unit, text, 5, false, true ) end end @@ -73484,20 +78619,20 @@ end --- Toggle status of time delay for smoking bomb impact points -- @param #RANGE self -- @param #string unitname Name of the player unit. -function RANGE:_SmokeBombDelayOnOff(unitname) - self:F(unitname) +function RANGE:_SmokeBombDelayOnOff( unitname ) + self:F( unitname ) - local unit, playername = self:_GetPlayerUnitAndName(unitname) + local unit, playername = self:_GetPlayerUnitAndName( unitname ) if unit and playername then local text - if self.PlayerSettings[playername].delaysmoke==true then - self.PlayerSettings[playername].delaysmoke=false - text=string.format("%s, %s, delayed smoke of bombs is now OFF.", self.rangename, playername) + if self.PlayerSettings[playername].delaysmoke == true then + self.PlayerSettings[playername].delaysmoke = false + text = string.format( "%s, %s, delayed smoke of bombs is now OFF.", self.rangename, playername ) else - self.PlayerSettigs[playername].delaysmoke=true - text=string.format("%s, %s, delayed smoke of bombs is now ON.", self.rangename, playername) + self.PlayerSettigs[playername].delaysmoke = true + text = string.format( "%s, %s, delayed smoke of bombs is now ON.", self.rangename, playername ) end - self:_DisplayMessageToGroup(unit, text, 5, false, true) + self:_DisplayMessageToGroup( unit, text, 5, false, true ) end end @@ -73505,19 +78640,62 @@ end --- Toggle display messages to player. -- @param #RANGE self -- @param #string unitname Name of the player unit. -function RANGE:_MessagesToPlayerOnOff(unitname) - self:F(unitname) +function RANGE:_MessagesToPlayerOnOff( unitname ) + self:F( unitname ) - local unit, playername = self:_GetPlayerUnitAndName(unitname) + local unit, playername = self:_GetPlayerUnitAndName( unitname ) if unit and playername then local text - if self.PlayerSettings[playername].messages==true then - text=string.format("%s, %s, display of ALL messages is now OFF.", self.rangename, playername) + if self.PlayerSettings[playername].messages == true then + text = string.format( "%s, %s, display of ALL messages is now OFF.", self.rangename, playername ) else - text=string.format("%s, %s, display of ALL messages is now ON.", self.rangename, playername) + text = string.format( "%s, %s, display of ALL messages is now ON.", self.rangename, playername ) + end + self:_DisplayMessageToGroup( unit, text, 5, false, true ) + self.PlayerSettings[playername].messages = not self.PlayerSettings[playername].messages + end + +end + +--- Targetsheet saves if player on or off. +-- @param #RANGE self +-- @param #string _unitname Name of the player unit. +function RANGE:_TargetsheetOnOff( _unitname ) + self:F2( _unitname ) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName( _unitname ) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData = self.PlayerSettings[playername] -- #RANGE.PlayerData + + if playerData then + + -- Check if option is enabled at all. + local text = "" + if self.targetsheet then + + -- Invert current setting. + playerData.targeton = not playerData.targeton + + -- Inform player. + if playerData and playerData.targeton == true then + text = string.format( "roger, your targetsheets are now SAVED." ) + else + text = string.format( "affirm, your targetsheets are NOT SAVED." ) + end + + else + text = "negative, target sheet data recorder is broken on this range." + end + + -- Message to player. + -- self:MessageToPlayer(playerData, text, nil, playerData.name, 5) + self:_DisplayMessageToGroup( unit, text, 5, false, false ) end - self:_DisplayMessageToGroup(unit, text, 5, false, true) - self.PlayerSettings[playername].messages=not self.PlayerSettings[playername].messages end end @@ -73525,20 +78703,20 @@ end --- Toggle status of flaring direct hits of range targets. -- @param #RANGE self -- @param #string unitname Name of the player unit. -function RANGE:_FlareDirectHitsOnOff(unitname) - self:F(unitname) +function RANGE:_FlareDirectHitsOnOff( unitname ) + self:F( unitname ) - local unit, playername = self:_GetPlayerUnitAndName(unitname) + local unit, playername = self:_GetPlayerUnitAndName( unitname ) if unit and playername then local text - if self.PlayerSettings[playername].flaredirecthits==true then - self.PlayerSettings[playername].flaredirecthits=false - text=string.format("%s, %s, flaring direct hits is now OFF.", self.rangename, playername) + if self.PlayerSettings[playername].flaredirecthits == true then + self.PlayerSettings[playername].flaredirecthits = false + text = string.format( "%s, %s, flaring direct hits is now OFF.", self.rangename, playername ) else - self.PlayerSettings[playername].flaredirecthits=true - text=string.format("%s, %s, flaring direct hits is now ON.", self.rangename, playername) + self.PlayerSettings[playername].flaredirecthits = true + text = string.format( "%s, %s, flaring direct hits is now ON.", self.rangename, playername ) end - self:_DisplayMessageToGroup(unit, text, 5, false, true) + self:_DisplayMessageToGroup( unit, text, 5, false, true ) end end @@ -73546,21 +78724,21 @@ end --- Mark bombing targets with smoke. -- @param #RANGE self -- @param #string unitname Name of the player unit. -function RANGE:_SmokeBombTargets(unitname) - self:F(unitname) +function RANGE:_SmokeBombTargets( unitname ) + self:F( unitname ) - for _,_bombtarget in pairs(self.bombingTargets) do - local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE - local coord=self:_GetBombTargetCoordinate(_bombtarget) + for _, _bombtarget in pairs( self.bombingTargets ) do + local _target = _bombtarget.target -- Wrapper.Positionable#POSITIONABLE + local coord = self:_GetBombTargetCoordinate( _bombtarget ) if coord then - coord:Smoke(self.BombSmokeColor) + coord:Smoke( self.BombSmokeColor ) end end if unitname then - local unit, playername = self:_GetPlayerUnitAndName(unitname) - local text=string.format("%s, %s, bombing targets are now marked with %s smoke.", self.rangename, playername, self:_smokecolor2text(self.BombSmokeColor)) - self:_DisplayMessageToGroup(unit, text, 5) + local unit, playername = self:_GetPlayerUnitAndName( unitname ) + local text = string.format( "%s, %s, bombing targets are now marked with %s smoke.", self.rangename, playername, self:_smokecolor2text( self.BombSmokeColor ) ) + self:_DisplayMessageToGroup( unit, text, 5 ) end end @@ -73568,17 +78746,17 @@ end --- Mark strafing targets with smoke. -- @param #RANGE self -- @param #string unitname Name of the player unit. -function RANGE:_SmokeStrafeTargets(unitname) - self:F(unitname) +function RANGE:_SmokeStrafeTargets( unitname ) + self:F( unitname ) - for _,_target in pairs(self.strafeTargets) do - _target.coordinate:Smoke(self.StrafeSmokeColor) + for _, _target in pairs( self.strafeTargets ) do + _target.coordinate:Smoke( self.StrafeSmokeColor ) end if unitname then - local unit, playername = self:_GetPlayerUnitAndName(unitname) - local text=string.format("%s, %s, strafing tragets are now marked with %s smoke.", self.rangename, playername, self:_smokecolor2text(self.StrafeSmokeColor)) - self:_DisplayMessageToGroup(unit, text, 5) + local unit, playername = self:_GetPlayerUnitAndName( unitname ) + local text = string.format( "%s, %s, strafing tragets are now marked with %s smoke.", self.rangename, playername, self:_smokecolor2text( self.StrafeSmokeColor ) ) + self:_DisplayMessageToGroup( unit, text, 5 ) end end @@ -73586,21 +78764,21 @@ end --- Mark approach boxes of strafe targets with smoke. -- @param #RANGE self -- @param #string unitname Name of the player unit. -function RANGE:_SmokeStrafeTargetBoxes(unitname) - self:F(unitname) +function RANGE:_SmokeStrafeTargetBoxes( unitname ) + self:F( unitname ) - for _,_target in pairs(self.strafeTargets) do - local zone=_target.polygon --Core.Zone#ZONE - zone:SmokeZone(self.StrafePitSmokeColor, 4) - for _,_point in pairs(_target.smokepoints) do - _point:SmokeOrange() --Corners are smoked orange. + for _, _target in pairs( self.strafeTargets ) do + local zone = _target.polygon -- Core.Zone#ZONE + zone:SmokeZone( self.StrafePitSmokeColor, 4 ) + for _, _point in pairs( _target.smokepoints ) do + _point:SmokeOrange() -- Corners are smoked orange. end end if unitname then - local unit, playername = self:_GetPlayerUnitAndName(unitname) - local text=string.format("%s, %s, strafing pit approach boxes are now marked with %s smoke.", self.rangename, playername, self:_smokecolor2text(self.StrafePitSmokeColor)) - self:_DisplayMessageToGroup(unit, text, 5) + local unit, playername = self:_GetPlayerUnitAndName( unitname ) + local text = string.format( "%s, %s, strafing pit approach boxes are now marked with %s smoke.", self.rangename, playername, self:_smokecolor2text( self.StrafePitSmokeColor ) ) + self:_DisplayMessageToGroup( unit, text, 5 ) end end @@ -73609,14 +78787,14 @@ end -- @param #RANGE self -- @param #string _unitName Name of the player unit. -- @param Utilities.Utils#SMOKECOLOR color ID of the smoke color. -function RANGE:_playersmokecolor(_unitName, color) - self:F({unitname=_unitName, color=color}) +function RANGE:_playersmokecolor( _unitName, color ) + self:F( { unitname = _unitName, color = color } ) - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) if _unit and _playername then - self.PlayerSettings[_playername].smokecolor=color - local text=string.format("%s, %s, your bomb impacts are now smoked in %s.", self.rangename, _playername, self:_smokecolor2text(color)) - self:_DisplayMessageToGroup(_unit, text, 5) + self.PlayerSettings[_playername].smokecolor = color + local text = string.format( "%s, %s, your bomb impacts are now smoked in %s.", self.rangename, _playername, self:_smokecolor2text( color ) ) + self:_DisplayMessageToGroup( _unit, text, 5 ) end end @@ -73625,14 +78803,14 @@ end -- @param #RANGE self -- @param #string _unitName Name of the player unit. -- @param Utilities.Utils#FLARECOLOR color ID of flare color. -function RANGE:_playerflarecolor(_unitName, color) - self:F({unitname=_unitName, color=color}) +function RANGE:_playerflarecolor( _unitName, color ) + self:F( { unitname = _unitName, color = color } ) - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) if _unit and _playername then - self.PlayerSettings[_playername].flarecolor=color - local text=string.format("%s, %s, your direct hits are now flared in %s.", self.rangename, _playername, self:_flarecolor2text(color)) - self:_DisplayMessageToGroup(_unit, text, 5) + self.PlayerSettings[_playername].flarecolor = color + local text = string.format( "%s, %s, your direct hits are now flared in %s.", self.rangename, _playername, self:_flarecolor2text( color ) ) + self:_DisplayMessageToGroup( _unit, text, 5 ) end end @@ -73641,22 +78819,22 @@ end -- @param #RANGE self -- @param Utilities.Utils#SMOKECOLOR color Color Id. -- @return #string Color text. -function RANGE:_smokecolor2text(color) - self:F(color) +function RANGE:_smokecolor2text( color ) + self:F( color ) - local txt="" - if color==SMOKECOLOR.Blue then - txt="blue" - elseif color==SMOKECOLOR.Green then - txt="green" - elseif color==SMOKECOLOR.Orange then - txt="orange" - elseif color==SMOKECOLOR.Red then - txt="red" - elseif color==SMOKECOLOR.White then - txt="white" + local txt = "" + if color == SMOKECOLOR.Blue then + txt = "blue" + elseif color == SMOKECOLOR.Green then + txt = "green" + elseif color == SMOKECOLOR.Orange then + txt = "orange" + elseif color == SMOKECOLOR.Red then + txt = "red" + elseif color == SMOKECOLOR.White then + txt = "white" else - txt=string.format("unknown color (%s)", tostring(color)) + txt = string.format( "unknown color (%s)", tostring( color ) ) end return txt @@ -73666,20 +78844,20 @@ end -- @param #RANGE self -- @param Utilities.Utils#FLARECOLOR color Color Id. -- @return #string Color text. -function RANGE:_flarecolor2text(color) - self:F(color) +function RANGE:_flarecolor2text( color ) + self:F( color ) - local txt="" - if color==FLARECOLOR.Green then - txt="green" - elseif color==FLARECOLOR.Red then - txt="red" - elseif color==FLARECOLOR.White then - txt="white" - elseif color==FLARECOLOR.Yellow then - txt="yellow" + local txt = "" + if color == FLARECOLOR.Green then + txt = "green" + elseif color == FLARECOLOR.Red then + txt = "red" + elseif color == FLARECOLOR.White then + txt = "white" + elseif color == FLARECOLOR.Yellow then + txt = "yellow" else - txt=string.format("unknown color (%s)", tostring(color)) + txt = string.format( "unknown color (%s)", tostring( color ) ) end return txt @@ -73689,33 +78867,33 @@ end -- @param #RANGE self -- @param #string name Name of the potential static object. -- @return #boolean Returns true if a static with this name exists. Retruns false if a unit with this name exists. Returns nil if neither unit or static exist. -function RANGE:_CheckStatic(name) - self:F2(name) +function RANGE:_CheckStatic( name ) + self:F2( name ) -- Get DCS static object. - local _DCSstatic=StaticObject.getByName(name) + local _DCSstatic = StaticObject.getByName( name ) if _DCSstatic and _DCSstatic:isExist() then - --Static does exist at least in DCS. Check if it also in the MOOSE DB. - local _MOOSEstatic=STATIC:FindByName(name, false) + -- Static does exist at least in DCS. Check if it also in the MOOSE DB. + local _MOOSEstatic = STATIC:FindByName( name, false ) -- If static is not yet in MOOSE DB, we add it. Can happen for cargo statics! if not _MOOSEstatic then - self:T(self.id..string.format("Adding DCS static to MOOSE database. Name = %s.", name)) - _DATABASE:AddStatic(name) + self:T( self.id .. string.format( "Adding DCS static to MOOSE database. Name = %s.", name ) ) + _DATABASE:AddStatic( name ) end return true else - self:T3(self.id..string.format("No static object with name %s exists.", name)) + self:T3( self.id .. string.format( "No static object with name %s exists.", name ) ) end -- Check if a unit has this name. - if UNIT:FindByName(name) then + if UNIT:FindByName( name ) then return false else - self:T3(self.id..string.format("No unit object with name %s exists.", name)) + self:T3( self.id .. string.format( "No unit object with name %s exists.", name ) ) end -- If not unit or static exist, we return nil. @@ -73726,17 +78904,17 @@ end -- @param #RANGE self -- @param Wrapper.Controllable#CONTROLLABLE controllable -- @return Maximum speed in km/h. -function RANGE:_GetSpeed(controllable) - self:F2(controllable) +function RANGE:_GetSpeed( controllable ) + self:F2( controllable ) -- Get DCS descriptors - local desc=controllable:GetDesc() + local desc = controllable:GetDesc() -- Get speed - local speed=0 + local speed = 0 if desc then - speed=desc.speedMax*3.6 - self:T({speed=speed}) + speed = desc.speedMax * 3.6 + self:T( { speed = speed } ) end return speed @@ -73748,20 +78926,20 @@ end -- @return Wrapper.Unit#UNIT Unit of player. -- @return #string Name of the player. -- @return nil If player does not exist. -function RANGE:_GetPlayerUnitAndName(_unitName) - self:F2(_unitName) +function RANGE:_GetPlayerUnitAndName( _unitName ) + self:F2( _unitName ) if _unitName ~= nil then -- Get DCS unit from its name. - local DCSunit=Unit.getByName(_unitName) + local DCSunit = Unit.getByName( _unitName ) if DCSunit then - local playername=DCSunit:getPlayerName() - local unit=UNIT:Find(DCSunit) + local playername = DCSunit:getPlayerName() + local unit = UNIT:Find( DCSunit ) - self:T2({DCSunit=DCSunit, unit=unit, playername=playername}) + self:T2( { DCSunit = DCSunit, unit = unit, playername = playername } ) if DCSunit and unit and playername then return unit, playername end @@ -73771,21 +78949,21 @@ function RANGE:_GetPlayerUnitAndName(_unitName) end -- Return nil if we could not find a player. - return nil,nil + return nil, nil end ---- Returns a string which consits of this callsign and the player name. +--- Returns a string which consists of the player name. -- @param #RANGE self -- @param #string unitname Name of the player unit. -function RANGE:_myname(unitname) - self:F2(unitname) +function RANGE:_myname( unitname ) + self:F2( unitname ) - local unit=UNIT:FindByName(unitname) - local pname=unit:GetPlayerName() - local csign=unit:GetCallsign() + local unit = UNIT:FindByName( unitname ) + local pname = unit:GetPlayerName() + -- local csign = unit:GetCallsign() - --return string.format("%s (%s)", csign, pname) - return string.format("%s", pname) + -- return string.format("%s (%s)", csign, pname) + return string.format( "%s", pname ) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -83255,7 +88433,7 @@ function PSEUDOATC:ReportWeather(GID, UID, position, location) local T=position:GetTemperature() -- Correct unit system. - local _T=string.format('%d°F', UTILS.CelciusToFarenheit(T)) + local _T=string.format('%d°F', UTILS.CelsiusToFahrenheit(T)) if settings:IsMetric() then _T=string.format('%d°C', T) end @@ -83462,11 +88640,14 @@ function PSEUDOATC:LocalAirports(GID, UID) for _,airbase in pairs(airports) do local name=airbase:getName() - local q=AIRBASE:FindByName(name):GetCoordinate() - local d=q:Get2DDistance(pos) + local a=AIRBASE:FindByName(name) + if a then + local q=a:GetCoordinate() + local d=q:Get2DDistance(pos) - -- Add to table. - table.insert(self.group[GID].player[UID].airports, {distance=d, name=name}) + -- Add to table. + table.insert(self.group[GID].player[UID].airports, {distance=d, name=name}) + end end end @@ -83554,9 +88735,6 @@ function PSEUDOATC:_myname(unitname) return string.format("%s (%s)", csign, pname) end - - - --- **Functional** - Simulation of logistic operations. -- -- === @@ -83639,12 +88817,14 @@ end -- @field #string autosavepath Path where the asset file is saved on auto save. -- @field #string autosavefile File name of the auto asset save file. Default is auto generated from warehouse id and name. -- @field #boolean safeparking If true, parking spots for aircraft are considered as occupied if e.g. a client aircraft is parked there. Default false. --- @field #boolean isunit If true, warehouse is represented by a unit instead of a static. +-- @field #boolean isUnit If `true`, warehouse is represented by a unit instead of a static. +-- @field #boolean isShip If `true`, warehouse is represented by a ship unit. -- @field #number lowfuelthresh Low fuel threshold. Triggers the event AssetLowFuel if for any unit fuel goes below this number. -- @field #boolean respawnafterdestroyed If true, warehouse is respawned after it was destroyed. Assets are kept. -- @field #number respawndelay Delay before respawn in seconds. -- @field #number runwaydestroyed Time stamp timer.getAbsTime() when the runway was destroyed. -- @field #number runwayrepairtime Time in seconds until runway will be repaired after it was destroyed. Default is 3600 sec (one hour). +-- @field Ops.FlightControl#FLIGHTCONTROL flightcontrol Flight control of this warehouse. -- @extends Core.Fsm#FSM --- Have your assets at the right place at the right time - or not! @@ -83906,6 +89086,7 @@ end -- * @{#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. @@ -85149,7 +90330,8 @@ WAREHOUSE = { autosavepath = nil, autosavefile = nil, saveparking = false, - isunit = false, + isUnit = false, + isShip = false, lowfuelthresh = 0.15, respawnafterdestroyed=false, respawndelay = nil, @@ -85158,6 +90340,8 @@ WAREHOUSE = { --- Item of the warehouse stock table. -- @type WAREHOUSE.Assetitem -- @field #number uid Unique id of the asset. +-- @field #number wid ID of the warehouse this asset belongs to. +-- @field #number rid Request ID of this asset (if any). -- @field #string templatename Name of the template group. -- @field #table template The spawn template of the group. -- @field DCS#Group.Category category Category of the group. @@ -85179,9 +90363,17 @@ WAREHOUSE = { -- @field #boolean spawned If true, asset was spawned into the cruel world. If false, it is still in stock. -- @field #string spawngroupname Name of the spawned group. -- @field #boolean iscargo If true, asset is cargo. If false asset is transport. Nil if in stock. --- @field #number rid The request ID of this asset. -- @field #boolean arrived If true, asset arrived at its destination. +-- -- @field #number damage Damage of asset group in percent. +-- @field Ops.AirWing#AIRWING.Payload payload The payload of the asset. +-- @field Ops.OpsGroup#OPSGROUP flightgroup The flightgroup object. +-- @field Ops.Cohort#COHORT cohort The cohort this asset belongs to. +-- @field Ops.Legion#LEGION legion The legion this asset belonts to. +-- @field #string squadname Name of the squadron this asset belongs to. +-- @field #number Treturned Time stamp when asset returned to its legion (airwing, brigade). +-- @field #boolean requested If `true`, asset was requested and cannot be selected by another request. +-- @field #boolean isReserved If `true`, asset was reserved and cannot be selected by another request. --- Item of the warehouse queue table. -- @type WAREHOUSE.Queueitem @@ -85204,6 +90396,7 @@ WAREHOUSE = { -- @field #table transportassets Table of transport carrier assets. Each element of the table is a @{#WAREHOUSE.Assetitem}. -- @field #number transportattribute Attribute of transport assets of type @{#WAREHOUSE.Attribute}. -- @field #number transportcategory Category of transport assets of type @{#WAREHOUSE.Category}. +-- @field #boolean lateActivation Assets are spawned in late activated state. --- Item of the warehouse pending queue table. -- @type WAREHOUSE.Pendingitem @@ -85249,6 +90442,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. @@ -85275,6 +90469,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", @@ -85401,40 +90596,50 @@ WAREHOUSE.version="1.0.2" --- The WAREHOUSE constructor. Creates a new WAREHOUSE object from a static object. Parameters like the coalition and country are taken from the static object structure. -- @param #WAREHOUSE self --- @param Wrapper.Static#STATIC warehouse The physical structure representing the warehouse. --- @param #string alias (Optional) Alias of the warehouse, i.e. the name it will be called when sending messages etc. Default is the name of the static +-- @param Wrapper.Static#STATIC warehouse The physical structure representing the warehouse. Can also be a @{Wrapper.Unit#UNIT}. +-- @param #string alias (Optional) Alias of the warehouse, i.e. the name it will be called when sending messages etc. Default is the name of the static/unit representing the warehouse. -- @return #WAREHOUSE self function WAREHOUSE:New(warehouse, alias) + -- Inherit everthing from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #WAREHOUSE + -- Check if just a string was given and convert to static. if type(warehouse)=="string" then - local warehousename=warehouse + local warehousename=warehouse warehouse=UNIT:FindByName(warehousename) if warehouse==nil then warehouse=STATIC:FindByName(warehousename, true) - self.isunit=false - else - self.isunit=true end end -- Nil check. if warehouse==nil then - BASE:E("ERROR: Warehouse does not exist!") + env.error("ERROR: Warehouse does not exist!") return nil end + + -- Check if we have a STATIC or UNIT object. + if warehouse:IsInstanceOf("STATIC") then + self.isUnit=false + elseif warehouse:IsInstanceOf("UNIT") then + self.isUnit=true + if warehouse:IsShip() then + self.isShip=true + end + else + env.error("ERROR: Warehouse is neither STATIC nor UNIT object!") + return nil + end -- Set alias. self.alias=alias or warehouse:GetName() - -- Print version. - env.info(string.format("Adding warehouse v%s for structure %s with alias %s", WAREHOUSE.version, warehouse:GetName(), self.alias)) - - -- Inherit everthing from FSM class. - local self=BASE:Inherit(self, FSM:New()) -- #WAREHOUSE - -- Set some string id for output to DCS.log file. self.lid=string.format("WAREHOUSE %s | ", self.alias) + + -- Print version. + self:I(self.lid..string.format("Adding warehouse v%s for structure %s [isUnit=%s, isShip=%s]", WAREHOUSE.version, warehouse:GetName(), tostring(self:IsUnit()), tostring(self:IsShip()))) -- Set some variables. self.warehouse=warehouse @@ -85458,14 +90663,20 @@ function WAREHOUSE:New(warehouse, alias) end -- Define warehouse and default spawn zone. - self.zone=ZONE_RADIUS:New(string.format("Warehouse zone %s", self.warehouse:GetName()), warehouse:GetVec2(), 500) - self.spawnzone=ZONE_RADIUS:New(string.format("Warehouse %s spawn zone", self.warehouse:GetName()), warehouse:GetVec2(), 250) + if self.isShip then + self.zone=ZONE_AIRBASE:New(self.warehouse:GetName(), 1000) + self.spawnzone=ZONE_AIRBASE:New(self.warehouse:GetName(), 1000) + else + self.zone=ZONE_RADIUS:New(string.format("Warehouse zone %s", self.warehouse:GetName()), warehouse:GetVec2(), 500) + self.spawnzone=ZONE_RADIUS:New(string.format("Warehouse %s spawn zone", self.warehouse:GetName()), warehouse:GetVec2(), 250) + end + -- Defaults self:SetMarker(true) self:SetReportOff() self:SetRunwayRepairtime() - --self:SetVerbosityLevel(0) + self.allowSpawnOnClientSpots=false -- Add warehouse to database. _WAREHOUSEDB.Warehouses[self.uid]=self @@ -86111,6 +91322,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%). @@ -86149,6 +91368,12 @@ function WAREHOUSE:SetSpawnZone(zone, maxdist) return self end +--- Get the spawn zone. +-- @param #WAREHOUSE self +-- @return Core.Zone#ZONE The spawn zone. +function WAREHOUSE:GetSpawnZone() + return self.spawnzone +end --- Set a warehouse zone. If this zone is captured, the warehouse and all its assets fall into the hands of the enemy. -- @param #WAREHOUSE self @@ -86190,9 +91415,8 @@ end --- Check parking ID. -- @param #WAREHOUSE self -- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot Parking spot. --- @param Wrapper.Airbase#AIRBASE airbase The airbase. -- @return #boolean If true, parking is valid. -function WAREHOUSE:_CheckParkingValid(spot, airbase) +function WAREHOUSE:_CheckParkingValid(spot) if self.parkingIDs==nil then return true @@ -86207,6 +91431,25 @@ function WAREHOUSE:_CheckParkingValid(spot, airbase) return false end +--- Check parking ID for an asset. +-- @param #WAREHOUSE self +-- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot Parking spot. +-- @return #boolean If true, parking is valid. +function WAREHOUSE:_CheckParkingAsset(spot, asset) + + if asset.parkingIDs==nil then + return true + end + + for _,id in pairs(asset.parkingIDs or {}) do + if spot.TerminalID==id then + return true + end + end + + return false +end + --- Enable auto save of warehouse assets at mission end event. -- @param #WAREHOUSE self @@ -86647,14 +91890,16 @@ end -- @param #WAREHOUSE self -- @return DCS#Vec3 The 3D vector of the warehouse. function WAREHOUSE:GetVec3() - return self.warehouse:GetVec3() + local vec3=self.warehouse:GetVec3() + return vec3 end --- Get 2D vector of warehouse static. -- @param #WAREHOUSE self -- @return DCS#Vec2 The 2D vector of the warehouse. function WAREHOUSE:GetVec2() - return self.warehouse:GetVec2() + local vec2=self.warehouse:GetVec2() + return vec2 end @@ -86723,18 +91968,6 @@ function WAREHOUSE:GetAssignment(request) return tostring(request.assignment) end ---[[ ---- Get warehouse unique ID from static warehouse object. This is the ID under which you find the @{#WAREHOUSE} object in the global data base. --- @param #WAREHOUSE self --- @param #string staticname Name of the warehouse static object. --- @return #number Warehouse unique ID. -function WAREHOUSE:GetWarehouseID(staticname) - local warehouse=STATIC:FindByName(staticname, true) - local uid=tonumber(warehouse:GetID()) - return uid -end -]] - --- Find a warehouse in the global warehouse data base. -- @param #WAREHOUSE self -- @param #number uid The unique ID of the warehouse. @@ -86850,7 +92083,7 @@ end --- Check if runway is operational. -- @param #WAREHOUSE self --- @return #boolean If true, runway is operational. +-- @return #boolean If `true`, runway is operational. function WAREHOUSE:IsRunwayOperational() if self.airbase then if self.runwaydestroyed then @@ -86886,6 +92119,27 @@ function WAREHOUSE:GetRunwayRepairtime() return 0 end +--- Check if warehouse physical representation is a unit (not a static) object. +-- @param #WAREHOUSE self +-- @return #boolean If `true`, warehouse object is a unit. +function WAREHOUSE:IsUnit() + return self.isUnit +end + +--- Check if warehouse physical representation is a static (not a unit) object. +-- @param #WAREHOUSE self +-- @return #boolean If `true`, warehouse object is a static. +function WAREHOUSE:IsStatic() + return not self.isUnit +end + +--- Check if warehouse physical representation is a ship. +-- @param #WAREHOUSE self +-- @return #boolean If `true`, warehouse object is a ship. +function WAREHOUSE:IsShip() + return self.isShip +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- FSM states ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -87096,7 +92350,7 @@ function WAREHOUSE:onafterStatus(From, Event, To) self:_PrintQueue(self.pending, "Queue pending") -- Check fuel for all assets. - self:_CheckFuel() + --self:_CheckFuel() -- Update warhouse marker on F10 map. self:_UpdateWarehouseMarkText() @@ -87480,6 +92734,8 @@ function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribu -- Asset is not spawned. asset.spawned=false + asset.requested=false + asset.isReserved=false asset.iscargo=nil asset.arrived=nil @@ -87530,9 +92786,21 @@ function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribu -- Destroy group if it is alive. if group:IsAlive()==true then self:_DebugMessage(string.format("Removing group %s", group:GetName()), 5) - -- Setting parameter to false, i.e. creating NO dead or remove unit event, seems to not confuse the dispatcher logic. - -- TODO: It would be nice, however, to have the remove event. - group:Destroy() --(false) + + local opsgroup=_DATABASE:GetOpsGroup(group:GetName()) + if opsgroup then + opsgroup:Despawn(0, true) + opsgroup:__Stop(-0.01) + else + -- Setting parameter to false, i.e. creating NO dead or remove unit event, seems to not confuse the dispatcher logic. + -- TODO: It would be nice, however, to have the remove event. + group:Destroy() --(false) + end + else + local opsgroup=_DATABASE:GetOpsGroup(group:GetName()) + if opsgroup then + opsgroup:Stop() + end end else @@ -87588,6 +92856,7 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, local cargobay={} local cargobaytot=0 local cargobaymax=0 + local weights={} for _i,_unit in pairs(group:GetUnits()) do local unit=_unit --Wrapper.Unit#UNIT local Desc=unit:GetDesc() @@ -87596,8 +92865,9 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, local unitweight=forceweight or Desc.massEmpty if unitweight then weight=weight+unitweight + weights[_i]=unitweight end - + local cargomax=0 local massfuel=Desc.fuelMassMax or 0 local massempty=Desc.massEmpty or 0 @@ -87646,6 +92916,7 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, asset.speedmax=SpeedMax asset.size=smax asset.weight=weight + asset.weights=weights asset.DCSdesc=Descriptors asset.attribute=attribute asset.cargobay=cargobay @@ -87658,6 +92929,8 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, asset.skill=skill asset.assignment=assignment asset.spawned=false + asset.requested=false + asset.isReserved=false asset.life0=group:GetLife0() asset.damage=0 asset.spawngroupname=string.format("%s_AID-%d", templategroupname, asset.uid) @@ -87696,7 +92969,7 @@ function WAREHOUSE:_AssetItemInfo(asset) text=text..string.format("Cargo bay max = %5.2f kg\n", asset.cargobaymax) text=text..string.format("Load radius = %s m\n", tostring(asset.loadradius)) text=text..string.format("Skill = %s\n", tostring(asset.skill)) - text=text..string.format("Livery = %s", tostring(asset.livery)) + text=text..string.format("Livery = %s", tostring(asset.livery)) self:I(self.lid..text) self:T({DCSdesc=asset.DCSdesc}) self:T3({Template=asset.template}) @@ -87867,9 +93140,16 @@ function WAREHOUSE:onafterAddRequest(From, Event, To, warehouse, AssetDescriptor -- Add request to queue. table.insert(self.queue, request) + + local descval="assetlist" + if request.assetdesc==WAREHOUSE.Descriptor.ASSETLIST then + + else + descval=tostring(request.assetdescval) + end - local text=string.format("Warehouse %s: New request from warehouse %s.\nDescriptor %s=%s, #assets=%s; Transport=%s, #transports =%s.", - self.alias, warehouse.alias, request.assetdesc, tostring(request.assetdescval), tostring(request.nasset), request.transporttype, tostring(request.ntransport)) + local text=string.format("Warehouse %s: New request from warehouse %s.\nDescriptor %s=%s, #assets=%s; Transport=%s, #transports=%s.", + self.alias, warehouse.alias, request.assetdesc, descval, tostring(request.nasset), request.transporttype, tostring(request.ntransport)) self:_DebugMessage(text, 5) end @@ -87909,7 +93189,7 @@ function WAREHOUSE:onbeforeRequest(From, Event, To, Request) -- Delete request from queue because it will never be possible. -- Unless(!) at least one is a moving warehouse, which could, e.g., be an aircraft carrier. - if not (self.isunit or Request.warehouse.isunit) then + if not (self.isUnit or Request.warehouse.isUnit) then self:_DeleteQueueItem(Request, self.queue) end @@ -88026,6 +93306,11 @@ function WAREHOUSE:onafterRequest(From, Event, To, Request) return end + -- Trigger event. + if spawngroup then + self:__AssetSpawned(0.01, spawngroup, _assetitem, Request) + end + end -- Init problem table. @@ -88442,6 +93727,13 @@ function WAREHOUSE:onbeforeArrived(From, Event, To, group) local asset=self:FindAssetInDB(group) if asset then + + if asset.flightgroup and not asset.arrived then + --env.info("FF asset has a flightgroup. arrival will be handled there!") + asset.arrived=true + return false + end + if asset.arrived==true then -- Asset already arrived (e.g. if multiple units trigger the event via landing). return false @@ -88449,6 +93741,7 @@ function WAREHOUSE:onbeforeArrived(From, Event, To, group) asset.arrived=true --ensure this is not called again from the same asset group. return true end + end end @@ -88820,7 +94113,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. @@ -88847,24 +94139,6 @@ function WAREHOUSE:onafterRunwayRepaired(From, Event, To) end ---- On before "AssetSpawned" event. Checks whether the asset was already set to "spawned" for groups with multiple units. --- @param #WAREHOUSE self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. --- @param Wrapper.Group#GROUP group The group spawned. --- @param #WAREHOUSE.Assetitem asset The asset that is dead. --- @param #WAREHOUSE.Pendingitem request The request of the dead asset. -function WAREHOUSE:onbeforeAssetSpawned(From, Event, To, group, asset, request) - if asset.spawned then - --return false - else - --return true - end - - return true -end - --- On after "AssetSpawned" event triggered when an asset group is spawned into the cruel world. -- @param #WAREHOUSE self -- @param #string From From state. @@ -88879,6 +94153,24 @@ function WAREHOUSE:onafterAssetSpawned(From, Event, To, group, asset, request) -- Sete asset state to spawned. asset.spawned=true + + -- Set spawn group name. + asset.spawngroupname=group:GetName() + + -- Remove asset from stock. + self:_DeleteStockItem(asset) + + -- Add group. + if asset.iscargo==true then + request.cargogroupset=request.cargogroupset or SET_GROUP:New() + request.cargogroupset:AddGroup(group) + else + request.transportgroupset=request.transportgroupset or SET_GROUP:New() + request.transportgroupset:AddGroup(group) + end + + -- Set warehouse state. + group:SetState(group, "WAREHOUSE", self) -- Check if all assets groups are spawned and trigger events. local n=0 @@ -88915,9 +94207,65 @@ end -- @param #WAREHOUSE.Assetitem asset The asset that is dead. -- @param #WAREHOUSE.Pendingitem request The request of the dead asset. function WAREHOUSE:onafterAssetDead(From, Event, To, asset, request) - local text=string.format("Asset %s from request id=%d is dead!", asset.templatename, request.uid) - self:T(self.lid..text) - self:_DebugMessage(text) + + if asset and request then + + -- Debug message. + local text=string.format("Asset %s from request id=%d is dead!", asset.templatename, request.uid) + self:T(self.lid..text) + + -- Here I need to get rid of the #CARGO at the end to obtain the original name again! + local groupname=asset.spawngroupname --self:_GetNameWithOut(group) + + -- Dont trigger a Remove event for the group sets. + local NoTriggerEvent=true + + if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then + + --- + -- Easy case: Group can simply be removed from the cargogroupset. + --- + + -- Remove dead group from cargo group set. + request.cargogroupset:Remove(groupname, NoTriggerEvent) + self:T(self.lid..string.format("Removed selfpropelled cargo %s: ncargo=%d.", groupname, request.cargogroupset:Count())) + + else + + --- + -- Complicated case: Dead unit could be: + -- 1.) A Cargo unit (e.g. waiting to be picked up). + -- 2.) A Transport unit which itself holds cargo groups. + --- + + -- Check if this a cargo or transport group. + local istransport=not asset.iscargo --self:_GroupIsTransport(group, request) + + if istransport==true then + + -- Whole carrier group is dead. Remove it from the carrier group set. + request.transportgroupset:Remove(groupname, NoTriggerEvent) + self:T(self.lid..string.format("Removed transport %s: ntransport=%d", groupname, request.transportgroupset:Count())) + + elseif istransport==false then + + -- This must have been an alive cargo group that was killed outside the carrier, e.g. waiting to be transported or waiting to be put back. + -- Remove dead group from cargo group set. + request.cargogroupset:Remove(groupname, NoTriggerEvent) + self:T(self.lid..string.format("Removed transported cargo %s outside carrier: ncargo=%d", groupname, request.cargogroupset:Count())) + -- This as well? + --request.transportcargoset:RemoveCargosByName(RemoveCargoNames) + + else + --self:E(self.lid..string.format("ERROR: Group %s is neither cargo nor transport!", group:GetName())) + end + end + + else + self:E(self.lid.."ERROR: Asset and/or Request is nil in onafterAssetDead") + + end + end @@ -89229,15 +94577,15 @@ function WAREHOUSE:_SpawnAssetRequest(Request) if asset.category==Group.Category.GROUND then -- Spawn ground troops. - _group=self:_SpawnAssetGroundNaval(_alias, asset, Request, self.spawnzone) + _group=self:_SpawnAssetGroundNaval(_alias, asset, Request, self.spawnzone, Request.lateActivation) elseif asset.category==Group.Category.AIRPLANE or asset.category==Group.Category.HELICOPTER then -- Spawn air units. if Parking[asset.uid] then - _group=self:_SpawnAssetAircraft(_alias, asset, Request, Parking[asset.uid], UnControlled) + _group=self:_SpawnAssetAircraft(_alias, asset, Request, Parking[asset.uid], UnControlled, Request.lateActivation) else - _group=self:_SpawnAssetAircraft(_alias, asset, Request, nil, UnControlled) + _group=self:_SpawnAssetAircraft(_alias, asset, Request, nil, UnControlled, Request.lateActivation) end elseif asset.category==Group.Category.TRAIN then @@ -89247,7 +94595,7 @@ function WAREHOUSE:_SpawnAssetRequest(Request) --TODO: Rail should only get one asset because they would spawn on top! -- Spawn naval assets. - _group=self:_SpawnAssetGroundNaval(_alias, asset, Request, self.spawnzone) + _group=self:_SpawnAssetGroundNaval(_alias, asset, Request, self.spawnzone, Request.lateActivation) end --self:E(self.lid.."ERROR: Spawning of TRAIN assets not possible yet!") @@ -89255,11 +94603,16 @@ function WAREHOUSE:_SpawnAssetRequest(Request) elseif asset.category==Group.Category.SHIP then -- Spawn naval assets. - _group=self:_SpawnAssetGroundNaval(_alias, asset, Request, self.portzone) + _group=self:_SpawnAssetGroundNaval(_alias, asset, Request, self.portzone, Request.lateActivation) else self:E(self.lid.."ERROR: Unknown asset category!") end + + -- Trigger event. + if _group then + self:__AssetSpawned(0.01, _group, asset, Request) + end end @@ -89272,9 +94625,9 @@ end -- @param #WAREHOUSE.Assetitem asset Ground asset that will be spawned. -- @param #WAREHOUSE.Queueitem request Request belonging to this asset. Needed for the name/alias. -- @param Core.Zone#ZONE spawnzone Zone where the assets should be spawned. --- @param #boolean aioff If true, AI of ground units are set to off. +-- @param #boolean lateactivated If true, groups are spawned late activated. -- @return Wrapper.Group#GROUP The spawned group or nil if the group could not be spawned. -function WAREHOUSE:_SpawnAssetGroundNaval(alias, asset, request, spawnzone, aioff) +function WAREHOUSE:_SpawnAssetGroundNaval(alias, asset, request, spawnzone, lateactivated) if asset and (asset.category==Group.Category.GROUND or asset.category==Group.Category.SHIP or asset.category==Group.Category.TRAIN) then @@ -89317,6 +94670,9 @@ function WAREHOUSE:_SpawnAssetGroundNaval(alias, asset, request, spawnzone, aiof end end + + -- Late activation. + template.lateActivation=lateactivated template.route.points[1].x = coord.x template.route.points[1].y = coord.z @@ -89328,14 +94684,6 @@ function WAREHOUSE:_SpawnAssetGroundNaval(alias, asset, request, spawnzone, aiof -- Spawn group. local group=_DATABASE:Spawn(template) --Wrapper.Group#GROUP - -- Activate group. Should only be necessary for late activated groups. - --group:Activate() - - -- Switch AI off if desired. This works only for ground and naval groups. - if aioff then - group:SetAIOff() - end - return group end @@ -89349,8 +94697,9 @@ end -- @param #WAREHOUSE.Queueitem request Request belonging to this asset. Needed for the name/alias. -- @param #table parking Parking data for this asset. -- @param #boolean uncontrolled Spawn aircraft in uncontrolled state. +-- @param #boolean lateactivated If true, groups are spawned late activated. -- @return Wrapper.Group#GROUP The spawned group or nil if the group could not be spawned. -function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrolled) +function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrolled, lateactivated) if asset and asset.category==Group.Category.AIRPLANE or asset.category==Group.Category.HELICOPTER then @@ -89367,6 +94716,14 @@ function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrol _action=COORDINATE.WaypointAction.FromParkingAreaHot uncontrolled=false end + + local airstart=asset.takeoffType and asset.takeoffType==COORDINATE.WaypointType.TurningPoint or false + + if airstart then + _type=COORDINATE.WaypointType.TurningPoint + _action=COORDINATE.WaypointAction.TurningPoint + uncontrolled=false + end -- Set route points. @@ -89374,7 +94731,15 @@ function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrol -- Get flight path if the group goes to another warehouse by itself. if request.toself then - local wp=self.airbase:GetCoordinate():WaypointAir("RADIO", _type, _action, 0, false, self.airbase, {}, "Parking") + + local coord=self.airbase:GetCoordinate() + + if airstart then + coord:SetAltitude(math.random(1000, 2000)) + end + + -- Single waypoint. + local wp=coord:WaypointAir("RADIO", _type, _action, 0, false, self.airbase, {}, "Parking") template.route.points={wp} else template.route.points=self:_GetFlightplan(asset, self.airbase, request.warehouse.airbase) @@ -89398,7 +94763,7 @@ function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrol else - if #parking<#template.units then + if #parking<#template.units and not airstart then local text=string.format("ERROR: Not enough parking! Free parking = %d < %d aircraft to be spawned.", #parking, #template.units) self:_DebugMessage(text) return nil @@ -89420,14 +94785,25 @@ function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrol unit.x=coord.x unit.y=coord.z unit.alt=coord.y + + if airstart then + unit.alt=math.random(1000, 2000) + end unit.parking_id = nil unit.parking = nil else - local coord=parking[i].Coordinate --Core.Point#COORDINATE - local terminal=parking[i].TerminalID --#number + local coord=nil --Core.Point#COORDINATE + local terminal=nil --#number + + if airstart then + coord=self.airbase:GetCoordinate():SetAltitude(math.random(1000, 2000)) + else + coord=parking[i].Coordinate + terminal=parking[i].TerminalID + end if self.Debug then coord:MarkToAll(string.format("Spawnplace unit %s terminal %d.", unit.name, terminal)) @@ -89602,18 +94978,10 @@ function WAREHOUSE:_RouteGround(group, request) end for n,wp in ipairs(Waypoints) do - env.info(n) local tf=self:_SimpleTaskFunctionWP("warehouse:_PassingWaypoint",group, n, #Waypoints) group:SetTaskWaypoint(wp, tf) end - -- Task function triggering the arrived event at the last waypoint. - --local TaskFunction = self:_SimpleTaskFunction("warehouse:_Arrived", group) - - -- Put task function on last waypoint. - --local Waypoint = Waypoints[#Waypoints] - --group:SetTaskWaypoint(Waypoint, TaskFunction) - -- Route group to destination. group:Route(Waypoints, 1) @@ -89691,9 +95059,12 @@ 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. - 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. self:T2(self.lid..string.format("RouteAir aircraft group %s alive=%s (after start command)", aircraft:GetName(), tostring(aircraft:IsAlive()))) @@ -89840,41 +95211,12 @@ function WAREHOUSE:_OnEventBirth(EventData) local request=self:GetRequestByID(rid) if asset and request then - + -- Debug message. self:T(self.lid..string.format("Warehouse %s captured event birth of request ID=%d, asset ID=%d, unit %s spawned=%s", self.alias, request.uid, asset.uid, EventData.IniUnitName, tostring(asset.spawned))) -- Set born to true. request.born=true - - -- Birth is triggered for each unit. We need to make sure not to call this too often! - if not asset.spawned then - - -- Remove asset from stock. - self:_DeleteStockItem(asset) - - -- Set spawned switch. - asset.spawned=true - asset.spawngroupname=group:GetName() - - -- Add group. - if asset.iscargo==true then - request.cargogroupset=request.cargogroupset or SET_GROUP:New() - request.cargogroupset:AddGroup(group) - else - request.transportgroupset=request.transportgroupset or SET_GROUP:New() - request.transportgroupset:AddGroup(group) - end - - -- Set warehouse state. - group:SetState(group, "WAREHOUSE", self) - - -- Asset spawned FSM function. - --self:__AssetSpawned(1, group, asset, request) - --env.info(string.format("FF asset spawned %s, %s", asset.spawngroupname, EventData.IniUnitName)) - self:AssetSpawned(group, asset, request) - - end else self:E(self.lid..string.format("ERROR: Either asset AID=%s or request RID=%s are nil in event birth of unit %s", tostring(aid), tostring(rid), tostring(EventData.IniUnitName))) @@ -90056,7 +95398,8 @@ function WAREHOUSE:_OnEventCrashOrDead(EventData) end end - --self:I(self.lid..string.format("Warehouse %s captured event dead or crash or unit %s.", self.alias, tostring(EventData.IniUnitName))) + -- Debug info. + self:T2(self.lid..string.format("Warehouse %s captured event dead or crash or unit %s", self.alias, tostring(EventData.IniUnitName))) -- Check if an asset unit was destroyed. if EventData.IniGroup then @@ -90071,7 +95414,7 @@ function WAREHOUSE:_OnEventCrashOrDead(EventData) if wid==self.uid then -- Debug message. - self:T(self.lid..string.format("Warehouse %s captured event dead or crash of its asset unit %s.", self.alias, EventData.IniUnitName)) + self:T(self.lid..string.format("Warehouse %s captured event dead or crash of its asset unit %s", self.alias, EventData.IniUnitName)) -- Loop over all pending requests and get the one belonging to this unit. for _,request in pairs(self.pending) do @@ -90081,7 +95424,7 @@ function WAREHOUSE:_OnEventCrashOrDead(EventData) if request.uid==rid then -- Update cargo and transport group sets of this request. We need to know if this job is finished. - self:_UnitDead(EventData.IniUnit, request) + self:_UnitDead(EventData.IniUnit, EventData.IniGroup, request) end end @@ -90094,38 +95437,46 @@ end -- This is important in order to determine if a job is done and can be removed from the (pending) queue. -- @param #WAREHOUSE self -- @param Wrapper.Unit#UNIT deadunit Unit that died. +-- @param Wrapper.Group#GROUP deadgroup Group of unit that died. -- @param #WAREHOUSE.Pendingitem request Request that needs to be updated. -function WAREHOUSE:_UnitDead(deadunit, request) +function WAREHOUSE:_UnitDead(deadunit, deadgroup, request) + self:F(self.lid.."FF unit dead "..deadunit:GetName()) - -- Flare unit. - if self.Debug then - deadunit:FlareRed() + -- Find opsgroup. + local opsgroup=_DATABASE:FindOpsGroup(deadgroup) + + -- Check if we have an opsgroup. + if opsgroup then + -- Handled in OPSGROUP:onafterDead() now. + return nil end - -- Group the dead unit belongs to. - local group=deadunit:GetGroup() - -- Number of alive units in group. - local nalive=group:CountAliveUnits() + local nalive=deadgroup:CountAliveUnits() -- Whole group is dead? - local groupdead=true + local groupdead=false if nalive>0 then groupdead=false + else + groupdead=true end + + -- Find asset. + local asset=self:FindAssetInDB(deadgroup) -- Here I need to get rid of the #CARGO at the end to obtain the original name again! local unitname=self:_GetNameWithOut(deadunit) - local groupname=self:_GetNameWithOut(group) + local groupname=self:_GetNameWithOut(deadgroup) -- Group is dead! if groupdead then - self:T(self.lid..string.format("Group %s (transport=%s) is dead!", groupname, tostring(self:_GroupIsTransport(group,request)))) + -- Debug output. + self:T(self.lid..string.format("Group %s (transport=%s) is dead!", groupname, tostring(self:_GroupIsTransport(deadgroup,request)))) if self.Debug then - group:SmokeWhite() + deadgroup:SmokeWhite() end - -- Trigger AssetDead event. - local asset=self:FindAssetInDB(group) + -- Trigger AssetDead event. self:AssetDead(asset, request) end @@ -90133,19 +95484,7 @@ function WAREHOUSE:_UnitDead(deadunit, request) -- Dont trigger a Remove event for the group sets. local NoTriggerEvent=true - if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then - - --- - -- Easy case: Group can simply be removed from the cargogroupset. - --- - - -- Remove dead group from cargo group set. - if groupdead==true then - request.cargogroupset:Remove(groupname, NoTriggerEvent) - self:T(self.lid..string.format("Removed selfpropelled cargo %s: ncargo=%d.", groupname, request.cargogroupset:Count())) - end - - else + if not request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then --- -- Complicated case: Dead unit could be: @@ -90153,10 +95492,7 @@ function WAREHOUSE:_UnitDead(deadunit, request) -- 2.) A Transport unit which itself holds cargo groups. --- - -- Check if this a cargo or transport group. - local istransport=self:_GroupIsTransport(group,request) - - if istransport==true then + if not asset.iscargo then -- Get the carrier unit table holding the cargo groups inside this carrier. local cargogroupnames=request.carriercargo[unitname] @@ -90171,25 +95507,8 @@ function WAREHOUSE:_UnitDead(deadunit, request) end - -- Whole carrier group is dead. Remove it from the carrier group set. - if groupdead then - request.transportgroupset:Remove(groupname, NoTriggerEvent) - self:T(self.lid..string.format("Removed transport %s: ntransport=%d", groupname, request.transportgroupset:Count())) - end - - elseif istransport==false then - - -- This must have been an alive cargo group that was killed outside the carrier, e.g. waiting to be transported or waiting to be put back. - -- Remove dead group from cargo group set. - if groupdead==true then - request.cargogroupset:Remove(groupname, NoTriggerEvent) - self:T(self.lid..string.format("Removed transported cargo %s outside carrier: ncargo=%d", groupname, request.cargogroupset:Count())) - -- This as well? - --request.transportcargoset:RemoveCargosByName(RemoveCargoNames) - end - else - self:E(self.lid..string.format("ERROR: Group %s is neither cargo nor transport!", group:GetName())) + self:E(self.lid..string.format("ERROR: Group %s is neither cargo nor transport!", deadgroup:GetName())) end end @@ -90575,10 +95894,9 @@ function WAREHOUSE:_CheckRequestValid(request) -- Check that both spawn zones are not in water. local inwater=self.spawnzone:GetCoordinate():IsSurfaceTypeWater() or request.warehouse.spawnzone:GetCoordinate():IsSurfaceTypeWater() - if inwater then + if inwater and not request.lateActivation then self:E("ERROR: Incorrect request. Ground asset requested but at least one spawn zone is in water!") - --valid=false - valid=false + return false end -- No ground assets directly to or from ships. @@ -91200,7 +96518,7 @@ function WAREHOUSE:_SimpleTaskFunction(Function, group) local DCSScript = {} DCSScript[#DCSScript+1] = string.format('local mygroup = GROUP:FindByName(\"%s\") ', groupname) -- The group that executes the task function. Very handy with the "...". - if self.isunit then + if self.isUnit then DCSScript[#DCSScript+1] = string.format("local mywarehouse = UNIT:FindByName(\"%s\") ", warehouse) -- The unit that holds the warehouse self object. else DCSScript[#DCSScript+1] = string.format("local mywarehouse = STATIC:FindByName(\"%s\") ", warehouse) -- The static that holds the warehouse self object. @@ -91231,7 +96549,7 @@ function WAREHOUSE:_SimpleTaskFunctionWP(Function, group, n, N) local DCSScript = {} DCSScript[#DCSScript+1] = string.format('local mygroup = GROUP:FindByName(\"%s\") ', groupname) -- The group that executes the task function. Very handy with the "...". - if self.isunit then + if self.isUnit then DCSScript[#DCSScript+1] = string.format("local mywarehouse = UNIT:FindByName(\"%s\") ", warehouse) -- The unit that holds the warehouse self object. else DCSScript[#DCSScript+1] = string.format("local mywarehouse = STATIC:FindByName(\"%s\") ", warehouse) -- The static that holds the warehouse self object. @@ -91305,14 +96623,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 @@ -91351,7 +96671,9 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) local _coord=unit:GetVec3() local _size=self:_GetObjectSize(unit:GetDCSObject()) local _name=unit:GetName() - table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="unit"}) + if unit and unit:IsAlive() then + table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="unit"}) + end end -- Check all statics. @@ -91393,6 +96715,9 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) -- Loop over all units - each one needs a spot. for i=1,_asset.nunits do + + -- Asset name + local assetname=_asset.spawngroupname.."-"..tostring(i) -- Loop over all parking spots. local gotit=false @@ -91400,7 +96725,7 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) local parkingspot=_parkingspot --Wrapper.Airbase#AIRBASE.ParkingSpot -- Check correct terminal type for asset. We don't want helos in shelters etc. - if AIRBASE._CheckTerminalType(parkingspot.TerminalType, terminaltype) and self:_CheckParkingValid(parkingspot, airbase) and airbase:_CheckParkingLists(parkingspot.TerminalID) then + if AIRBASE._CheckTerminalType(parkingspot.TerminalType, terminaltype) and self:_CheckParkingValid(parkingspot) and self:_CheckParkingAsset(parkingspot, asset) and airbase:_CheckParkingLists(parkingspot.TerminalID) then -- Coordinate of the parking spot. local _spot=parkingspot.Coordinate -- Core.Point#COORDINATE @@ -91417,13 +96742,13 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) -- Spot is blocked. if not safe then - --env.info(string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is NOT SAFE", _asset.templatename, _asset.uid, _termid, dist)) + self:T3(self.lid..string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is NOT SAFE", assetname, _asset.uid, _termid, dist)) free=false problem=obstacle problem.dist=dist break else - --env.info(string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is SAFE", _asset.templatename, _asset.uid, _termid, dist)) + --env.info(string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is SAFE", assetname, _asset.uid, _termid, dist)) end end @@ -91435,32 +96760,36 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) table.insert(parking[_asset.uid], parkingspot) -- Debug - self:T(self.lid..string.format("Parking spot %d is free for asset id=%d!", _termid, _asset.uid)) + self:T(self.lid..string.format("Parking spot %d is free for asset %s [id=%d]!", _termid, assetname, _asset.uid)) -- Add the unit as obstacle so that this spot will not be available for the next unit. - table.insert(obstacles, {coord=_spot, size=_asset.size, name=_asset.templatename, type="asset"}) + table.insert(obstacles, {coord=_spot, size=_asset.size, name=assetname, type="asset"}) gotit=true break else - -- Debug output for occupied spots. - self:T(self.lid..string.format("Parking spot %d is occupied or not big enough!", _termid)) + -- Debug output for occupied spots. if self.Debug then local coord=problem.coord --Core.Point#COORDINATE - local text=string.format("Obstacle blocking spot #%d is %s type %s with size=%.1f m and distance=%.1f m.", _termid, problem.name, problem.type, problem.size, problem.dist) + local text=string.format("Obstacle %s [type=%s] blocking spot=%d! Size=%.1f m and distance=%.1f m.", problem.name, problem.type, _termid, problem.size, problem.dist) + self:I(self.lid..text) coord:MarkToAll(string.format(text)) + else + self:T(self.lid..string.format("Parking spot %d is occupied or not big enough!", _termid)) end end + else + self:T2(self.lid..string.format("Terminal ID=%d: type=%s not supported", parkingspot.TerminalID, parkingspot.TerminalType)) end -- check terminal type end -- loop over parking spots -- No parking spot for at least one asset :( if not gotit then - self:I(self.lid..string.format("WARNING: No free parking spot for asset id=%d",_asset.uid)) + self:I(self.lid..string.format("WARNING: No free parking spot for asset %s [id=%d]", assetname, _asset.uid)) return nil end end -- loop over asset units @@ -91557,57 +96886,12 @@ end -- @return #number Request ID. function WAREHOUSE:_GetIDsFromGroup(group) - ---@param #string text The text to analyse. - local function analyse(text) - - -- Get rid of #0001 tail from spawn. - local unspawned=UTILS.Split(text, "#")[1] - - -- Split keywords. - local keywords=UTILS.Split(unspawned, "_") - local _wid=nil -- warehouse UID - local _aid=nil -- asset UID - local _rid=nil -- request UID - - -- Loop over keys. - for _,keys in pairs(keywords) do - local str=UTILS.Split(keys, "-") - local key=str[1] - local val=str[2] - if key:find("WID") then - _wid=tonumber(val) - elseif key:find("AID") then - _aid=tonumber(val) - elseif key:find("RID") then - _rid=tonumber(val) - end - end - - return _wid,_aid,_rid - end - if group then - + -- Group name - local name=group:GetName() - - -- Get asset id from group name. - local wid,aid,rid=analyse(name) - - -- Get Asset. - local asset=self:GetAssetByID(aid) - - -- Get warehouse and request id from asset table. - if asset then - wid=asset.wid - rid=asset.rid - end - - -- Debug info - self:T(self.lid..string.format("Group Name = %s", tostring(name))) - self:T(self.lid..string.format("Warehouse ID = %s", tostring(wid))) - self:T(self.lid..string.format("Asset ID = %s", tostring(aid))) - self:T(self.lid..string.format("Request ID = %s", tostring(rid))) + local groupname=group:GetName() + + local wid, aid, rid=self:_GetIDsFromGroupName(groupname) return wid,aid,rid else @@ -91616,14 +96900,13 @@ function WAREHOUSE:_GetIDsFromGroup(group) end - --- Get warehouse id, asset id and request id from group name (alias). -- @param #WAREHOUSE self --- @param Wrapper.Group#GROUP group The group from which the info is gathered. +-- @param #string groupname Name of the group from which the info is gathered. -- @return #number Warehouse ID. -- @return #number Asset ID. -- @return #number Request ID. -function WAREHOUSE:_GetIDsFromGroupOLD(group) +function WAREHOUSE:_GetIDsFromGroupName(groupname) ---@param #string text The text to analyse. local function analyse(text) @@ -91654,25 +96937,26 @@ function WAREHOUSE:_GetIDsFromGroupOLD(group) return _wid,_aid,_rid end - if group then - -- Group name - local name=group:GetName() + -- Get asset id from group name. + local wid,aid,rid=analyse(groupname) - -- Get ids - local wid,aid,rid=analyse(name) + -- Get Asset. + local asset=self:GetAssetByID(aid) - -- Debug info - self:T3(self.lid..string.format("Group Name = %s", tostring(name))) - self:T3(self.lid..string.format("Warehouse ID = %s", tostring(wid))) - self:T3(self.lid..string.format("Asset ID = %s", tostring(aid))) - self:T3(self.lid..string.format("Request ID = %s", tostring(rid))) - - return wid,aid,rid - else - self:E("WARNING: Group not found in GetIDsFromGroup() function!") + -- Get warehouse and request id from asset table. + if asset then + wid=asset.wid + rid=asset.rid end + -- Debug info + self:T3(self.lid..string.format("Group Name = %s", tostring(groupname))) + self:T3(self.lid..string.format("Warehouse ID = %s", tostring(wid))) + self:T3(self.lid..string.format("Asset ID = %s", tostring(aid))) + self:T3(self.lid..string.format("Request ID = %s", tostring(rid))) + + return wid,aid,rid end --- Filter stock assets by descriptor and attribute. @@ -91818,9 +97102,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") @@ -91857,6 +97142,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 @@ -92299,7 +97586,7 @@ end -- @param #number duration Message display duration in seconds. Default 20 sec. If duration is zero, no message is displayed. function WAREHOUSE:_DebugMessage(text, duration) duration=duration or 20 - if duration>0 then + if self.Debug and duration>0 then MESSAGE:New(text, duration):ToAllIf(self.Debug) end self:T(self.lid..text) @@ -92606,11 +97893,11 @@ function WAREHOUSE:_GetFlightplan(asset, departure, destination) -- Hot start. if asset.takeoffType and asset.takeoffType==COORDINATE.WaypointType.TakeOffParkingHot then - env.info("FF hot") + --env.info("FF hot") _type=COORDINATE.WaypointType.TakeOffParkingHot _action=COORDINATE.WaypointAction.FromParkingAreaHot else - env.info("FF cold") + --env.info("FF cold") end @@ -92683,7 +97970,6 @@ end -- @module Functional.FOX -- @image Functional_FOX.png - --- FOX class. -- @type FOX -- @field #string ClassName Name of the class. @@ -92707,8 +97993,7 @@ end -- @field #number dt10 Time step [sec] for missile position updates if distance to target > 10 km and < 50 km. Default 1 sec. -- @field #number dt05 Time step [sec] for missile position updates if distance to target > 5 km and < 10 km. Default 0.5 sec. -- @field #number dt01 Time step [sec] for missile position updates if distance to target > 1 km and < 5 km. Default 0.1 sec. --- @field #number dt00 Time step [sec] for missile position updates if distance to target < 1 km. Default 0.01 sec. --- @field #boolean +-- @field #number dt00 Time step [sec] for missile position updates if distance to target < 1 km. Default 0.01 sec. -- @extends Core.Fsm#FSM --- Fox 3! @@ -93454,7 +98739,7 @@ function FOX:onafterMissileLaunch(From, Event, To, missile) local text=string.format("Missile launch detected! Distance %.1f NM, bearing %03d°.", UTILS.MetersToNM(distance), bearing) -- Say notching headings. - BASE:ScheduleOnce(5, FOX._SayNotchingHeadings, self, player, missile.weapon) + self:ScheduleOnce(5, FOX._SayNotchingHeadings, self, player, missile.weapon) --TODO: ALERT or INFO depending on whether this is a direct target. --TODO: lauchalertall option. @@ -93776,6 +99061,13 @@ end -- Event Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- FOX event handler for event birth. +-- @param #FOX self +-- @param Core.Event#EVENTDATA EventData +function FOX:OnEventPlayerEnterAircraft(EventData) + +end + --- FOX event handler for event birth. -- @param #FOX self -- @param Core.Event#EVENTDATA EventData @@ -93817,7 +99109,7 @@ function FOX:OnEventBirth(EventData) -- Add F10 radio menu for player. if not self.menudisabled then - SCHEDULER:New(nil, self._AddF10Commands, {self,_unitName}, 0.1) + self:ScheduleOnce(0.1, FOX._AddF10Commands, self, _unitname) end -- Player data. @@ -94473,15 +99765,14 @@ end --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- **Functional** -- Modular, Automatic and Network capable Targeting and Interception System for Air Defenses +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ **Functional** -- Modular, Automatic and Network capable Targeting and Interception System for Air Defenses -- -- === -- -- **MANTIS** - Moose derived Modular, Automatic and Network capable Targeting and Interception System --- Controls a network of SAM sites. Use detection to switch on the AA site closest to the enemy --- Leverage evasiveness from SEAD --- Leverage attack range setup added by DCS in 11/20 +-- Controls a network of SAM sites. Uses detection to switch on the AA site closest to the enemy. +-- Automatic mode (default since 0.8) can set-up your SAM site network automatically for you. +-- Leverage evasiveness from SEAD, leverage attack range setting. -- -- === -- @@ -94495,8 +99786,8 @@ end -- -- @module Functional.Mantis -- @image Functional.Mantis.jpg - --- Date: July 2021 +-- +-- Date: Dec 2021 ------------------------------------------------------------------------- --- **MANTIS** class, extends Core.Base#BASE @@ -94541,26 +99832,77 @@ end -- -- #MANTIS -- Moose derived Modular, Automatic and Network capable Targeting and Interception System. --- Controls a network of SAM sites. Use detection to switch on the AA site closest to the enemy. --- Leverage evasiveness from @{Functional.Sead#SEAD}. --- Leverage attack range setup added by DCS in 11/20. +-- * Controls a network of SAM sites. Uses detection to switch on the SAM site closest to the enemy. +-- * **Automatic mode** (default since 0.8) can set-up your SAM site network automatically for you +-- * **Classic mode** behaves like before +-- * Leverage evasiveness from SEAD, leverage attack range setting +-- * Automatic setup of SHORAD based on groups of the class "short-range" -- --- Set up your SAM sites in the mission editor. Name the groups with common prefix like "Red SAM". --- Set up your EWR system in the mission editor. Name the groups with common prefix like "Red EWR". Can be e.g. AWACS or a combination of AWACS and Search Radars like e.g. EWR 1L13 etc. +-- # 0. Base considerations and naming conventions +-- +-- **Before** you start to set up your SAM sites in the mission editor, please think of naming conventions. This is especially critical to make +-- eveything work as intended, also if you have both a blue and a red installation! +-- +-- You need three **non-overlapping** "name spaces" for everything to work properly: +-- +-- * SAM sites, e.g. each **group name** begins with "Red SAM" +-- * EWR network and AWACS, e.g. each **group name** begins with "Red EWR" and *not* e.g. "Red SAM EWR" (overlap with "Red SAM"), "Red EWR Awacs" will be found by "Red EWR" +-- * SHORAD, e.g. each **group name** begins with "Red SHORAD" and *not" e.g. just "SHORAD" because you might also have "Blue SHORAD" +-- +-- It's important to get this right because of the nature of the filter-system in @{Core.Set#SET_GROUP}. Filters are "greedy", that is they +-- will match *any* string that contains the search string - hence we need to avoid that SAMs, EWR and SHORAD step on each other\'s toes. +-- +-- Second, for auto-mode to work, the SAMs need the **SAM Type Name** in their group name, as MANTIS will determine their capabilities from this. +-- This is case-sensitive, so "sa-11" is not equal to "SA-11" is not equal to "Sa-11"! +-- +-- Known SAM types at the time of writing are: +-- +-- * Avenger +-- * Chaparrel +-- * Hawk +-- * Linebacker +-- * NASAMS +-- * Patriot +-- * Rapier +-- * Roland +-- * Silkworm (though strictly speaking this is a surface to ship missile) +-- * SA-2, SA-3, SA-5, SA-6, SA-7, SA-8, SA-9, SA-10, SA-11, SA-13, SA-15, SA-19 +-- * and from HDS (see note below): SA-2, SA-3, SA-10B, SA-10C, SA-12, SA-17, SA-20A, SA-20B, SA-23, HQ-2 +-- +-- Following the example started above, an SA-6 site group name should start with "Red SAM SA-6" then, or a blue Patriot installation with e.g. "Blue SAM Patriot". +-- **NOTE** If you are using the High-Digit-Sam Mod, please note that the **group name** for the following SAM types also needs to contain the keyword "HDS": +-- +-- * SA-2 (with V759 missile, e.g. "Red SAM SA-2 HDS") +-- * SA-2 (with HQ-2 launcher, use HQ-2 in the group name, e.g. "Red SAM HQ-2" ) +-- * SA-3 (with V601P missile, e.g. "Red SAM SA-3 HDS") +-- * SA-10B (overlap with other SA-10 types, e.g. "Red SAM SA-10B HDS") +-- * SA-10C (overlap with other SA-10 types, e.g. "Red SAM SA-10C HDS") +-- * SA-12 (launcher dependent range, e.g. "Red SAM SA-12 HDS") +-- * SA-23 (launcher dependent range, e.g. "Red SAM SA-23 HDS") +-- +-- The other HDS types work like the rest of the known SAM systems. +-- +-- # 0.1 Set-up in the mission editor +-- +-- Set up your SAM sites in the mission editor. Name the groups using a systematic approach like above. +-- Set up your EWR system in the mission editor. Name the groups using a systematic approach like above. Can be e.g. AWACS or a combination of AWACS and Search Radars like e.g. EWR 1L13 etc. +-- Search Radars usually have "SR" or "STR" in their names. Use the encyclopedia in the mission editor to inform yourself. +-- Set up your SHORAD systems. They need to be **close** to (i.e. around) the SAM sites to be effective. Use **one** group per SAM location. SA-15 TOR systems offer a good missile defense. +-- -- [optional] Set up your HQ. Can be any group, e.g. a command vehicle. --- +-- -- # 1. Basic tactical considerations when setting up your SAM sites -- -- ## 1.1 Radar systems and AWACS -- -- Typically, your setup should consist of EWR (early warning) radars to detect and track targets, accompanied by AWACS if your scenario forsees that. Ensure that your EWR radars have a good coverage of the area you want to track. --- **Location** is of highest importantance here. Whilst AWACS in DCS has almost the "all seeing eye", EWR don't have that. Choose your location wisely, against a mountain backdrop or inside a valley even the best EWR system +-- **Location** is of highest importance here. Whilst AWACS in DCS has almost the "all seeing eye", EWR don't have that. Choose your location wisely, against a mountain backdrop or inside a valley even the best EWR system -- doesn't work well. Prefer higher-up locations with a good view; use F7 in-game to check where you actually placed your EWR and have a look around. Apart from the obvious choice, do also consider other radar units -- for this role, most have "SR" (search radar) or "STR" (search and track radar) in their names, use the encyclopedia to see what they actually do. -- -- ## 1.2 SAM sites -- --- Typically your SAM should cover all attack ranges. The closer the enemy gets, the more systems you will need to deploy to defend your location. Use a combination of long-range systems like the SA-10/11, midrange like SA-6 and short-range like +-- Typically your SAM should cover all attack ranges. The closer the enemy gets, the more systems you will need to deploy to defend your location. Use a combination of long-range systems like the SA-5/10/11, midrange like SA-6 and short-range like -- SA-2 for defense (Patriot, Hawk, Gepard, Blindfire for the blue side). For close-up defense and defense against HARMs or low-flying aircraft, helicopters it is also advisable to deploy SA-15 TOR systems, Shilka, Strela and Tunguska units, as well as manpads (Think Gepard, Avenger, Chaparral, -- Linebacker, Roland systems for the blue side). If possible, overlap ranges for mutual coverage. -- @@ -94570,42 +99912,72 @@ end -- -- * bad placement of radar units, -- * overestimation how far units can "see" and --- * not taking into account that a SAM site will take (e.g for a SA-6) 30-40 seconds between switching to RED, acquiring the target and firing. +-- * not taking into account that a SAM site will take (e.g for a SA-6) 30-40 seconds between switching on, acquiring the target and firing. -- -- An attacker doing 350knots will cover ca 180meters/second or thus more than 6km until the SA-6 fires. Use triggers zones and the ruler in the missione editor to understand distances and zones. Take into account that the ranges given by the circles -- in the mission editor are absolute maximum ranges; in-game this is rather 50-75% of that depending on the system. Fiddle with placement and options to see what works best for your scenario, and remember **everything in here is in meters**. -- -- # 2. Start up your MANTIS with a basic setting -- --- `myredmantis = MANTIS:New("myredmantis","Red SAM","Red EWR",nil,"red",false)` --- `myredmantis:Start()` +-- myredmantis = MANTIS:New("myredmantis","Red SAM","Red EWR",nil,"red",false) +-- myredmantis:Start() -- --- [optional] Use +-- Use -- --- * `MANTIS:SetEWRGrouping(radius)` --- * `MANTIS:SetEWRRange(radius)` --- * `MANTIS:SetSAMRadius(radius)` --- * `MANTIS:SetDetectInterval(interval)` --- * `MANTIS:SetAutoRelocate(hq, ewr)` +-- * MANTIS:SetEWRGrouping(radius) [classic mode] +-- * MANTIS:SetSAMRadius(radius) [classic mode] +-- * MANTIS:SetDetectInterval(interval) [classic & auto modes] +-- * MANTIS:SetAutoRelocate(hq, ewr) [classic & auto modes] -- -- before starting #MANTIS to fine-tune your setup. -- --- If you want to use a separate AWACS unit (default detection range: 250km) to support your EWR system, use e.g. the following setup: +-- If you want to use a separate AWACS unit to support your EWR system, use e.g. the following setup: -- --- `mybluemantis = MANTIS:New("bluemantis","Blue SAM","Blue EWR",nil,"blue",false,"Blue Awacs")` --- `mybluemantis:Start()` --- --- # 3. Default settings +-- mybluemantis = MANTIS:New("bluemantis","Blue SAM","Blue EWR",nil,"blue",false,"Blue Awacs") +-- mybluemantis:Start() +-- +-- ## 2.1 Auto mode features +-- +-- ### 2.1.1 You can now add Accept-, Reject- and Conflict-Zones to your setup, e.g. to consider borders or de-militarized zones: +-- +-- -- Parameters are tables of Core.Zone#ZONE objects! +-- -- This is effectively a 3-stage filter allowing for zone overlap. A coordinate is accepted first when +-- -- it is inside any AcceptZone. Then RejectZones are checked, which enforces both borders, but also overlaps of +-- -- Accept- and RejectZones. Last, if it is inside a conflict zone, it is accepted. +-- `mybluemantis:AddZones(AcceptZones,RejectZones,ConflictZones)` +-- +-- +-- ### 2.1.2 Change the number of long-, mid- and short-range systems going live on a detected target: +-- +-- -- parameters are numbers. Defaults are 1,2,2,6 respectively +-- `mybluemantis:SetMaxActiveSAMs(Short,Mid,Long,Classic)` +-- +-- ### 2.1.3 SHORAD will automatically be added from SAM sites of type "short-range" +-- +-- ### 2.1.4 Advanced features +-- +-- -- switch off auto mode **before** you start MANTIS. +-- `mybluemantis.automode = false` +-- +-- -- switch off auto shorad **before** you start MANTIS. +-- `mybluemantis.autoshorad = false` +-- +-- -- scale of the activation range, i.e. don't activate at the fringes of max range, defaults below. +-- -- also see engagerange below. +-- ` self.radiusscale[MANTIS.SamType.LONG] = 1.1` +-- ` self.radiusscale[MANTIS.SamType.MEDIUM] = 1.2` +-- ` self.radiusscale[MANTIS.SamType.SHORT] = 1.3` +-- +-- # 3. Default settings [both modes unless stated otherwise] -- -- By default, the following settings are active: -- -- * SAM_Templates_Prefix = "Red SAM" - SAM site group names in the mission editor begin with "Red SAM" -- * EWR_Templates_Prefix = "Red EWR" - EWR group names in the mission editor begin with "Red EWR" - can also be combined with an AWACS unit --- * checkradius = 25000 (meters) - SAMs will engage enemy flights, if they are within a 25km around each SAM site - `MANTIS:SetSAMRadius(radius)` +-- * [classic mode] checkradius = 25000 (meters) - SAMs will engage enemy flights, if they are within a 25km around each SAM site - `MANTIS:SetSAMRadius(radius)` -- * grouping = 5000 (meters) - Detection (EWR) will group enemy flights to areas of 5km for tracking - `MANTIS:SetEWRGrouping(radius)` --- * acceptrange = 80000 (meters) - Detection (EWR) will on consider flights inside a 80km radius - `MANTIS:SetEWRRange(radius)` -- * detectinterval = 30 (seconds) - MANTIS will decide every 30 seconds which SAM to activate - `MANTIS:SetDetectInterval(interval)` --- * engagerange = 85 (percent) - SAMs will only fire if flights are inside of a 85% radius of their max firerange - `MANTIS:SetSAMRange(range)` +-- * engagerange = 95 (percent) - SAMs will only fire if flights are inside of a 95% radius of their max firerange - `MANTIS:SetSAMRange(range)` -- * dynamic = false - Group filtering is set to once, i.e. newly added groups will not be part of the setup by default - `MANTIS:New(name,samprefix,ewrprefix,hq,coaltion,dynamic)` -- * autorelocate = false - HQ and (mobile) EWR system will not relocate in random intervals between 30mins and 1 hour - `MANTIS:SetAutoRelocate(hq, ewr)` -- * debug = false - Debugging reports on screen are set to off - `MANTIS:Debug(onoff)` @@ -94614,26 +99986,51 @@ end -- -- Advanced mode will *decrease* reactivity of MANTIS, if HQ and/or EWR network dies. Awacs is counted as one EWR unit. It will set SAMs to RED state if both are dead. Requires usage of an **HQ** object and the **dynamic** option. -- --- E.g. `mymantis:SetAdvancedMode( true, 90 )` +-- E.g. mymantis:SetAdvancedMode( true, 90 ) -- -- Use this option if you want to make use of or allow advanced SEAD tactics. -- --- # 5. Integrate SHORAD +-- # 5. Integrate SHORAD [classic mode] -- -- You can also choose to integrate Mantis with @{Functional.Shorad#SHORAD} for protection against HARMs and AGMs. When SHORAD detects a missile fired at one of MANTIS' SAM sites, it will activate SHORAD systems in -- the given defense checkradius around that SAM site. Create a SHORAD object first, then integrate with MANTIS like so: -- --- `local SamSet = SET_GROUP:New():FilterPrefixes("Blue SAM"):FilterCoalitions("blue"):FilterStart()` --- `myshorad = SHORAD:New("BlueShorad", "Blue SHORAD", SamSet, 22000, 600, "blue")` --- `-- now set up MANTIS` --- `mymantis = MANTIS:New("BlueMantis","Blue SAM","Blue EWR",nil,"blue",false,"Blue Awacs")` --- `mymantis:AddShorad(myshorad,720)` --- `mymantis:Start()` +-- local SamSet = SET_GROUP:New():FilterPrefixes("Blue SAM"):FilterCoalitions("blue"):FilterStart() +-- myshorad = SHORAD:New("BlueShorad", "Blue SHORAD", SamSet, 22000, 600, "blue") +-- -- now set up MANTIS +-- mymantis = MANTIS:New("BlueMantis","Blue SAM","Blue EWR",nil,"blue",false,"Blue Awacs") +-- mymantis:AddShorad(myshorad,720) +-- mymantis:Start() +-- +-- If you systematically name your SHORAD groups starting with "Blue SHORAD" you'll need exactly **one** SHORAD instance to manage all SHORAD groups. +-- +-- (Optionally) you can remove the link later on with -- --- and (optionally) remove the link later on with --- --- `mymantis:RemoveShorad()` +-- mymantis:RemoveShorad() -- +-- # 6. Integrated SEAD +-- +-- MANTIS is using @{Functional.Sead#SEAD} internally to both detect and evade HARM attacks. No extra efforts needed to set this up! +-- Once a HARM attack is detected, MANTIS (via SEAD) will shut down the radars of the attacked SAM site and take evasive action by moving the SAM +-- vehicles around (*if they are __drivable__*, that is). There's a component of randomness in detection and evasion, which is based on the +-- skill set of the SAM set (the higher the skill, the more likely). When a missile is fired from far away, the SAM will stay active for a +-- period of time to stay defensive, before it takes evasive actions. +-- +-- You can link into the SEAD driven events of MANTIS like so: +-- +-- function mymantis:OnAfterSeadSuppressionPlanned(From, Event, To, Group, Name, SuppressionStartTime, SuppressionEndTime) +-- -- your code here - SAM site shutdown and evasion planned, but not yet executed +-- -- Time entries relate to timer.getTime() - see https://wiki.hoggitworld.com/view/DCS_func_getTime +-- end +-- +-- function mymantis:OnAfterSeadSuppressionStart(From, Event, To, Group, Name) +-- -- your code here - SAM site is emissions off and possibly moving +-- end +-- +-- function mymantis:OnAfterSeadSuppressionEnd(From, Event, To, Group, Name) +-- -- your code here - SAM site is back online +-- end +-- -- @field #MANTIS MANTIS = { ClassName = "MANTIS", @@ -94643,9 +100040,12 @@ MANTIS = { EWR_Templates_Prefix = "", EWR_Group = nil, Adv_EWR_Group = nil, - HQ_Template_CC = "", - HQ_CC = nil, + HQ_Template_CC = "", + HQ_CC = nil, SAM_Table = {}, + SAM_Table_Long = {}, + SAM_Table_Medium = {}, + SAM_Table_Short = {}, lid = "", Detection = nil, AWACS_Detection = nil, @@ -94654,7 +100054,7 @@ MANTIS = { grouping = 5000, acceptrange = 80000, detectinterval = 30, - engagerange = 75, + engagerange = 95, autorelocate = false, advanced = false, adv_ratio = 100, @@ -94666,7 +100066,7 @@ MANTIS = { Shorad = nil, ShoradLink = false, ShoradTime = 600, - ShoradActDistance = 15000, + ShoradActDistance = 25000, UseEmOnOff = false, TimeStamp = 0, state2flag = false, @@ -94674,6 +100074,10 @@ MANTIS = { DLink = false, DLTimeStamp = 0, Padding = 10, + SuppressedGroups = {}, + automode = true, + autoshorad = true, + ShoradGroupSet = nil, } --- Advanced state enumerator @@ -94684,6 +100088,72 @@ MANTIS.AdvancedState = { RED = 2, } +--- SAM Type +-- @type MANTIS.SamType +MANTIS.SamType = { + SHORT = "Short", + MEDIUM = "Medium", + LONG = "Long", +} + +--- SAM data +-- @type MANTIS.SamData +-- @field #number Range Max firing range in km +-- @field #number Blindspot no-firing range (green circle) +-- @field #number Height Max firing height in km +-- @field #string Type #MANTIS.SamType of SAM, i.e. SHORT, MEDIUM or LONG (range) +-- @field #string Radar Radar typename on unit level (used as key) +MANTIS.SamData = { + ["Hawk"] = { Range=44, Blindspot=0, Height=9, Type="Medium", Radar="Hawk" }, -- measures in km + ["NASAMS"] = { Range=14, Blindspot=0, Height=3, Type="Short", Radar="NSAMS" }, + ["Patriot"] = { Range=99, Blindspot=0, Height=9, Type="Long", Radar="Patriot" }, + ["Rapier"] = { Range=6, Blindspot=0, Height=3, Type="Short", Radar="rapier" }, + ["SA-2"] = { Range=40, Blindspot=7, Height=25, Type="Medium", Radar="S_75M_Volhov" }, + ["SA-3"] = { Range=18, Blindspot=6, Height=18, Type="Short", Radar="5p73 s-125 ln" }, + ["SA-5"] = { Range=250, Blindspot=7, Height=40, Type="Long", Radar="5N62V" }, + ["SA-6"] = { Range=25, Blindspot=0, Height=8, Type="Medium", Radar="1S91" }, + ["SA-10"] = { Range=119, Blindspot=0, Height=18, Type="Long" , Radar="S-300PS 4"}, + ["SA-11"] = { Range=35, Blindspot=0, Height=20, Type="Medium", Radar="SA-11" }, + ["Roland"] = { Range=8, Blindspot=0, Height=3, Type="Short", Radar="Roland" }, + ["HQ-7"] = { Range=12, Blindspot=0, Height=3, Type="Short", Radar="HQ-7" }, + ["SA-9"] = { Range=4, Blindspot=0, Height=3, Type="Short", Radar="Strela" }, + ["SA-8"] = { Range=10, Blindspot=0, Height=5, Type="Short", Radar="Osa 9A33" }, + ["SA-19"] = { Range=8, Blindspot=0, Height=3, Type="Short", Radar="Tunguska" }, + ["SA-15"] = { Range=11, Blindspot=0, Height=6, Type="Short", Radar="Tor 9A331" }, + ["SA-13"] = { Range=5, Blindspot=0, Height=3, Type="Short", Radar="Strela" }, + ["Avenger"] = { Range=4, Blindspot=0, Height=3, Type="Short", Radar="Avenger" }, + ["Chaparrel"] = { Range=8, Blindspot=0, Height=3, Type="Short", Radar="Chaparral" }, + ["Linebacker"] = { Range=4, Blindspot=0, Height=3, Type="Short", Radar="Linebacker" }, + ["Silkworm"] = { Range=90, Blindspot=1, Height=0.2, Type="Long", Radar="Silkworm" }, + -- units from HDS Mod, multi launcher options is tricky + ["SA-10B"] = { Range=75, Blindspot=0, Height=18, Type="Medium" , Radar="SA-10B"}, + ["SA-17"] = { Range=50, Blindspot=3, Height=30, Type="Medium", Radar="SA-17" }, + ["SA-20A"] = { Range=150, Blindspot=5, Height=27, Type="Long" , Radar="S-300PMU1"}, + ["SA-20B"] = { Range=200, Blindspot=4, Height=27, Type="Long" , Radar="S-300PMU2"}, + ["HQ-2"] = { Range=50, Blindspot=6, Height=35, Type="Medium", Radar="HQ_2_Guideline_LN" }, +} + +--- SAM data HDS +-- @type MANTIS.SamDataHDS +-- @field #number Range Max firing range in km +-- @field #number Blindspot no-firing range (green circle) +-- @field #number Height Max firing height in km +-- @field #string Type #MANTIS.SamType of SAM, i.e. SHORT, MEDIUM or LONG (range) +-- @field #string Radar Radar typename on unit level (used as key) +MANTIS.SamDataHDS = { + -- units from HDS Mod, multi launcher options is tricky + -- group name MUST contain HDS to ID launcher type correctly! + ["SA-2 HDS"] = { Range=56, Blindspot=7, Height=30, Type="Medium", Radar="V759" }, + ["SA-3 HDS"] = { Range=20, Blindspot=6, Height=30, Type="Short", Radar="V-601P" }, + ["SA-10C HDS 2"] = { Range=90, Blindspot=5, Height=25, Type="Long" , Radar="5P85DE ln"}, -- V55RUD + ["SA-10C HDS 1"] = { Range=90, Blindspot=5, Height=25, Type="Long" , Radar="5P85CE ln"}, -- V55RUD + ["SA-12 HDS 2"] = { Range=100, Blindspot=10, Height=25, Type="Long" , Radar="S-300V 9A82 l"}, + ["SA-12 HDS 1"] = { Range=75, Blindspot=1, Height=25, Type="Long" , Radar="S-300V 9A83 l"}, + ["SA-23 HDS 2"] = { Range=200, Blindspot=5, Height=37, Type="Long", Radar="S-300VM 9A82ME" }, + ["SA-23 HDS 1"] = { Range=100, Blindspot=1, Height=50, Type="Long", Radar="S-300VM 9A83ME" }, + ["HQ-2 HDS"] = { Range=50, Blindspot=6, Height=35, Type="Medium", Radar="HQ_2_Guideline_LN" }, +} + ----------------------------------------------------------------------- -- MANTIS System ----------------------------------------------------------------------- @@ -94703,23 +100173,20 @@ do --@return #MANTIS self --@usage Start up your MANTIS with a basic setting -- - -- `myredmantis = MANTIS:New("myredmantis","Red SAM","Red EWR",nil,"red",false)` - -- `myredmantis:Start()` + -- myredmantis = MANTIS:New("myredmantis","Red SAM","Red EWR",nil,"red",false) + -- myredmantis:Start() -- -- [optional] Use -- - -- * `MANTIS:SetEWRGrouping(radius)` - -- * `MANTIS:SetEWRRange(radius)` - -- * `MANTIS:SetSAMRadius(radius)` - -- * `MANTIS:SetDetectInterval(interval)` - -- * `MANTIS:SetAutoRelocate(hq, ewr)` + -- myredmantis:SetDetectInterval(interval) + -- myredmantis:SetAutoRelocate(hq, ewr) -- -- before starting #MANTIS to fine-tune your setup. -- -- If you want to use a separate AWACS unit (default detection range: 250km) to support your EWR system, use e.g. the following setup: -- - -- `mybluemantis = MANTIS:New("bluemantis","Blue SAM","Blue EWR",nil,"blue",false,"Blue Awacs")` - -- `mybluemantis:Start()` + -- mybluemantis = MANTIS:New("bluemantis","Blue SAM","Blue EWR",nil,"blue",false,"Blue Awacs") + -- mybluemantis:Start() -- function MANTIS:New(name,samprefix,ewrprefix,hq,coaltion,dynamic,awacs, EmOnOff, Padding) @@ -94728,19 +100195,23 @@ do -- DONE: Set SAMs to auto if EWR dies -- DONE: Refresh SAM table in dynamic mode -- DONE: Treat Awacs separately, since they might be >80km off site + -- DONE: Allow tables of prefixes for the setup + -- DONE: Auto-Mode with range setups for various known SAM types. - self.name = name or "mymantis" self.SAM_Templates_Prefix = samprefix or "Red SAM" self.EWR_Templates_Prefix = ewrprefix or "Red EWR" self.HQ_Template_CC = hq or nil self.Coalition = coaltion or "red" self.SAM_Table = {} + self.SAM_Table_Long = {} + self.SAM_Table_Medium = {} + self.SAM_Table_Short = {} self.dynamic = dynamic or false self.checkradius = 25000 self.grouping = 5000 self.acceptrange = 80000 self.detectinterval = 30 - self.engagerange = 75 + self.engagerange = 95 self.autorelocate = false self.autorelocateunits = { HQ = false, EWR = false} self.advanced = false @@ -94753,20 +100224,35 @@ do self.Shorad = nil self.ShoradLink = false self.ShoradTime = 600 - self.ShoradActDistance = 15000 + self.ShoradActDistance = 25000 self.TimeStamp = timer.getAbsTime() self.relointerval = math.random(1800,3600) -- random between 30 and 60 mins self.state2flag = false self.SamStateTracker = {} -- table to hold alert states, so we don't trigger state changes twice in adv mode self.DLink = false self.Padding = Padding or 10 - - if EmOnOff then - if EmOnOff == false then - self.UseEmOnOff = false - else - self.UseEmOnOff = true - end + self.SuppressedGroups = {} + -- 0.8 additions + self.automode = true + self.radiusscale = {} + self.radiusscale[MANTIS.SamType.LONG] = 1.1 + self.radiusscale[MANTIS.SamType.MEDIUM] = 1.2 + self.radiusscale[MANTIS.SamType.SHORT] = 1.3 + --self.SAMCheckRanges = {} + self.usezones = false + self.AcceptZones = {} + self.RejectZones = {} + self.ConflictZones = {} + self.maxlongrange = 1 + self.maxmidrange = 2 + self.maxshortrange = 2 + self.maxclassic = 6 + self.autoshorad = true + self.ShoradGroupSet = SET_GROUP:New() -- Core.Set#SET_GROUP + + self.UseEmOnOff = true + if EmOnOff == false then + self.UseEmOnOff = false end if type(awacs) == "string" then @@ -94788,26 +100274,50 @@ do --BASE:TraceClass("SEAD") BASE:TraceLevel(1) end - + + self.ewr_templates = {} + if type(samprefix) ~= "table" then + self.SAM_Templates_Prefix = {samprefix} + end + + if type(ewrprefix) ~= "table" then + self.EWR_Templates_Prefix = {ewrprefix} + end + + for _,_group in pairs (self.SAM_Templates_Prefix) do + table.insert(self.ewr_templates,_group) + end + + for _,_group in pairs (self.EWR_Templates_Prefix) do + table.insert(self.ewr_templates,_group) + end + + if self.advAwacs then + table.insert(self.ewr_templates,awacs) + end + + self:T({self.ewr_templates}) + if self.dynamic then -- Set SAM SET_GROUP self.SAM_Group = SET_GROUP:New():FilterPrefixes(self.SAM_Templates_Prefix):FilterCoalitions(self.Coalition):FilterStart() -- Set EWR SET_GROUP - self.EWR_Group = SET_GROUP:New():FilterPrefixes({self.SAM_Templates_Prefix,self.EWR_Templates_Prefix}):FilterCoalitions(self.Coalition):FilterStart() + self.EWR_Group = SET_GROUP:New():FilterPrefixes(self.ewr_templates):FilterCoalitions(self.Coalition):FilterStart() else -- Set SAM SET_GROUP self.SAM_Group = SET_GROUP:New():FilterPrefixes(self.SAM_Templates_Prefix):FilterCoalitions(self.Coalition):FilterOnce() -- Set EWR SET_GROUP - self.EWR_Group = SET_GROUP:New():FilterPrefixes({self.SAM_Templates_Prefix,self.EWR_Templates_Prefix}):FilterCoalitions(self.Coalition):FilterOnce() + self.EWR_Group = SET_GROUP:New():FilterPrefixes(self.ewr_templates):FilterCoalitions(self.Coalition):FilterOnce() end -- set up CC if self.HQ_Template_CC then self.HQ_CC = GROUP:FindByName(self.HQ_Template_CC) end - + + -- TODO Version -- @field #string version - self.version="0.6.2" + self.version="0.8.8" self:I(string.format("***** Starting MANTIS Version %s *****", self.version)) --- FSM Functions --- @@ -94816,15 +100326,18 @@ do self:SetStartState("Stopped") -- Add FSM transitions. - -- From State --> Event --> To State - self:AddTransition("Stopped", "Start", "Running") -- Start FSM. - self:AddTransition("*", "Status", "*") -- MANTIS status update. - self:AddTransition("*", "Relocating", "*") -- MANTIS HQ and EWR are relocating. - self:AddTransition("*", "GreenState", "*") -- MANTIS A SAM switching to GREEN state. - self:AddTransition("*", "RedState", "*") -- MANTIS A SAM switching to RED state. - self:AddTransition("*", "AdvStateChange", "*") -- MANTIS advanced mode state change. - self:AddTransition("*", "ShoradActivated", "*") -- MANTIS woke up a connected SHORAD. - self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FSM. + self:AddTransition("*", "Status", "*") -- MANTIS status update. + self:AddTransition("*", "Relocating", "*") -- MANTIS HQ and EWR are relocating. + self:AddTransition("*", "GreenState", "*") -- MANTIS A SAM switching to GREEN state. + self:AddTransition("*", "RedState", "*") -- MANTIS A SAM switching to RED state. + self:AddTransition("*", "AdvStateChange", "*") -- MANTIS advanced mode state change. + self:AddTransition("*", "ShoradActivated", "*") -- MANTIS woke up a connected SHORAD. + self:AddTransition("*", "SeadSuppressionStart", "*") -- SEAD has switched off one group. + self:AddTransition("*", "SeadSuppressionEnd", "*") -- SEAD has switched on one group. + self:AddTransition("*", "SeadSuppressionPlanned", "*") -- SEAD has planned a suppression. + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. ------------------------ --- Pseudo Functions --- @@ -94903,6 +100416,37 @@ do -- @param #number Radius Radius around the named group to find SHORAD groups -- @param #number Ontime Seconds the SHORAD will stay active + --- On After "SeadSuppressionPlanned" event. Mantis has planned to switch off a site to defend SEAD attack. + -- @function [parent=#MANTIS] OnAfterSeadSuppressionPlanned + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @param Wrapper.Group#GROUP Group The suppressed GROUP object + -- @param #string Name Name of the suppressed group + -- @param #number SuppressionStartTime Model start time of the suppression from `timer.getTime()` + -- @param #number SuppressionEndTime Model end time of the suppression from `timer.getTime()` + -- @param Wrapper.Group#GROUP Attacker The attacking GROUP object + + --- On After "SeadSuppressionStart" event. Mantis has switched off a site to defend a SEAD attack. + -- @function [parent=#MANTIS] OnAfterSeadSuppressionStart + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @param Wrapper.Group#GROUP Group The suppressed GROUP object + -- @param #string Name Name of the suppressed group + -- @param Wrapper.Group#GROUP Attacker The attacking GROUP object + + --- On After "SeadSuppressionEnd" event. Mantis has switched on a site after a SEAD attack. + -- @function [parent=#MANTIS] OnAfterSeadSuppressionEnd + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @param Wrapper.Group#GROUP Group The suppressed GROUP object + -- @param #string Name Name of the suppressed group + return self end @@ -94936,20 +100480,42 @@ do self.grouping = radius return self end - - --- Function to set the detection radius of the EWR in meters + + --- Function to set accept and reject zones. + -- @param #MANTIS self + -- @param #table AcceptZones Table of @{Core.Zone#ZONE} objects + -- @param #table RejectZones Table of @{Core.Zone#ZONE} objects + -- @param #table ConflictZones Table of @{Core.Zone#ZONE} objects + -- @return #MANTIS self + -- @usage + -- Parameters are **tables of Core.Zone#ZONE** objects! + -- This is effectively a 3-stage filter allowing for zone overlap. A coordinate is accepted first when + -- it is inside any AcceptZone. Then RejectZones are checked, which enforces both borders, but also overlaps of + -- Accept- and RejectZones. Last, if it is inside a conflict zone, it is accepted. + function MANTIS:AddZones(AcceptZones,RejectZones, ConflictZones) + self:T(self.lid .. "AddZones") + self.AcceptZones = AcceptZones or {} + self.RejectZones = RejectZones or {} + self.ConflictZones = ConflictZones or {} + if #AcceptZones > 0 or #RejectZones > 0 or #ConflictZones > 0 then + self.usezones = true + end + return self + end + + --- Function to set the detection radius of the EWR in meters. (Deprecated, SAM range is used) -- @param #MANTIS self -- @param #number radius Radius of the EWR detection zone function MANTIS:SetEWRRange(radius) self:T(self.lid .. "SetEWRRange") - local radius = radius or 80000 - self.acceptrange = radius + --local radius = radius or 80000 + -- self.acceptrange = radius return self end - --- Function to set switch-on/off zone for the SAM sites in meters + --- Function to set switch-on/off zone for the SAM sites in meters. Overwritten per SAM in automode. -- @param #MANTIS self - -- @param #number radius Radius of the firing zone + -- @param #number radius Radius of the firing zone in classic mode function MANTIS:SetSAMRadius(radius) self:T(self.lid .. "SetSAMRadius") local radius = radius or 25000 @@ -94957,27 +100523,43 @@ do return self end - --- Function to set SAM firing engage range, 0-100 percent, e.g. 75 + --- Function to set SAM firing engage range, 0-100 percent, e.g. 85 -- @param #MANTIS self -- @param #number range Percent of the max fire range function MANTIS:SetSAMRange(range) self:T(self.lid .. "SetSAMRange") - local range = range or 75 + local range = range or 95 if range < 0 or range > 100 then - range = 75 + range = 95 end self.engagerange = range return self end + + --- Function to set number of SAMs going active on a valid, detected thread + -- @param #MANTIS self + -- @param #number Short Number of short-range systems activated, defaults to 1. + -- @param #number Mid Number of mid-range systems activated, defaults to 2. + -- @param #number Long Number of long-range systems activated, defaults to 2. + -- @param #number Classic (non-automode) Number of overall systems activated, defaults to 6. + -- @return #MANTIS self + function MANTIS:SetMaxActiveSAMs(Short,Mid,Long,Classic) + self:T(self.lid .. "SetMaxActiveSAMs") + self.maxclassic = Classic or 6 + self.maxlongrange = Long or 1 + self.maxmidrange = Mid or 2 + self.maxshortrange = Short or 2 + return self + end --- Function to set a new SAM firing engage range, use this method to adjust range while running MANTIS, e.g. for different setups day and night -- @param #MANTIS self -- @param #number range Percent of the max fire range function MANTIS:SetNewSAMRangeWhileRunning(range) self:T(self.lid .. "SetNewSAMRangeWhileRunning") - local range = range or 75 + local range = range or 95 if range < 0 or range > 100 then - range = 75 + range = 95 end self.engagerange = range self:_RefreshSAMTable() @@ -95101,7 +100683,7 @@ do return self end - --- Set using an #INTEL_DLINK object instead of #DETECTION + --- Set using your own #INTEL_DLINK object instead of #DETECTION -- @param #MANTIS self -- @param Ops.Intelligence#INTEL_DLINK DLink The data link object to be used. function MANTIS:SetUsingDLink(DLink) @@ -95222,6 +100804,7 @@ do --- [Internal] Function to execute the relocation -- @param #MANTIS self + -- @return #MANTIS self function MANTIS:_RelocateGroups() self:T(self.lid .. "RelocateGroups") local text = self.lid.." Relocating Groups" @@ -95256,38 +100839,124 @@ do end return self end - + + --- [Internal] Function to check accept and reject zones + -- @param #MANTIS self + -- @param Core.Point#COORDINATE coord The coordinate to check + -- @return #boolean outcome + function MANTIS:_CheckCoordinateInZones(coord) + -- DEBUG + self:T(self.lid.."_CheckCoordinateInZones") + local inzone = false + -- acceptzones + if #self.AcceptZones > 0 then + for _,_zone in pairs(self.AcceptZones) do + local zone = _zone -- Core.Zone#ZONE + if zone:IsCoordinateInZone(coord) then + inzone = true + self:T(self.lid.."Target coord in Accept Zone!") + break + end + end + end + -- rejectzones + if #self.RejectZones > 0 and inzone then -- maybe in accept zone, but check the overlaps + for _,_zone in pairs(self.RejectZones) do + local zone = _zone -- Core.Zone#ZONE + if zone:IsCoordinateInZone(coord) then + inzone = false + self:T(self.lid.."Target coord in Reject Zone!") + break + end + end + end + -- conflictzones + if #self.ConflictZones > 0 and not inzone then -- if not already accepted, might be in conflict zones + for _,_zone in pairs(self.ConflictZones) do + local zone = _zone -- Core.Zone#ZONE + if zone:IsCoordinateInZone(coord) then + inzone = true + self:T(self.lid.."Target coord in Conflict Zone!") + break + end + end + end + return inzone + end + + --- [Internal] Function to prefilter height based + -- @param #MANTIS self + -- @param #number height + -- @return #table set + function MANTIS:_PreFilterHeight(height) + self:T(self.lid.."_PreFilterHeight") + local set = {} + local dlink = self.Detection -- Ops.Intelligence#INTEL_DLINK + local detectedgroups = dlink:GetContactTable() + for _,_contact in pairs(detectedgroups) do + local contact = _contact -- Ops.Intelligence#INTEL.Contact + local grp = contact.group -- Wrapper.Group#GROUP + if grp:IsAlive() then + if grp:GetHeight(true) < height then + local coord = grp:GetCoordinate() + table.insert(set,coord) + end + end + end + return set + end + --- [Internal] Function to check if any object is in the given SAM zone -- @param #MANTIS self -- @param #table dectset Table of coordinates of detected items -- @param Core.Point#COORDINATE samcoordinate Coordinate object. + -- @param #number radius Radius to check. + -- @param #number height Height to check. + -- @param #boolean dlink Data from DLINK. -- @return #boolean True if in any zone, else false -- @return #number Distance Target distance in meters or zero when no object is in zone - function MANTIS:CheckObjectInZone(dectset, samcoordinate) - self:T(self.lid.."CheckObjectInZone") + function MANTIS:_CheckObjectInZone(dectset, samcoordinate, radius, height, dlink) + self:T(self.lid.."_CheckObjectInZone") -- check if non of the coordinate is in the given defense zone - local radius = self.checkradius + local rad = radius or self.checkradius local set = dectset + if dlink then + -- DEBUG + set = self:_PreFilterHeight(height) + end for _,_coord in pairs (set) do local coord = _coord -- get current coord to check -- output for cross-check local targetdistance = samcoordinate:DistanceFromPointVec2(coord) - if self.verbose or self.debug then + if not targetdistance then + targetdistance = samcoordinate:Get2DDistance(coord) + end + -- check accept/reject zones + local zonecheck = true + if self.usezones then + -- DONE + zonecheck = self:_CheckCoordinateInZones(coord) + end + if self.verbose and self.debug then local dectstring = coord:ToStringLLDMS() local samstring = samcoordinate:ToStringLLDMS() - local text = string.format("Checking SAM at % s - Distance %d m - Target %s", samstring, targetdistance, dectstring) + local inrange = "false" + if targetdistance <= rad then + inrange = "true" + end + local text = string.format("Checking SAM at %s | Targetdist %d | Rad %d | Inrange %s", samstring, targetdistance, rad, inrange) local m = MESSAGE:New(text,10,"Check"):ToAllIf(self.debug) - self:I(self.lid..text) + self:T(self.lid..text) end -- end output to cross-check - if targetdistance <= radius then + if targetdistance <= rad and zonecheck then return true, targetdistance end end return false, 0 end - --- [Internal] Function to start the detection via EWR groups + --- [Internal] Function to start the detection via EWR groups - if INTEL isn\'t available -- @param #MANTIS self -- @return Functional.Detection #DETECTION_AREAS The running detection set function MANTIS:StartDetection() @@ -95296,29 +100965,52 @@ do -- start detection local groupset = self.EWR_Group local grouping = self.grouping or 5000 - local acceptrange = self.acceptrange or 80000 - local interval = self.detectinterval or 60 - - --@param Functional.Detection #DETECTION_AREAS _MANTISdetection [Internal] The MANTIS detection object + --local acceptrange = self.acceptrange or 80000 + local interval = self.detectinterval or 20 + local MANTISdetection = DETECTION_AREAS:New( groupset, grouping ) --[Internal] Grouping detected objects to 5000m zones MANTISdetection:FilterCategories({ Unit.Category.AIRPLANE, Unit.Category.HELICOPTER }) - MANTISdetection:SetAcceptRange(acceptrange) + --MANTISdetection:SetAcceptRange(acceptrange) -- deprecated - in range of SAMs is used anyway MANTISdetection:SetRefreshTimeInterval(interval) - MANTISdetection:Start() + MANTISdetection:__Start(2) - function MANTISdetection:OnAfterDetectedItem(From,Event,To,DetectedItem) - --BASE:I( { From, Event, To, DetectedItem }) - local debug = false - if DetectedItem.IsDetected and debug then - local Coordinate = DetectedItem.Coordinate -- Core.Point#COORDINATE - local text = "MANTIS: Detection at "..Coordinate:ToStringLLDMS() - local m = MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) - end - end return MANTISdetection end - - --- [Internal] Function to start the detection via AWACS if defined as separate + + --- [Internal] Function to start the detection with INTEL via EWR groups + -- @param #MANTIS self + -- @return Ops.Intel#INTEL_DLINK The running detection set + function MANTIS:StartIntelDetection() + self:T(self.lid.."Starting Intel Detection") + -- DEBUG + -- start detection + local groupset = self.EWR_Group + local samset = self.SAM_Group + + self.intelset = {} + + local IntelOne = INTEL:New(groupset,self.Coalition,self.name.." IntelOne") + --IntelOne:SetClusterAnalysis(true,true) + --IntelOne:SetClusterRadius(5000) + IntelOne:Start() + + local IntelTwo = INTEL:New(samset,self.Coalition,self.name.." IntelTwo") + --IntelTwo:SetClusterAnalysis(true,true) + --IntelTwo:SetClusterRadius(5000) + IntelTwo:Start() + + local IntelDlink = INTEL_DLINK:New({IntelOne,IntelTwo},self.name.." DLINK",22,300) + IntelDlink:__Start(1) + + self:SetUsingDLink(IntelDlink) + + table.insert(self.intelset, IntelOne) + table.insert(self.intelset, IntelTwo) + + return IntelDlink + end + + --- [Internal] Function to start the detection via AWACS if defined as separate (classic) -- @param #MANTIS self -- @return Functional.Detection #DETECTION_AREAS The running detection set function MANTIS:StartAwacsDetection() @@ -95338,53 +101030,167 @@ do MANTISAwacs:SetRefreshTimeInterval(interval) MANTISAwacs:Start() - function MANTISAwacs:OnAfterDetectedItem(From,Event,To,DetectedItem) - --BASE:I( { From, Event, To, DetectedItem }) - local debug = false - if DetectedItem.IsDetected and debug then - local Coordinate = DetectedItem.Coordinate -- Core.Point#COORDINATE - local text = "Awacs Detection at "..Coordinate:ToStringLLDMS() - local m = MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) - end - end return MANTISAwacs end - + + --- [Internal] Function to get SAM firing data from units types. + -- @param #MANTIS self + -- @param #string grpname Name of the group + -- @param #boolean mod HDS mod flag + -- @return #number range Max firing range + -- @return #number height Max firing height + -- @return #string type Long, medium or short range + -- @return #number blind "blind" spot + function MANTIS:_GetSAMDataFromUnits(grpname,mod) + self:T(self.lid.."_GetSAMRangeFromUnits") + local found = false + local range = self.checkradius + local height = 3000 + local type = MANTIS.SamType.MEDIUM + local radiusscale = self.radiusscale[type] + local blind = 0 + local group = GROUP:FindByName(grpname) -- Wrapper.Group#GROUP + local units = group:GetUnits() + local SAMData = self.SamData + if mod then + SAMData = self.SamDataHDS + end + --self:I("Looking to auto-match for "..grpname) + for _,_unit in pairs(units) do + local unit = _unit -- Wrapper.Unit#UNIT + local type = string.lower(unit:GetTypeName()) + --self:I(string.format("Matching typename: %s",type)) + for idx,entry in pairs(SAMData) do + local _entry = entry -- #MANTIS.SamData + local _radar = string.lower(_entry.Radar) + --self:I(string.format("Trying typename: %s",_radar)) + if string.find(type,_radar,1,true) then + type = _entry.Type + radiusscale = self.radiusscale[type] + range = _entry.Range * 1000 * radiusscale -- max firing range used as switch-on + height = _entry.Height * 1000 -- max firing height + blind = _entry.Blindspot * 100 -- blind spot range + --self:I(string.format("Match: %s - %s",_radar,type)) + found = true + break + end + end + if found then break end + end + if not found then + self:E(self.lid .. string.format("*****Could not match radar data for %s! Will default to midrange values!",grpname)) + end + return range, height, type, blind + end + + --- [Internal] Function to get SAM firing data + -- @param #MANTIS self + -- @param #string grpname Name of the group + -- @return #number range Max firing range + -- @return #number height Max firing height + -- @return #string type Long, medium or short range + -- @return #number blind "blind" spot + function MANTIS:_GetSAMRange(grpname) + self:T(self.lid.."_GetSAMRange") + local range = self.checkradius + local height = 3000 + local type = MANTIS.SamType.MEDIUM + local radiusscale = self.radiusscale[type] + local blind = 0 + local found = false + local HDSmod = false + if string.find(grpname,"HDS",1,true) then + HDSmod = true + end + if self.automode then + for idx,entry in pairs(self.SamData) do + --self:I("ID = " .. idx) + if string.find(grpname,idx,1,true) then + local _entry = entry -- #MANTIS.SamData + type = _entry.Type + radiusscale = self.radiusscale[type] + range = _entry.Range * 1000 * radiusscale -- max firing range + height = _entry.Height * 1000 -- max firing height + blind = _entry.Blindspot + --self:I("Matching Groupname = " .. grpname .. " Range= " .. range) + found = true + break + end + end + end + -- secondary filter if not found + if (not found and self.automode) or HDSmod then + range, height, type = self:_GetSAMDataFromUnits(grpname,HDSmod) + elseif not found then + self:E(self.lid .. string.format("*****Could not match radar data for %s! Will default to midrange values!",grpname)) + end + return range, height, type, blind + end + --- [Internal] Function to set the SAM start state -- @param #MANTIS self -- @return #MANTIS self function MANTIS:SetSAMStartState() -- DONE: if using dynamic filtering, update SAM_Table and the (active) SEAD groups, pull req #1405/#1406 + -- DONE: Auto mode self:T(self.lid.."Setting SAM Start States") -- get SAM Group local SAM_SET = self.SAM_Group local SAM_Grps = SAM_SET.Set --table of objects local SAM_Tbl = {} -- table of SAM defense zones + local SAM_Tbl_lg = {} -- table of long range SAM defense zones + local SAM_Tbl_md = {} -- table of mid range SAM defense zones + local SAM_Tbl_sh = {} -- table of short range SAM defense zones local SEAD_Grps = {} -- table of SAM names to make evasive local engagerange = self.engagerange -- firing range in % of max --cycle through groups and set alarm state etc for _i,_group in pairs (SAM_Grps) do + if _group:IsGround() and _group:IsAlive() then local group = _group -- Wrapper.Group#GROUP - -- TODO: add emissions on/off + -- DONE: add emissions on/off if self.UseEmOnOff then + group:OptionAlarmStateRed() group:EnableEmission(false) --group:SetAIOff() else group:OptionAlarmStateGreen() -- AI off end - group:SetOption(AI.Option.Ground.id.AC_ENGAGEMENT_RANGE_RESTRICTION,engagerange) --default engagement will be 75% of firing range - if group:IsGround() and group:IsAlive() then - local grpname = group:GetName() - local grpcoord = group:GetCoordinate() - table.insert( SAM_Tbl, {grpname, grpcoord}) + group:OptionEngageRange(engagerange) --default engagement will be 95% of firing range + local grpname = group:GetName() + local grpcoord = group:GetCoordinate() + local grprange,grpheight,type,blind = self:_GetSAMRange(grpname) + table.insert( SAM_Tbl, {grpname, grpcoord, grprange, grpheight, blind}) + --table.insert( SEAD_Grps, grpname ) + if type == MANTIS.SamType.LONG then + table.insert( SAM_Tbl_lg, {grpname, grpcoord, grprange, grpheight, blind}) table.insert( SEAD_Grps, grpname ) - self.SamStateTracker[grpname] = "GREEN" + --self:T("SAM "..grpname.." is type LONG") + elseif type == MANTIS.SamType.MEDIUM then + table.insert( SAM_Tbl_md, {grpname, grpcoord, grprange, grpheight, blind}) + table.insert( SEAD_Grps, grpname ) + --self:T("SAM "..grpname.." is type MEDIUM") + elseif type == MANTIS.SamType.SHORT then + table.insert( SAM_Tbl_sh, {grpname, grpcoord, grprange, grpheight, blind}) + --self:T("SAM "..grpname.." is type SHORT") + self.ShoradGroupSet:Add(grpname,group) + if not self.autoshorad then + table.insert( SEAD_Grps, grpname ) + end + end + self.SamStateTracker[grpname] = "GREEN" end end self.SAM_Table = SAM_Tbl + self.SAM_Table_Long = SAM_Tbl_lg + self.SAM_Table_Medium = SAM_Tbl_md + self.SAM_Table_Short = SAM_Tbl_sh -- make SAMs evasive local mysead = SEAD:New( SEAD_Grps, self.Padding ) -- Functional.Sead#SEAD mysead:SetEngagementRange(engagerange) + mysead:AddCallBack(self) + if self.UseEmOnOff then + mysead:SwitchEmissions(true) + end self.mysead = mysead return self end @@ -95399,20 +101205,41 @@ do local SAM_SET = self.SAM_Group local SAM_Grps = SAM_SET.Set --table of objects local SAM_Tbl = {} -- table of SAM defense zones + local SAM_Tbl_lg = {} -- table of long range SAM defense zones + local SAM_Tbl_md = {} -- table of mid range SAM defense zones + local SAM_Tbl_sh = {} -- table of short range SAM defense zon local SEAD_Grps = {} -- table of SAM names to make evasive local engagerange = self.engagerange -- firing range in % of max --cycle through groups and set alarm state etc for _i,_group in pairs (SAM_Grps) do - local group = _group - group:SetOption(AI.Option.Ground.id.AC_ENGAGEMENT_RANGE_RESTRICTION,engagerange) --engagement will be 75% of firing range + local group = _group -- Wrapper.Group#GROUP + group:OptionEngageRange(engagerange) --engagement will be 95% of firing range if group:IsGround() and group:IsAlive() then local grpname = group:GetName() local grpcoord = group:GetCoordinate() - table.insert( SAM_Tbl, {grpname, grpcoord}) -- make the table lighter, as I don't really use the zone here + local grprange, grpheight,type,blind = self:_GetSAMRange(grpname) + table.insert( SAM_Tbl, {grpname, grpcoord, grprange, grpheight, blind}) -- make the table lighter, as I don't really use the zone here table.insert( SEAD_Grps, grpname ) + if type == MANTIS.SamType.LONG then + table.insert( SAM_Tbl_lg, {grpname, grpcoord, grprange, grpheight, blind}) + --self:I({grpname,grprange, grpheight}) + elseif type == MANTIS.SamType.MEDIUM then + table.insert( SAM_Tbl_md, {grpname, grpcoord, grprange, grpheight, blind}) + --self:I({grpname,grprange, grpheight}) + elseif type == MANTIS.SamType.SHORT then + table.insert( SAM_Tbl_sh, {grpname, grpcoord, grprange, grpheight, blind}) + -- self:I({grpname,grprange, grpheight}) + self.ShoradGroupSet:Add(grpname,group) + if self.autoshorad then + self.Shorad.Groupset = self.ShoradGroupSet + end + end end end self.SAM_Table = SAM_Tbl + self.SAM_Table_Long = SAM_Tbl_lg + self.SAM_Table_Medium = SAM_Tbl_md + self.SAM_Table_Short = SAM_Tbl_sh -- make SAMs evasive if self.mysead ~= nil then local mysead = self.mysead @@ -95449,44 +101276,48 @@ do ----------------------------------------------------------------------- -- MANTIS main functions ----------------------------------------------------------------------- - + --- [Internal] Check detection function -- @param #MANTIS self - -- @param Functional.Detection#DETECTION_AREAS detection Detection object + -- @param #table samset Table of SAM data + -- @param #table detset Table of COORDINATES + -- @param #boolean dlink Using DLINK + -- @param #limit limit of SAM sites to go active on a contact -- @return #MANTIS self - function MANTIS:_Check(detection) - self:T(self.lid .. "Check") - --get detected set - local detset = detection:GetDetectedItemCoordinates() - self:T("Check:", {detset}) - -- randomly update SAM Table - local rand = math.random(1,100) - if rand > 65 then -- 1/3 of cases - self:_RefreshSAMTable() - end - -- switch SAMs on/off if (n)one of the detected groups is inside their reach - local samset = self:_GetSAMTable() -- table of i.1=names, i.2=coordinates + function MANTIS:_CheckLoop(samset,detset,dlink,limit) + self:T(self.lid .. "CheckLoop " .. #detset .. " Coordinates") + local switchedon = 0 for _,_data in pairs (samset) do local samcoordinate = _data[2] local name = _data[1] + local radius = _data[3] + local height = _data[4] + local blind = _data[5] * 1.25 + 1 local samgroup = GROUP:FindByName(name) - local IsInZone, Distance = self:CheckObjectInZone(detset, samcoordinate) - if IsInZone then --check any target in zone + local IsInZone, Distance = self:_CheckObjectInZone(detset, samcoordinate, radius, height, dlink) + local suppressed = self.SuppressedGroups[name] or false + local activeshorad = self.Shorad.ActiveGroups[name] or false + if IsInZone and not suppressed and not activeshorad then --check any target in zone and not currently managed by SEAD if samgroup:IsAlive() then -- switch on SAM - if self.UseEmOnOff then - -- TODO: add emissions on/off - --samgroup:SetAIOn() + local switch = false + if self.UseEmOnOff and switchedon < limit then + -- DONE: add emissions on/off samgroup:EnableEmission(true) + switchedon = switchedon + 1 + switch = true + elseif (not self.UseEmOnOff) and switchedon < limit then + samgroup:OptionAlarmStateRed() + switchedon = switchedon + 1 + switch = true end - samgroup:OptionAlarmStateRed() - if self.SamStateTracker[name] ~= "RED" then + if self.SamStateTracker[name] ~= "RED" and switch then self:__RedState(1,samgroup) self.SamStateTracker[name] = "RED" end -- link in to SHORAD if available -- DONE: Test integration fully - if self.ShoradLink and Distance < self.ShoradActDistance then -- don't give SHORAD position away too early + if self.ShoradLink and (Distance < self.ShoradActDistance or Distance < blind ) then -- don't give SHORAD position away too early local Shorad = self.Shorad local radius = self.checkradius local ontime = self.ShoradTime @@ -95494,25 +101325,26 @@ do self:__ShoradActivated(1,name, radius, ontime) end -- debug output - if self.debug or self.verbose then - local text = string.format("SAM %s switched to alarm state RED!", name) + if (self.debug or self.verbose) and switch then + local text = string.format("SAM %s in alarm state RED!", name) local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) if self.verbose then self:I(self.lid..text) end end end --end alive else - if samgroup:IsAlive() then + if samgroup:IsAlive() and not suppressed and not activeshorad then -- switch off SAM - if self.UseEmOnOff then + if self.UseEmOnOff then samgroup:EnableEmission(false) - end + else samgroup:OptionAlarmStateGreen() - if self.SamStateTracker[name] ~= "GREEN" then - self:__GreenState(1,samgroup) - self.SamStateTracker[name] = "GREEN" - end + end + if self.SamStateTracker[name] ~= "GREEN" then + self:__GreenState(1,samgroup) + self.SamStateTracker[name] = "GREEN" + end if self.debug or self.verbose then - local text = string.format("SAM %s switched to alarm state GREEN!", name) + local text = string.format("SAM %s in alarm state GREEN!", name) local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) if self.verbose then self:I(self.lid..text) end end @@ -95521,6 +101353,36 @@ do end --for for loop return self end + + --- [Internal] Check detection function + -- @param #MANTIS self + -- @param Functional.Detection#DETECTION_AREAS detection Detection object + -- @param #boolean dlink + -- @return #MANTIS self + function MANTIS:_Check(detection,dlink) + self:T(self.lid .. "Check") + --get detected set + local detset = detection:GetDetectedItemCoordinates() + --self:T("Check:", {detset}) + -- randomly update SAM Table + local rand = math.random(1,100) + if rand > 65 then -- 1/3 of cases + self:_RefreshSAMTable() + end + -- switch SAMs on/off if (n)one of the detected groups is inside their reach + if self.automode then + local samset = self.SAM_Table_Long -- table of i.1=names, i.2=coordinates, i.3=firing range, i.4=firing height + self:_CheckLoop(samset,detset,dlink,self.maxlongrange) + local samset = self.SAM_Table_Medium -- table of i.1=names, i.2=coordinates, i.3=firing range, i.4=firing height + self:_CheckLoop(samset,detset,dlink,self.maxmidrange) + local samset = self.SAM_Table_Short -- table of i.1=names, i.2=coordinates, i.3=firing range, i.4=firing height + self:_CheckLoop(samset,detset,dlink,self.maxshortrange) + else + local samset = self:_GetSAMTable() -- table of i.1=names, i.2=coordinates, i.3=firing range, i.4=firing height + self:_CheckLoop(samset,detset,dlink,self.maxclassic) + end + return self + end --- [Internal] Relocation relay function -- @param #MANTIS self @@ -95550,11 +101412,12 @@ do local samgroup = GROUP:FindByName(name) if samgroup:IsAlive() then if self.UseEmOnOff then - -- TODO: add emissions on/off + -- DONE: add emissions on/off --samgroup:SetAIOn() samgroup:EnableEmission(true) + else + samgroup:OptionAlarmStateRed() end - samgroup:OptionAlarmStateRed() end -- end alive end -- end for loop elseif newstate <= 1 then @@ -95590,12 +101453,22 @@ do self:T({From, Event, To}) self:T(self.lid.."Starting MANTIS") self:SetSAMStartState() - if not self.DLink then + if not INTEL then self.Detection = self:StartDetection() + else + self.Detection = self:StartIntelDetection() end - if self.advAwacs then + --[[ + if self.advAwacs and not self.automode then self.AWACS_Detection = self:StartAwacsDetection() end + --]] + if self.autoshorad then + self.Shorad = SHORAD:New(self.name.."-SHORAD",self.name.."-SHORAD",self.SAM_Group,self.ShoradActDistance,self.ShoradTime,self.coalition,self.UseEmOnOff) + self.Shorad:SetDefenseLimits(80,95) + self.ShoradLink = true + self.Shorad.Groupset=self.ShoradGroupSet + end self:__Status(-math.random(1,10)) return self end @@ -95610,14 +101483,15 @@ do self:T({From, Event, To}) -- check detection if not self.state2flag then - self:_Check(self.Detection) + self:_Check(self.Detection,self.DLink) end - -- check Awacs + --[[ check Awacs if self.advAwacs and not self.state2flag then - self:_Check(self.AWACS_Detection) + self:_Check(self.AWACS_Detection,false) end - + --]] + -- relocate HQ and EWR if self.autorelocate then local relointerval = self.relointerval @@ -95657,7 +101531,7 @@ do function MANTIS:onafterStatus(From,Event,To) self:T({From, Event, To}) -- Display some states - if self.debug then + if self.debug and self.verbose then self:I(self.lid .. "Status Report") for _name,_state in pairs(self.SamStateTracker) do self:I(string.format("Site %s\tStatus %s",_name,_state)) @@ -95698,7 +101572,7 @@ do -- @param Wrapper.Group#GROUP Group The GROUP object whose state was changed -- @return #MANTIS self function MANTIS:onafterGreenState(From, Event, To, Group) - self:T({From, Event, To, Group}) + self:T({From, Event, To, Group:GetName()}) return self end @@ -95710,7 +101584,7 @@ do -- @param Wrapper.Group#GROUP Group The GROUP object whose state was changed -- @return #MANTIS self function MANTIS:onafterRedState(From, Event, To, Group) - self:T({From, Event, To, Group}) + self:T({From, Event, To, Group:GetName()}) return self end @@ -95740,6 +101614,56 @@ do self:T({From, Event, To, Name, Radius, Ontime}) return self end + + --- [Internal] Function triggered by Event SeadSuppressionStart + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @param Wrapper.Group#GROUP Group The suppressed GROUP object + -- @param #string Name Name of the suppressed group + -- @param Wrapper.Group#GROUP Attacker The attacking GROUP object + function MANTIS:onafterSeadSuppressionStart(From, Event, To, Group, Name, Attacker) + self:T({From, Event, To, Name}) + self.SuppressedGroups[Name] = true + if self.ShoradLink then + local Shorad = self.Shorad + local radius = self.checkradius + local ontime = self.ShoradTime + Shorad:WakeUpShorad(Name, radius, ontime) + self:__ShoradActivated(1,Name, radius, ontime) + end + return self + end + + --- [Internal] Function triggered by Event SeadSuppressionEnd + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @param Wrapper.Group#GROUP Group The suppressed GROUP object + -- @param #string Name Name of the suppressed group + function MANTIS:onafterSeadSuppressionEnd(From, Event, To, Group, Name) + self:T({From, Event, To, Name}) + self.SuppressedGroups[Name] = false + return self + end + + --- [Internal] Function triggered by Event SeadSuppressionPlanned + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @param Wrapper.Group#GROUP Group The suppressed GROUP object + -- @param #string Name Name of the suppressed group + -- @param #number SuppressionStartTime Model start time of the suppression from `timer.getTime()` + -- @param #number SuppressionEndTime Model end time of the suppression from `timer.getTime()` + -- @param Wrapper.Group#GROUP Attacker The attacking GROUP object + function MANTIS:onafterSeadSuppressionPlanned(From, Event, To, Group, Name, SuppressionStartTime, SuppressionEndTime, Attacker) + self:T({From, Event, To, Name}) + return self + end + end ----------------------------------------------------------------------- -- MANTIS end @@ -95764,7 +101688,7 @@ end -- @module Functional.Shorad -- @image Functional.Shorad.jpg -- --- Date: July 2021 +-- Date: Nov 2021 ------------------------------------------------------------------------- --- **SHORAD** class, extends Core.Base#BASE @@ -95787,6 +101711,7 @@ end -- @field #boolean UseEmOnOff Decide if we are using Emission on/off (default) or AlarmState red/green. -- @extends Core.Base#BASE + --- *Good friends are worth defending.* Mr Tushman, Wonder (the Movie) -- -- Simple Class for a more intelligent Short Range Air Defense System @@ -95877,6 +101802,7 @@ do ["Kh29"] = "Kh29", ["Kh31"] = "Kh31", ["Kh66"] = "Kh66", + --["BGM_109"] = "BGM_109", } --- Instantiates a new SHORAD object @@ -95884,13 +101810,13 @@ do -- @param #string Name Name of this SHORAD -- @param #string ShoradPrefix Filter for the Shorad #SET_GROUP -- @param Core.Set#SET_GROUP Samset The #SET_GROUP of SAM sites to defend - -- @param #number Radius Defense radius in meters, used to switch on groups + -- @param #number Radius Defense radius in meters, used to switch on SHORAD groups **within** this radius -- @param #number ActiveTimer Determines how many seconds the systems stay on red alert after wake-up call -- @param #string Coalition Coalition, i.e. "blue", "red", or "neutral" -- @param #boolean UseEmOnOff Use Emissions On/Off rather than Alarm State Red/Green (default: use Emissions switch) -- @retunr #SHORAD self function SHORAD:New(Name, ShoradPrefix, Samset, Radius, ActiveTimer, Coalition, UseEmOnOff) - local self = BASE:Inherit( self, BASE:New() ) + local self = BASE:Inherit( self, FSM:New() ) self:T({Name, ShoradPrefix, Samset, Radius, ActiveTimer, Coalition}) local GroupSet = SET_GROUP:New():FilterPrefixes(ShoradPrefix):FilterCoalitions(Coalition):FilterCategoryGround():FilterStart() @@ -95908,11 +101834,17 @@ do self.DefenseLowProb = 70 -- probability to detect a missile shot, low margin self.DefenseHighProb = 90 -- probability to detect a missile shot, high margin self.UseEmOnOff = UseEmOnOff or false -- Decide if we are using Emission on/off (default) or AlarmState red/green - self:I("*** SHORAD - Started Version 0.2.8") + self:I("*** SHORAD - Started Version 0.3.1") -- Set the string id for output to DCS.log file. self.lid=string.format("SHORAD %s | ", self.name) self:_InitState() self:HandleEvent(EVENTS.Shot, self.HandleEventShot) + + -- Start State. + self:SetStartState("Running") + self:AddTransition("*", "WakeUpShorad", "*") + self:AddTransition("*", "CalculateHitZone", "*") + return self end @@ -96056,7 +101988,7 @@ do local hit = false if self.DefendHarms then for _,_name in pairs (SHORAD.Harms) do - if string.find(WeaponName,_name,1) then hit = true end + if string.find(WeaponName,_name,1,true) then hit = true end end end return hit @@ -96072,7 +102004,7 @@ do local hit = false if self.DefendMavs then for _,_name in pairs (SHORAD.Mavs) do - if string.find(WeaponName,_name,1) then hit = true end + if string.find(WeaponName,_name,1,true) then hit = true end end end return hit @@ -96113,7 +102045,7 @@ do local returnname = false for _,_groups in pairs (shoradset) do local groupname = _groups:GetName() - if string.find(groupname, tgtgrp, 1) then + if string.find(groupname, tgtgrp, 1, true) then returnname = true --_groups:RelocateGroundRandomInRadius(7,100,false,false) -- be a bit evasive end @@ -96134,7 +102066,7 @@ do local returnname = false for _,_groups in pairs (shoradset) do local groupname = _groups:GetName() - if string.find(groupname, tgtgrp, 1) then + if string.find(groupname, tgtgrp, 1, true) then returnname = true end end @@ -96146,6 +102078,7 @@ do -- @return #boolean Returns true for a detection, else false function SHORAD:_ShotIsDetected() self:T(self.lid .. " _ShotIsDetected") + if self.debug then return true end local IsDetected = false local DetectionProb = math.random(self.DefenseLowProb, self.DefenseHighProb) -- reference value local ActualDetection = math.random(1,100) -- value for this shot @@ -96169,7 +102102,7 @@ do -- mymantis = MANTIS:New("BlueMantis","Blue SAM","Blue EWR",nil,"blue",false,"Blue Awacs") -- mymantis:AddShorad(myshorad,720) -- mymantis:Start() - function SHORAD:WakeUpShorad(TargetGroup, Radius, ActiveTimer, TargetCat) + function SHORAD:onafterWakeUpShorad(From, Event, To, TargetGroup, Radius, ActiveTimer, TargetCat) self:T(self.lid .. " WakeUpShorad") self:T({TargetGroup, Radius, ActiveTimer, TargetCat}) local targetcat = TargetCat or Object.Category.UNIT @@ -96223,6 +102156,76 @@ do return self end +--- (Internal) Calculate hit zone of an AGM-88 +-- @param #SHORAD self +-- @param #table SEADWeapon DCS.Weapon object +-- @param Core.Point#COORDINATE pos0 Position of the plane when it fired +-- @param #number height Height when the missile was fired +-- @param Wrapper.Group#GROUP SEADGroup Attacker group +-- @return #SHORAD self +function SHORAD:onafterCalculateHitZone(From,Event,To,SEADWeapon,pos0,height,SEADGroup) + self:T("**** Calculating hit zone") + if SEADWeapon and SEADWeapon:isExist() then + --local pos = SEADWeapon:getPoint() + + -- postion and height + local position = SEADWeapon:getPosition() + local mheight = height + -- heading + local wph = math.atan2(position.x.z, position.x.x) + if wph < 0 then + wph=wph+2*math.pi + end + wph=math.deg(wph) + + -- velocity + local wpndata = SEAD.HarmData["AGM_88"] + local mveloc = math.floor(wpndata[2] * 340.29) + local c1 = (2*mheight*9.81)/(mveloc^2) + local c2 = (mveloc^2) / 9.81 + local Ropt = c2 * math.sqrt(c1+1) + if height <= 5000 then + Ropt = Ropt * 0.72 + elseif height <= 7500 then + Ropt = Ropt * 0.82 + elseif height <= 10000 then + Ropt = Ropt * 0.87 + elseif height <= 12500 then + Ropt = Ropt * 0.98 + end + + -- look at a couple of zones across the trajectory + for n=1,3 do + local dist = Ropt - ((n-1)*20000) + local predpos= pos0:Translate(dist,wph) + if predpos then + + local targetzone = ZONE_RADIUS:New("Target Zone",predpos:GetVec2(),20000) + + if self.debug then + predpos:MarkToAll(string.format("height=%dm | heading=%d | velocity=%ddeg | Ropt=%dm",mheight,wph,mveloc,Ropt),false) + targetzone:DrawZone(coalition.side.BLUE,{0,0,1},0.2,nil,nil,3,true) + end + + local seadset = self.Groupset + local tgtcoord = targetzone:GetRandomPointVec2() + local tgtgrp = seadset:FindNearestGroupFromPointVec2(tgtcoord) + local _targetgroup = nil + local _targetgroupname = "none" + local _targetskill = "Random" + if tgtgrp and tgtgrp:IsAlive() then + _targetgroup = tgtgrp + _targetgroupname = tgtgrp:GetName() -- group name + _targetskill = tgtgrp:GetUnit(1):GetSkill() + self:T("*** Found Target = ".. _targetgroupname) + self:WakeUpShorad(_targetgroupname, self.Radius, self.ActiveTimer, Object.Category.UNIT) + end + end + end + end + return self +end + --- Main function - work on the EventData -- @param #SHORAD self -- @param Core.Event#EVENTDATA EventData The event details table data set @@ -96250,13 +102253,48 @@ do if (self:_CheckHarms(ShootingWeaponName) or self:_CheckMavs(ShootingWeaponName)) and IsDetected then -- get target data local targetdata = EventData.Weapon:getTarget() -- Identify target + -- Is there target data? + if not targetdata or self.debug then + if string.find(ShootingWeaponName,"AGM_88",1,true) then + self:I("**** Tracking AGM-88 with no target data.") + local pos0 = EventData.IniUnit:GetCoordinate() + local fheight = EventData.IniUnit:GetHeight() + self:__CalculateHitZone(20,ShootingWeapon,pos0,fheight,EventData.IniGroup) + end + return self + end + local targetcat = targetdata:getCategory() -- Identify category self:T(string.format("Target Category (3=STATIC, 1=UNIT)= %s",tostring(targetcat))) + self:T({targetdata}) local targetunit = nil if targetcat == Object.Category.UNIT then -- UNIT targetunit = UNIT:Find(targetdata) elseif targetcat == Object.Category.STATIC then -- STATIC - targetunit = STATIC:Find(targetdata) + --self:T("Static Target Data") + --self:T({targetdata:isExist()}) + --self:T({targetdata:getPoint()}) + local tgtcoord = COORDINATE:NewFromVec3(targetdata:getPoint()) + --tgtcoord:MarkToAll("Missile Target",true) + + local tgtgrp1 = self.Samset:FindNearestGroupFromPointVec2(tgtcoord) + local tgtcoord1 = tgtgrp1:GetCoordinate() + --tgtcoord1:MarkToAll("Close target SAM",true) + + local tgtgrp2 = self.Groupset:FindNearestGroupFromPointVec2(tgtcoord) + local tgtcoord2 = tgtgrp2:GetCoordinate() + --tgtcoord2:MarkToAll("Close target SHORAD",true) + + local dist1 = tgtcoord:Get2DDistance(tgtcoord1) + local dist2 = tgtcoord:Get2DDistance(tgtcoord2) + + if dist1 < dist2 then + targetunit = tgtgrp1 + targetcat = Object.Category.UNIT + else + targetunit = tgtgrp2 + targetcat = Object.Category.UNIT + end end --local targetunitname = Unit.getName(targetdata) -- Unit name if targetunit and targetunit:IsAlive() then @@ -96265,7 +102303,11 @@ do local targetgroup = nil local targetgroupname = "none" if targetcat == Object.Category.UNIT then - targetgroup = targetunit:GetGroup() + if targetunit.ClassName == "UNIT" then + targetgroup = targetunit:GetGroup() + elseif targetunit.ClassName == "GROUP" then + targetgroup = targetunit + end targetgroupname = targetgroup:GetName() -- group name elseif targetcat == Object.Category.STATIC then targetgroup = targetunit @@ -96286,6 +102328,7 @@ do end end end + return self end -- end @@ -96399,10 +102442,11 @@ AUTOLASE = { -- @field #string unitname -- @field #string reccename -- @field #string unittype +-- @field Core.Point#COORDINATE coordinate --- AUTOLASE class version. -- @field #string version -AUTOLASE.version = "0.0.9" +AUTOLASE.version = "0.0.12" ------------------------------------------------------------------- -- Begin Functional.Autolase.lua @@ -96590,13 +102634,15 @@ end -- @param #AUTOLASE self -- @return #AUTOLASE self function AUTOLASE:SetPilotMenu() - local pilottable = self.pilotset:GetSetObjects() or {} - for _,_unit in pairs (pilottable) do - local Unit = _unit -- Wrapper.Unit#UNIT - if Unit and Unit:IsAlive() then - local Group = Unit:GetGroup() - local lasemenu = MENU_GROUP_COMMAND:New(Group,"Autolase Status",nil,self.ShowStatus,self,Group) - lasemenu:Refresh() + if self.usepilotset then + local pilottable = self.pilotset:GetSetObjects() or {} + for _,_unit in pairs (pilottable) do + local Unit = _unit -- Wrapper.Unit#UNIT + if Unit and Unit:IsAlive() then + local Group = Unit:GetGroup() + local lasemenu = MENU_GROUP_COMMAND:New(Group,"Autolase Status",nil,self.ShowStatus,self,Group) + lasemenu:Refresh() + end end end return self @@ -96635,7 +102681,7 @@ function AUTOLASE:GetSmokeColor(RecceName) if self.RecceSmokeColor[RecceName] == nil then self.RecceSmokeColor[RecceName] = color else - color = self.RecceLaserCode[RecceName] + color = self.RecceSmokeColor[RecceName] end return color end @@ -96869,6 +102915,17 @@ function AUTOLASE:ShowStatus(Group) local typename = entry.unittype local code = entry.lasercode local locationstring = entry.location + local playername = Group:GetPlayerName() + if playername then + local settings = _DATABASE:GetPlayerSettings(playername) + if settings then + if settings:IsA2G_MGRS() then + locationstring = entry.coordinate:ToStringMGRS(settings) + elseif settings:IsA2G_LL_DMS() then + locationstring = entry.coordinate:ToStringLLDMS() + end + end + end local text = string.format("%s lasing %s code %d\nat %s",reccename,typename,code,locationstring) report:Add(text) lines = lines + 1 @@ -96965,25 +103022,27 @@ end function AUTOLASE:CanLase(Recce,Unit) local canlase = false -- cooldown? - local name = Recce:GetName() - local cooldown = self.RecceUnits[name].cooldown and self.forcecooldown - if cooldown then - local Tdiff = timer.getAbsTime() - self.RecceUnits[name].timestamp - if Tdiff < self.cooldowntime then - return false - else - self.RecceUnits[name].cooldown = false + if Recce and Recce:IsAlive() == true then + local name = Recce:GetName() + local cooldown = self.RecceUnits[name].cooldown and self.forcecooldown + if cooldown then + local Tdiff = timer.getAbsTime() - self.RecceUnits[name].timestamp + if Tdiff < self.cooldowntime then + return false + else + self.RecceUnits[name].cooldown = false + end + end + -- calculate LOS + local reccecoord = Recce:GetCoordinate() + local unitcoord = Unit:GetCoordinate() + local islos = reccecoord:IsLOS(unitcoord,2.5) + -- calculate distance + local distance = math.floor(reccecoord:Get3DDistance(unitcoord)) + local lasedistance = self:GetLosFromUnit(Recce) + if distance <= lasedistance and islos then + canlase = true end - end - -- calculate LOS - local reccecoord = Recce:GetCoordinate() - local unitcoord = Unit:GetCoordinate() - local islos = reccecoord:IsLOS(unitcoord,2.5) - -- calculate distance - local distance = math.floor(reccecoord:Get3DDistance(unitcoord)) - local lasedistance = self:GetLosFromUnit(Recce) - if distance <= lasedistance and islos then - canlase = true end return canlase end @@ -97027,20 +103086,22 @@ function AUTOLASE:onafterMonitor(From, Event, To) local contact = _contact -- Ops.Intelligence#INTEL.Contact local grp = contact.group local coord = contact.position - local reccename = contact.recce + local reccename = contact.recce or "none" local reccegrp = UNIT:FindByName(reccename) - local reccecoord = reccegrp:GetCoordinate() - local distance = math.floor(reccecoord:Get3DDistance(coord)) - local text = string.format("%s of %s | Distance %d km | Threatlevel %d",contact.attribute, contact.groupname, math.floor(distance/1000), contact.threatlevel) - report:Add(text) - self:T(text) - if self.debug then self:I(text) end - lines = lines + 1 - -- sort out groups beyond sight - local lasedistance = self:GetLosFromUnit(reccegrp) - if grp:IsGround() and lasedistance >= distance then - table.insert(groupsbythreat,{contact.group,contact.threatlevel}) - self.RecceNames[contact.groupname] = contact.recce + if reccegrp then + local reccecoord = reccegrp:GetCoordinate() + local distance = math.floor(reccecoord:Get3DDistance(coord)) + local text = string.format("%s of %s | Distance %d km | Threatlevel %d",contact.attribute, contact.groupname, math.floor(distance/1000), contact.threatlevel) + report:Add(text) + self:T(text) + if self.debug then self:I(text) end + lines = lines + 1 + -- sort out groups beyond sight + local lasedistance = self:GetLosFromUnit(reccegrp) + if grp:IsGround() and lasedistance >= distance then + table.insert(groupsbythreat,{contact.group,contact.threatlevel}) + self.RecceNames[contact.groupname] = contact.recce + end end end @@ -97119,7 +103180,16 @@ function AUTOLASE:onafterMonitor(From, Event, To) local code = self:GetLaserCode(reccename) local spot = SPOT:New(recce) spot:LaseOn(unit,code,self.LaseDuration) - local locationstring = unit:GetCoordinate():ToStringLLDDM() + local locationstring = unit:GetCoordinate():ToStringLLDDM() + if _SETTINGS:IsA2G_MGRS() then + local precision = _SETTINGS:GetMGRS_Accuracy() + local settings = {} + settings.MGRS_Accuracy = precision + locationstring = unit:GetCoordinate():ToStringMGRS(settings) + elseif _SETTINGS:IsA2G_LL_DMS() then + locationstring = unit:GetCoordinate():ToStringLLDMS() + end + local laserspot = { -- #AUTOLASE.LaserSpot laserspot = spot, lasedunit = unit, @@ -97130,6 +103200,7 @@ function AUTOLASE:onafterMonitor(From, Event, To) unitname = unitname, reccename = reccename, unittype = unit:GetTypeName(), + coordinate = unit:GetCoordinate(), } if self.smoketargets then local coord = unit:GetCoordinate() @@ -97246,6 +103317,986 @@ end ------------------------------------------------------------------- -- End Functional.Autolase.lua ------------------------------------------------------------------- +--- **Functional** - AI CSAR system +-- +-- ## Main Features: +-- +-- * Send out helicopters to downed pilots +-- * Rescues players and AI alike +-- * Coalition specific +-- * Starting from a FARP or Airbase +-- * Dedicated MASH zone +-- * Some FSM functions to include in your mission scripts +-- * Limit number of available helos +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/CSR-001%20-%20Basics). +-- +-- === +-- +-- ### Author: **applevangelist** +-- Last Update April 2022 +-- +-- === +-- @module Functional.AICSAR +-- @image MOOSE.JPG + + + +--- AI CSAR class. +-- @type AICSAR +-- @field #string ClassName Name of this class. +-- @field #string version Versioning. +-- @field #string lid LID for log entries. +-- @field #number coalition Colition side. +-- @field #string template Template for pilot. +-- @field #string helotemplate Template for CSAR helo. +-- @field #string alias Alias Name. +-- @field Wrapper.Airbase#AIRBASE farp FARP object from where to start. +-- @field Core.Zone#ZONE farpzone MASH zone to drop rescued pilots. +-- @field #number maxdistance Max distance to go for a rescue. +-- @field #table pilotqueue Queue of pilots to rescue. +-- @field #number pilotindex Table index to bind pilot to helo. +-- @field #table helos Table of Ops.FlightGroup#FLIGHTGROUP objects +-- @field #boolean verbose Switch more output. +-- @field #number rescuezoneradius Radius around downed pilot for the helo to land in. +-- @field #table rescued Track number of rescued pilot. +-- @field #boolean autoonoff Only send a helo when no human heli pilots are available. +-- @field Core.Set#SET_CLIENT playerset Track if alive heli pilots are available. +-- @field #boolean limithelos limit available number of helos going on mission (defaults to true) +-- @field #number helonumber number of helos available (default: 3) +-- @extends Core.Fsm#FSM + + +--- *I once donated a pint of my finest red corpuscles to the great American Red Cross and the doctor opined my blood was very helpful; contained so much alcohol they could use it to sterilize their instruments.* +-- W.C.Fields +-- +-- === +-- +-- # AICSAR Concept +-- +-- For an AI or human pilot landing with a parachute, a rescue mission will be spawned. The helicopter will fly to the pilot, pick him or her up, +-- and fly back to a designated MASH (medical) zone, drop the pilot and then return to base. +-- Operational maxdistance can be set as well as the landing radius around the downed pilot. +-- Keep in mind that AI helicopters cannot hover-load at the time of writing, so rescue operations over water or in the mountains might not +-- work. +-- Optionally, if you have a CSAR operation with human pilots in your mission, you can set AICSAR to ignore missions when human helicopter +-- pilots are around. +-- +-- ## Setup +-- +-- Setup is a one-liner: +-- +-- -- @param #string Alias Name of this instance. +-- -- @param #number Coalition Coalition as in coalition.side.BLUE, can also be passed as "blue", "red" or "neutral" +-- -- @param #string Pilottemplate Pilot template name. +-- -- @param #string Helotemplate Helicopter template name. +-- -- @param Wrapper.Airbase#AIRBASE FARP FARP object or Airbase from where to start. +-- -- @param Core.Zone#ZONE MASHZone Zone where to drop pilots after rescue. +-- local my_aicsar=AICSAR:New("Luftrettung",coalition.side.BLUE,"Downed Pilot","Rescue Helo",AIRBASE:FindByName("Test FARP"),ZONE:New("MASH")) +-- +-- ## Options are +-- +-- my_aicsar.maxdistance -- maximum operational distance in meters. Defaults to 50NM or 92.6km +-- my_aicsar.rescuezoneradius -- landing zone around downed pilot. Defaults to 200m +-- my_aicsar.autoonoff -- stop operations when human helicopter pilots are around. Defaults to true. +-- my_aicsar.verbose -- text messages to own coalition about ongoing operations. Defaults to true. +-- my_aicsarlimithelos -- limit available number of helos going on mission (defaults to true) +-- my_aicsar.helonumber -- number of helos available (default: 3) +-- +-- ## Radio options +-- +-- Radio messages, soundfile names and (for SRS) lengths are defined in three enumerators, so you can customize, localize messages and soundfiles to your liking: +-- +-- Defaults are: +-- +-- AICSAR.Messages = { +-- EN = { +-- INITIALOK = "Roger, Pilot, we hear you. Stay where you are, a helo is on the way!", +-- INITIALNOTOK = "Sorry, Pilot. You're behind maximum operational distance! Good Luck!", +-- PILOTDOWN = "Pilot down at ", -- note that this will be appended with the position +-- PILOTKIA = "Pilot KIA!", +-- HELODOWN = "CSAR Helo Down!", +-- PILOTRESCUED = "Pilot rescued!", +-- PILOTINHELO = "Pilot picked up!", +-- }, +-- } +-- +-- Correspondingly, sound file names are defined as these defaults: +-- +-- AICSAR.RadioMessages = { +-- EN = { +-- INITIALOK = "initialok.ogg", +-- INITIALNOTOK = "initialnotok.ogg", +-- PILOTDOWN = "pilotdown.ogg", +-- PILOTKIA = "pilotkia.ogg", +-- HELODOWN = "helodown.ogg", +-- PILOTRESCUED = "pilotrescued.ogg", +-- PILOTINHELO = "pilotinhelo.ogg", +-- }, +-- } +-- +-- and these default transmission lengths in seconds: +-- +-- AICSAR.RadioLength = { +-- EN = { +-- INITIALOK = 4.1, +-- INITIALNOTOK = 4.6, +-- PILOTDOWN = 2.6, +-- PILOTKIA = 1.1, +-- HELODOWN = 2.1, +-- PILOTRESCUED = 3.5, +-- PILOTINHELO = 2.6, +-- }, +-- } +-- +-- The easiest way to add a soundfile to your mission is to use the "Sound to..." trigger in the mission editor. This will effectively +-- save your sound file inside of the .miz mission file. +-- +-- To customize or localize your texts and sounds, you can take e.g. the following approach to add a German language version: +-- +-- -- parameters are: locale, ID, text, soundfilename, duration +-- my_aicsar.gettext:AddEntry("de","INITIALOK","Copy, Pilot, wir hören Sie. Bleiben Sie, wo Sie sind, ein Hubschrauber sammelt Sie auf!","okneu.ogg",5.0) +-- my_aicsar.locale = "de" -- plays and shows the defined German language texts and sound. Fallback is "en", if something is undefined. +-- +-- Switch on radio transmissions via **either** SRS **or** "normal" DCS radio e.g. like so: +-- +-- my_aicsar:SetSRSRadio(true,"C:\\Program Files\\DCS-SimpleRadio-Standalone",270,radio.modulation.AM,5002) +-- +-- or +-- +-- my_aicsar:SetDCSRadio(true,300,radio.modulation.AM,GROUP:FindByName("FARP-Radio")) +-- +-- See the function documentation for parameter details. +-- +-- === +--- +-- +-- @field #AICSAR +AICSAR = { + ClassName = "AICSAR", + version = "0.0.8", + lid = "", + coalition = coalition.side.BLUE, + template = "", + helotemplate = "", + alias = "", + farp = nil, + farpzone = nil, + maxdistance = UTILS.NMToMeters(50), + pilotqueue = {}, + pilotindex = 0, + helos = {}, + verbose = true, + rescuezoneradius = 200, + rescued = {}, + autoonoff = true, + playerset = nil, + Messages = {}, + SRS = nil, + SRSRadio = false, + SRSFrequency = 243, + SRSPath = "\\", + SRSModulation = radio.modulation.AM, + SRSSoundPath = nil, -- defaults to "l10n/DEFAULT/", i.e. add messages via "Sount to..." in the ME + SRSPort = 5002, + DCSRadio = false, + DCSFrequency = 243, + DCSModulation = radio.modulation.AM, + DCSRadioGroup = nil, + limithelos = true, + helonumber = 3, + gettext = nil, + locale ="en", -- default text language +} + +-- TODO Messages +--- Messages enum +-- @field Messages +AICSAR.Messages = { + EN = { + INITIALOK = "Roger, Pilot, we hear you. Stay where you are, a helo is on the way!", + INITIALNOTOK = "Sorry, Pilot. You're behind maximum operational distance! Good Luck!", + PILOTDOWN = "Pilot down at ", + PILOTKIA = "Pilot KIA!", + HELODOWN = "CSAR Helo Down!", + PILOTRESCUED = "Pilot rescued!", + PILOTINHELO = "Pilot picked up!", + }, + DE = { + INITIALOK = "Copy, Pilot, wir hören Sie. Bleiben Sie, wo Sie sind!\nEin Hubschrauber sammelt Sie auf!", + INITIALNOTOK = "Verstehe, Pilot. Sie sind zu weit weg von uns.\nViel Glück!", + PILOTDOWN = "Pilot abgestürzt: ", + PILOTKIA = "Pilot gefallen!", + HELODOWN = "CSAR Hubschrauber verloren!", + PILOTRESCUED = "Pilot gerettet!", + PILOTINHELO = "Pilot an Bord geholt!", + }, +} + +-- TODO Radio Messages +--- Radio Messages enum for ogg files +-- @field RadioMessages +AICSAR.RadioMessages = { + EN = { + INITIALOK = "initialok.ogg", -- 4.1 secs + INITIALNOTOK = "initialnotok.ogg", -- 4.6 secs + PILOTDOWN = "pilotdown.ogg", -- 2.6 secs + PILOTKIA = "pilotkia.ogg", -- 1.1 sec + HELODOWN = "helodown.ogg", -- 2.1 secs + PILOTRESCUED = "pilotrescued.ogg", -- 3.5 secs + PILOTINHELO = "pilotinhelo.ogg", -- 2.6 secs + }, +} + +-- TODO Radio Messages +--- Radio Messages enum for ogg files length in secs +-- @field RadioLength +AICSAR.RadioLength = { + EN = { + INITIALOK = 4.1, + INITIALNOTOK = 4.6, + PILOTDOWN = 2.6, + PILOTKIA = 1.1, + HELODOWN = 2.1, + PILOTRESCUED = 3.5, + PILOTINHELO = 2.6, + }, +} + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Function to create a new AICSAR object +-- @param #AICSAR self +-- @param #string Alias Name of this instance. +-- @param #number Coalition Coalition as in coalition.side.BLUE, can also be passed as "blue", "red" or "neutral" +-- @param #string Pilottemplate Pilot template name. +-- @param #string Helotemplate Helicopter template name. +-- @param Wrapper.Airbase#AIRBASE FARP FARP object or Airbase from where to start. +-- @param Core.Zone#ZONE MASHZone Zone where to drop pilots after rescue. +-- @return #AICSAR self +function AICSAR:New(Alias,Coalition,Pilottemplate,Helotemplate,FARP,MASHZone) + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) + + --set Coalition + if Coalition and type(Coalition)=="string" then + if Coalition=="blue" then + self.coalition=coalition.side.BLUE + self.coalitiontxt = Coalition + elseif Coalition=="red" then + self.coalition=coalition.side.RED + self.coalitiontxt = Coalition + elseif Coalition=="neutral" then + self.coalition=coalition.side.NEUTRAL + self.coalitiontxt = Coalition + else + self:E("ERROR: Unknown coalition in AICSAR!") + end + else + self.coalition = Coalition + self.coalitiontxt = string.lower(UTILS.GetCoalitionName(self.coalition)) + end + + -- Set alias. + if Alias then + self.alias=tostring(Alias) + else + self.alias="Red Cross" + if self.coalition then + if self.coalition==coalition.side.RED then + self.alias="IFRC" + elseif self.coalition==coalition.side.BLUE then + self.alias="CSAR" + end + end + end + + self.template = Pilottemplate + self.helotemplate = Helotemplate + self.farp = FARP + self.farpzone = MASHZone + self.playerset = SET_CLIENT:New():FilterActive(true):FilterCategories("helicopter"):FilterStart() + + -- Radio + self.SRS = nil + self.SRSRadio = false + self.SRSFrequency = 243 + self.SRSPath = "\\" + self.SRSModulation = radio.modulation.AM + self.SRSSoundPath = nil -- defaults to "l10n/DEFAULT/", i.e. add messages via "Sound to..." in the ME + self.SRSPort = 5002 + + -- DCS Radio - add messages via "Sound to..." in the ME + self.DCSRadio = false + self.DCSFrequency = 243 + self.DCSModulation = radio.modulation.AM + self.DCSRadioGroup = nil + self.DCSRadioQueue = nil + + self.MGRS_Accuracy = 2 + + -- limit number of available helos at the same time + self.limithelos = true + self.helonumber = 3 + + -- localization + self:InitLocalization() + + -- Set some string id for output to DCS.log file. + self.lid=string.format("%s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown") + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FSM. + self:AddTransition("*", "Status", "*") -- CSAR status update. + self:AddTransition("*", "PilotDown", "*") -- Pilot down + self:AddTransition("*", "PilotPickedUp", "*") -- Pilot in helo + self:AddTransition("*", "PilotRescued", "*") -- Pilot Rescued + self:AddTransition("*", "PilotKIA", "*") -- Pilot dead + self:AddTransition("*", "HeloDown", "*") -- Helo dead + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + self:HandleEvent(EVENTS.LandingAfterEjection) + + self:__Start(math.random(2,5)) + + local text = string.format("%sAICSAR Version %s Starting",self.lid,self.version) + + self:I(text) + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Status". + -- @function [parent=#AICSAR] Status + -- @param #AICSAR self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#AICSAR] __Status + -- @param #AICSAR self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". + -- @function [parent=#AICSAR] Stop + -- @param #AICSAR self + + --- Triggers the FSM event "Stop" after a delay. + -- @function [parent=#AICSAR] __Stop + -- @param #AICSAR self + -- @param #number delay Delay in seconds. + + --- On after "PilotDown" event. + -- @function [parent=#AICSAR] OnAfterPilotDown + -- @param #AICSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Point#COORDINATE Coordinate Location of the pilot. + -- @param #boolean InReach True if in maxdistance else false. + + --- On after "PilotPickedUp" event. + -- @function [parent=#AICSAR] OnAfterPilotPickedUp + -- @param #AICSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.FlightGroup#FLIGHTGROUP Helo + -- @param #table CargoTable of Ops.OpsGroup#OPSGROUP Cargo objects + -- @param #number Index + + --- On after "PilotRescued" event. + -- @function [parent=#AICSAR] OnAfterPilotRescued + -- @param #AICSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On after "PilotKIA" event. + -- @function [parent=#AICSAR] OnAfterPilotKIA + -- @param #AICSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On after "HeloDown" event. + -- @function [parent=#AICSAR] OnAfterHeloDown + -- @param #AICSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.FlightGroup#FLIGHTGROUP Helo + -- @param #number Index + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- [Internal] Create the Moose TextAndSoundEntries +-- @param #AICSAR self +-- @return #AICSAR self +function AICSAR:InitLocalization() + self:T(self.lid .. "InitLocalization") + -- English standard localization + self.gettext=TEXTANDSOUND:New(self.ClassName, "en") + self.gettext:AddEntry("en","INITIALOK",AICSAR.Messages.EN.INITIALOK,AICSAR.RadioMessages.EN.INITIALOK,AICSAR.RadioLength.INITIALOK) + self.gettext:AddEntry("en","INITIALNOTOK",AICSAR.Messages.EN.INITIALNOTOK,AICSAR.RadioMessages.EN.INITIALNOTOK,AICSAR.RadioLength.EN.INITIALNOTOK) + self.gettext:AddEntry("en","HELODOWN",AICSAR.Messages.EN.HELODOWN,AICSAR.RadioMessages.EN.HELODOWN,AICSAR.RadioLength.EN.HELODOWN) + self.gettext:AddEntry("en","PILOTDOWN",AICSAR.Messages.EN.PILOTDOWN,AICSAR.RadioMessages.EN.PILOTDOWN,AICSAR.RadioLength.EN.PILOTDOWN) + self.gettext:AddEntry("en","PILOTINHELO",AICSAR.Messages.EN.PILOTINHELO,AICSAR.RadioMessages.EN.PILOTINHELO,AICSAR.RadioLength.EN.PILOTINHELO) + self.gettext:AddEntry("en","PILOTKIA",AICSAR.Messages.EN.PILOTKIA,AICSAR.RadioMessages.EN.PILOTKIA,AICSAR.RadioLength.EN.PILOTKIA) + self.gettext:AddEntry("en","PILOTRESCUED",AICSAR.Messages.EN.PILOTRESCUED,AICSAR.RadioMessages.EN.PILOTRESCUED,AICSAR.RadioLength.EN.PILOTRESCUED) + -- German localization - we keep the sound files English + self.gettext:AddEntry("de","INITIALOK",AICSAR.Messages.DE.INITIALOK,AICSAR.RadioMessages.EN.INITIALOK,AICSAR.RadioLength.INITIALOK) + self.gettext:AddEntry("de","INITIALNOTOK",AICSAR.Messages.DE.INITIALNOTOK,AICSAR.RadioMessages.EN.INITIALNOTOK,AICSAR.RadioLength.EN.INITIALNOTOK) + self.gettext:AddEntry("de","HELODOWN",AICSAR.Messages.DE.HELODOWN,AICSAR.RadioMessages.EN.HELODOWN,AICSAR.RadioLength.EN.HELODOWN) + self.gettext:AddEntry("de","PILOTDOWN",AICSAR.Messages.DE.PILOTDOWN,AICSAR.RadioMessages.EN.PILOTDOWN,AICSAR.RadioLength.EN.PILOTDOWN) + self.gettext:AddEntry("de","PILOTINHELO",AICSAR.Messages.DE.PILOTINHELO,AICSAR.RadioMessages.EN.PILOTINHELO,AICSAR.RadioLength.EN.PILOTINHELO) + self.gettext:AddEntry("de","PILOTKIA",AICSAR.Messages.DE.PILOTKIA,AICSAR.RadioMessages.EN.PILOTKIA,AICSAR.RadioLength.EN.PILOTKIA) + self.gettext:AddEntry("de","PILOTRESCUED",AICSAR.Messages.DE.PILOTRESCUED,AICSAR.RadioMessages.EN.PILOTRESCUED,AICSAR.RadioLength.EN.PILOTRESCUED) + self.locale = "en" + return self +end + +--- [User] Switch sound output on and use SRS +-- @param #AICSAR self +-- @param #boolean OnOff Switch on (true) or off (false). +-- @param #string Path Path to your SRS Server Component, e.g. "E:\\\\Program Files\\\\DCS-SimpleRadio-Standalone" +-- @param #number Frequency Defaults to 243 (guard) +-- @param #number Modulation Radio modulation. Defaults to radio.modulation.AM +-- @param #string SoundPath Where to find the audio files. Defaults to nil, i.e. add messages via "Sound to..." in the Mission Editor. +-- @param #number Port Port of the SRS, defaults to 5002. +-- @return #AICSAR self +function AICSAR:SetSRSRadio(OnOff,Path,Frequency,Modulation,SoundPath,Port) + self:T(self.lid .. "SetSRSRadio") + self:T(self.lid .. "SetSRSRadio to "..tostring(OnOff)) + self.SRSRadio = OnOff and true + self.SRSFrequency = Frequency or 243 + self.SRSPath = Path or "c:\\" + self.SRSModulation = Modulation or radio.modulation.AM + local soundpath = os.getenv('TMP') .. "\\DCS\\Mission\\l10n\\DEFAULT" -- defaults to "l10n/DEFAULT/", i.e. add messages by "Sound to..." in the ME + self.SRSSoundPath = SoundPath or soundpath + self.SRSPort = Port or 5002 + if OnOff then + self.SRS = MSRS:New(Path,Frequency,Modulation) + self.SRS:SetPort(self.SRSPort) + end + return self +end + +--- [User] Switch sound output on and use normale (DCS) radio +-- @param #AICSAR self +-- @param #boolean OnOff Switch on (true) or off (false). +-- @param #number Frequency Defaults to 243 (guard). +-- @param #number Modulation Radio modulation. Defaults to radio.modulation.AM. +-- @param Wrapper.Group#GROUP Group The group to use as sending station. +-- @return #AICSAR self +function AICSAR:SetDCSRadio(OnOff,Frequency,Modulation,Group) + self:T(self.lid .. "SetDCSRadio") + self:T(self.lid .. "SetDCSRadio to "..tostring(OnOff)) + self.DCSRadio = OnOff and true + self.DCSFrequency = Frequency or 243 + self.DCSModulation = Modulation or radio.modulation.AM + self.DCSRadioGroup = Group + if self.DCSRadio then + self.DCSRadioQueue = RADIOQUEUE:New(Frequency,Modulation,"AI-CSAR") + self.DCSRadioQueue:Start(5,5) + self.DCSRadioQueue:SetRadioPower(1000) + self.DCSRadioQueue:SetSenderCoordinate(Group:GetCoordinate()) + else + if self.DCSRadioQueue then + self.DCSRadioQueue:Stop() + end + end + return self +end + +--- [Internal] Sound output via non-SRS Radio. Add message files (.ogg) via "Sound to..." in the ME. +-- @param #AICSAR self +-- @param #string Soundfile Name of the soundfile +-- @param #number Duration Duration of the sound +-- @param #string Subtitle Text to display +-- @return #AICSAR self +function AICSAR:DCSRadioBroadcast(Soundfile,Duration,Subtitle) + self:T(self.lid .. "DCSRadioBroadcast") + local radioqueue = self.DCSRadioQueue -- Sound.RadioQueue#RADIOQUEUE + radioqueue:NewTransmission(Soundfile,Duration,nil,2,nil,Subtitle,10) + return self +end + +--- [Internal] Catch the landing after ejection and spawn a pilot in situ. +-- @param #AICSAR self +-- @param Core.Event#EVENTDATA EventData +-- @return #AICSAR self +function AICSAR:OnEventLandingAfterEjection(EventData) + self:T(self.lid .. "OnEventLandingAfterEjection ID=" .. EventData.id) + + -- autorescue on off? + if self.autoonoff then + if self.playerset:CountAlive() > 0 then + return self + end + end + + local _event = EventData -- Core.Event#EVENTDATA + -- get position and spawn in a template pilot + local _LandingPos = COORDINATE:NewFromVec3(_event.initiator:getPosition().p) + local _country = _event.initiator:getCountry() + local _coalition = coalition.getCountryCoalition( _country ) + + -- DONE: add distance check + local distancetofarp = _LandingPos:Get2DDistance(self.farp:GetCoordinate()) + + -- Mayday Message + local Text,Soundfile,Soundlength,Subtitle = self.gettext:GetEntry("PILOTDOWN",self.locale) + local text = "" + if _coalition == self.coalition then + if self.verbose then + local setting = {} + setting.MGRS_Accuracy = self.MGRS_Accuracy + local location = _LandingPos:ToStringMGRS(setting) + text = Text .. location .. "!" + MESSAGE:New(text,15,"AICSAR"):ToCoalition(self.coalition) + end + if self.SRSRadio then + local sound = SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength) + sound:SetPlayWithSRS(true) + self.SRS:PlaySoundFile(sound,2) + elseif self.DCSRadio then + self:DCSRadioBroadcast(Soundfile,Soundlength,text) + end + end + + -- further processing + if _coalition == self.coalition and distancetofarp <= self.maxdistance then + -- in reach + self:T(self.lid .. "Spawning new Pilot") + self.pilotindex = self.pilotindex + 1 + local newpilot = SPAWN:NewWithAlias(self.template,string.format("%s-AICSAR-%d",self.template, self.pilotindex)) + newpilot:InitDelayOff() + newpilot:OnSpawnGroup( + function (grp) + self.pilotqueue[self.pilotindex] = grp + end + ) + newpilot:SpawnFromCoordinate(_LandingPos) + + Unit.destroy(_event.initiator) -- shagrat remove static Pilot model + self:__PilotDown(2,_LandingPos,true) + elseif _coalition == self.coalition and distancetofarp > self.maxdistance then + -- out of reach, apologies, too far off + self:T(self.lid .. "Pilot out of reach") + self:__PilotDown(2,_LandingPos,false) + end + return self +end + +--- [Internal] Get FlightGroup +-- @param #AICSAR self +-- @return Ops.FlightGroup#FLIGHTGROUP The FlightGroup +function AICSAR:_GetFlight() + self:T(self.lid .. "_GetFlight") + -- Helo Carrier. + local newhelo = SPAWN:NewWithAlias(self.helotemplate,self.helotemplate..math.random(1,10000)) + :InitDelayOff() + :InitUnControlled(true) + :Spawn() + + local nhelo=FLIGHTGROUP:New(newhelo) + nhelo:SetHomebase(self.farp) + nhelo:Activate() + return nhelo +end + +--- [Internal] Create a new rescue mission +-- @param #AICSAR self +-- @param Wrapper.Group#GROUP Pilot The pilot to be rescued. +-- @param #number Index Index number of this pilot +-- @return #AICSAR self +function AICSAR:_InitMission(Pilot,Index) + self:T(self.lid .. "_InitMission") + + local pickupzone = ZONE_GROUP:New(Pilot:GetName(),Pilot,self.rescuezoneradius) + --local pilotset = SET_GROUP:New() + --pilotset:AddGroup(Pilot) + + -- Cargo transport assignment. + local opstransport=OPSTRANSPORT:New(Pilot, pickupzone, self.farpzone) + + local helo = self:_GetFlight() + -- inject reservation + helo.AICSARReserved = true + + -- Cargo transport assignment to first Huey group. + helo:AddOpsTransport(opstransport) + + -- callback functions + local function AICPickedUp(Helo,Cargo,Index) + self:__PilotPickedUp(2,Helo,Cargo,Index) + end + + local function AICHeloDead(Helo,Index) + self:__HeloDown(2,Helo,Index) + end + + function helo:OnAfterLoadingDone(From,Event,To) + AICPickedUp(helo,helo:GetCargoGroups(),Index) + end + + function helo:OnAfterDead(From,Event,To) + AICHeloDead(helo,Index) + end + + self.helos[Index] = helo + + return self +end + +--- [Internal] Check if pilot arrived in rescue zone (MASH) +-- @param #AICSAR self +-- @param Wrapper.Group#GROUP Pilot The pilot to be rescued. +-- @return #boolean outcome +function AICSAR:_CheckInMashZone(Pilot) + self:T(self.lid .. "_CheckQueue") + if Pilot:IsInZone(self.farpzone) then + return true + else + return false + end +end + +--- [Internal] Check helo queue +-- @param #AICSAR self +-- @return #AICSAR self +function AICSAR:_CheckHelos() + self:T(self.lid .. "_CheckHelos") + for _index,_helo in pairs(self.helos) do + local helo = _helo -- Ops.FlightGroup#FLIGHTGROUP + if helo and helo.ClassName == "FLIGHTGROUP" then + local state = helo:GetState() + local name = helo:GetName() + self:T("Helo group "..name.." in state "..state) + if state == "Arrived" then + helo:__Stop(5) + self.helos[_index] = nil + end + else + self.helos[_index] = nil + end + end + return self +end + +--- [Internal] Count helos queue +-- @param #AICSAR self +-- @return #number Number of helos on mission +function AICSAR:_CountHelos() + self:T(self.lid .. "_CountHelos") + local count = 0 + for _index,_helo in pairs(self.helos) do + count = count + 1 + end + return count +end + +--- [Internal] Check pilot queue for next mission +-- @param #AICSAR self +-- @return #AICSAR self +function AICSAR:_CheckQueue() + self:T(self.lid .. "_CheckQueue") + for _index, _pilot in pairs(self.pilotqueue) do + local classname = _pilot.ClassName and _pilot.ClassName or "NONE" + local name = _pilot.GroupName and _pilot.GroupName or "NONE" + local helocount = self:_CountHelos() + --self:T("Looking at " .. classname .. " " .. name) + -- find one w/o mission + if _pilot and _pilot.ClassName and _pilot.ClassName == "GROUP" then + local flightgroup = self.helos[_index] -- Ops.FlightGroup#FLIGHTGROUP + -- rescued? + if self:_CheckInMashZone(_pilot) then + self:T("Pilot" .. _pilot.GroupName .. " rescued!") + _pilot:Destroy(false) + self.pilotqueue[_index] = nil + self.rescued[_index] = true + self:__PilotRescued(2) + if flightgroup then + flightgroup.AICSARReserved = false + end + end -- end rescued + -- has no mission assigned? + if not _pilot.AICSAR then + -- helo available? + if self.limithelos and helocount >= self.helonumber then + -- none free + break + end -- end limit + _pilot.AICSAR = {} + _pilot.AICSAR.Status = "Initiated" + _pilot.AICSAR.Boarded = false + self:_InitMission(_pilot,_index) + break + else + -- update status from OPSGROUP + if flightgroup then + local state = flightgroup:GetState() + _pilot.AICSAR.Status = state + end + --self:T("Flight for " .. _pilot.GroupName .. " in state " .. state) + end -- end has mission + end -- end if pilot + end -- end loop + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- [Internal] onafterStart +-- @param #AICSAR self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AICSAR self +function AICSAR:onafterStart(From, Event, To) + self:T({From, Event, To}) + self:__Status(3) + return self +end + +--- [Internal] onafterStatus +-- @param #AICSAR self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AICSAR self +function AICSAR:onafterStatus(From, Event, To) + self:T({From, Event, To}) + self:_CheckQueue() + self:_CheckHelos() + self:__Status(30) + return self +end + +--- [Internal] onafterStop +-- @param #AICSAR self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AICSAR self +function AICSAR:onafterStop(From, Event, To) + self:T({From, Event, To}) + self:UnHandleEvent(EVENTS.LandingAfterEjection) + if self.DCSRadioQueue then + self.DCSRadioQueue:Stop() + end + return self +end + +--- [Internal] onafterPilotDown +-- @param #AICSAR self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Core.Point#COORDINATE Coordinate Location of the pilot. +-- @param #boolean InReach True if in maxdistance else false. +-- @return #AICSAR self +function AICSAR:onafterPilotDown(From, Event, To, Coordinate, InReach) + self:T({From, Event, To}) + local CoordinateText = Coordinate:ToStringMGRS() + local inreach = tostring(InReach) + --local text = string.format("Pilot down at %s. In reach = %s",CoordinateText,inreach) + if InReach then + local text,Soundfile,Soundlength,Subtitle = self.gettext:GetEntry("INITIALOK",self.locale) + --local text = AICSAR.Messages.EN.INITIALOK + self:T(text) + if self.verbose then + MESSAGE:New(text,15,"AICSAR"):ToCoalition(self.coalition) + end + if self.SRSRadio then + local sound = SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength) + sound:SetPlayWithSRS(true) + self.SRS:PlaySoundFile(sound,2) + elseif self.DCSRadio then + self:DCSRadioBroadcast(Soundfile,Soundlength,text) + end + else + local text,Soundfile,Soundlength,Subtitle = self.gettext:GetEntry("INITIALNOTOK",self.locale) + --local text = AICSAR.Messages.EN.INITIALNOTOK + self:T(text) + if self.verbose then + MESSAGE:New(text,15,"AICSAR"):ToCoalition(self.coalition) + end + if self.SRSRadio then + local sound = SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength) + sound:SetPlayWithSRS(true) + self.SRS:PlaySoundFile(sound,2) + elseif self.DCSRadio then + self:DCSRadioBroadcast(Soundfile,Soundlength,text) + end + end + return self +end + +--- [Internal] onafterPilotKIA +-- @param #AICSAR self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AICSAR self +function AICSAR:onafterPilotKIA(From, Event, To) + self:T({From, Event, To}) + local text,Soundfile,Soundlength,Subtitle = self.gettext:GetEntry("PILOTKIA",self.locale) + if self.verbose then + MESSAGE:New(text,15,"AICSAR"):ToCoalition(self.coalition) + end + if self.SRSRadio then + local sound = SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength) + sound:SetPlayWithSRS(true) + self.SRS:PlaySoundFile(sound,2) + elseif self.DCSRadio then + self:DCSRadioBroadcast(Soundfile,Soundlength,text) + end + return self +end + +--- [Internal] onafterHeloDown +-- @param #AICSAR self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.FlightGroup#FLIGHTGROUP Helo +-- @param #number Index +-- @return #AICSAR self +function AICSAR:onafterHeloDown(From, Event, To, Helo, Index) + self:T({From, Event, To}) + local text,Soundfile,Soundlength,Subtitle = self.gettext:GetEntry("HELODOWN",self.locale) + if self.verbose then + MESSAGE:New(text,15,"AICSAR"):ToCoalition(self.coalition) + end + if self.SRSRadio then + local sound = SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength) + sound:SetPlayWithSRS(true) + self.SRS:PlaySoundFile(sound,2) + elseif self.DCSRadio then + self:DCSRadioBroadcast(Soundfile,Soundlength,text) + end + local findex = 0 + local fhname = Helo:GetName() + -- find index of Helo + if Index and Index > 0 then + findex=Index + else + for _index, _helo in pairs(self.helos) do + local helo = _helo -- Ops.FlightGroup#FLIGHTGROUP + local hname = helo:GetName() + if fhname == hname then + findex = _index + break + end + end + end + -- find pilot + if findex > 0 and not self.rescued[findex] then + local pilot = self.pilotqueue[findex] + self.helos[findex] = nil + if pilot.AICSAR.Boarded then + self:T("Helo Down: Found DEAD Pilot ID " .. findex .. " with name " .. pilot:GetName()) + -- pilot also dead + self:__PilotKIA(2) + self.pilotqueue[findex] = nil + else + -- initiate new mission + self:T("Helo Down: Found ALIVE Pilot ID " .. findex .. " with name " .. pilot:GetName()) + self:_InitMission(pilot,findex) + end + end + return self +end + +--- [Internal] onafterPilotRescued +-- @param #AICSAR self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AICSAR self +function AICSAR:onafterPilotRescued(From, Event, To) + self:T({From, Event, To}) + local text,Soundfile,Soundlength,Subtitle = self.gettext:GetEntry("PILOTRESCUED",self.locale) + if self.verbose then + MESSAGE:New(text,15,"AICSAR"):ToCoalition(self.coalition) + end + if self.SRSRadio then + local sound = SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength) + sound:SetPlayWithSRS(true) + self.SRS:PlaySoundFile(sound,2) + elseif self.DCSRadio then + self:DCSRadioBroadcast(Soundfile,Soundlength,text) + end + return self +end + +--- [Internal] onafterPilotPickedUp +-- @param #AICSAR self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.FlightGroup#FLIGHTGROUP Helo +-- @param #table CargoTable of Ops.OpsGroup#OPSGROUP Cargo objects +-- @param #number Index +-- @return #AICSAR self +function AICSAR:onafterPilotPickedUp(From, Event, To, Helo, CargoTable, Index) + self:T({From, Event, To}) + local text,Soundfile,Soundlength,Subtitle = self.gettext:GetEntry("PILOTINHELO",self.locale) + if self.verbose then + MESSAGE:New(text,15,"AICSAR"):ToCoalition(self.coalition) + end + if self.SRSRadio then + local sound = SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength) + sound:SetPlayWithSRS(true) + self.SRS:PlaySoundFile(sound,2) + elseif self.DCSRadio then + self:DCSRadioBroadcast(Soundfile,Soundlength,text) + end + local findex = 0 + local fhname = Helo:GetName() + if Index and Index > 0 then + findex = Index + else + -- find index of Helo + for _index, _helo in pairs(self.helos) do + local helo = _helo -- Ops.FlightGroup#FLIGHTGROUP + local hname = helo:GetName() + if fhname == hname then + findex = _index + break + end + end + end + -- find pilot + if findex > 0 then + local pilot = self.pilotqueue[findex] + self:T("Boarded: Found Pilot ID " .. findex .. " with name " .. pilot:GetName()) + pilot.AICSAR.Boarded = true -- mark as boarded + end + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- END AICSAR +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- **Ops** - Manages aircraft CASE X recoveries for carrier operations (X=I, II, III). -- -- The AIRBOSS class manages recoveries of human pilots and AI aircraft on aircraft carriers. @@ -97275,14 +104326,16 @@ end -- **Supported Carriers:** -- -- * [USS John C. Stennis](https://en.wikipedia.org/wiki/USS_John_C._Stennis) (CVN-74) --- * [USS Theodore Roosevelt](https://en.wikipedia.org/wiki/USS_Theodore_Roosevelt_(CVN-71)) (CVN-71) [Super Carrier Module] --- * [USS Abraham Lincoln](https://en.wikipedia.org/wiki/USS_Abraham_Lincoln_(CVN-72)) (CVN-72) [Super Carrier Module] --- * [USS George Washington](https://en.wikipedia.org/wiki/USS_George_Washington_(CVN-73)) (CVN-73) [Super Carrier Module] --- * [USS Harry S. Truman](https://en.wikipedia.org/wiki/USS_Harry_S._Truman) (CVN-75) [Super Carrier Module] --- * [USS Forrestal](https://en.wikipedia.org/wiki/USS_Forrestal_(CV-59)) (CV-59) [Heatblur Carrier Module] +-- * [USS Theodore Roosevelt](https://en.wikipedia.org/wiki/USS_Theodore_Roosevelt_(CVN-71)) (CVN-71) [Super Carrier Module] +-- * [USS Abraham Lincoln](https://en.wikipedia.org/wiki/USS_Abraham_Lincoln_(CVN-72)) (CVN-72) [Super Carrier Module] +-- * [USS George Washington](https://en.wikipedia.org/wiki/USS_George_Washington_(CVN-73)) (CVN-73) [Super Carrier Module] +-- * [USS Harry S. Truman](https://en.wikipedia.org/wiki/USS_Harry_S._Truman) (CVN-75) [Super Carrier Module] +-- * [USS Forrestal](https://en.wikipedia.org/wiki/USS_Forrestal_(CV-59)) (CV-59) [Heatblur Carrier Module] +-- * [HMS Hermes](https://en.wikipedia.org/wiki/HMS_Hermes_(R12)) (R12) [**WIP**] -- * [USS Tarawa](https://en.wikipedia.org/wiki/USS_Tarawa_(LHA-1)) (LHA-1) [**WIP**] -- * [USS America](https://en.wikipedia.org/wiki/USS_America_(LHA-6)) (LHA-6) [**WIP**] -- * [Juan Carlos I](https://en.wikipedia.org/wiki/Spanish_amphibious_assault_ship_Juan_Carlos_I) (L61) [**WIP**] +-- * [HMAS Canberra](https://en.wikipedia.org/wiki/HMAS_Canberra_(L02)) (L02) [**WIP**] -- -- **Supported Aircraft:** -- @@ -97299,9 +104352,9 @@ end -- -- At the moment, optimized parameters are available for the F/A-18C Hornet (Lot 20) and A-4E community mod as aircraft and the USS John C. Stennis as carrier. -- --- The AV-8B Harrier, the USS Tarawa, USS America and Juan Carlos I are WIP. The AV-8B harrier and the LHA's and LHD can only be used together, i.e. these ships are the only carriers the harrier is supposed to land on and +-- The AV-8B Harrier, HMS Hermes, the USS Tarawa, USS America, HMAS Canberra, and Juan Carlos I are WIP. The AV-8B harrier and the LHA's and LHD can only be used together, i.e. these ships are the only carriers the harrier is supposed to land on and -- no other fixed wing aircraft (human or AI controlled) are supposed to land on these ships. Currently only Case I is supported. Case II/III take slightly different steps from the CVN carrier. --- However, the two Case II/III pattern are very similar so this is not a big drawback. +-- However, if no offset is used for the holding radial this provides a very close representation of the V/STOL Case III, allowing for an approach to over the deck and a vertical landing. -- -- Heatblur's mighty F-14B Tomcat has been added (March 13th 2019) as well. Same goes for the A version. -- @@ -97356,14 +104409,16 @@ end -- * [DCS: F/A-18C Hornet – Episode 16: CASE III Introduction](https://www.youtube.com/watch?v=DvlMHnLjbDQ) -- * [DCS: F/A-18C Hornet Case I Carrier Landing Training Lesson Recording](https://www.youtube.com/watch?v=D33uM9q4xgA) -- --- ### AV-8B Harrier at USS Tarawa +-- ### AV-8B Harrier and V/STOL Operations: -- -- * [Harrier Ship Landing Mission with Auto LSO!](https://www.youtube.com/watch?v=lqmVvpunk2c) +-- * [Updated Airboss V/STOL Features USS Tarawa](https://youtu.be/K7I4pU6j718) -- * [Harrier Practice pattern USS America](https://youtu.be/99NigITYmcI) +-- * [Harrier CASE III TACAN Approach USS Tarawa](https://www.youtube.com/watch?v=wWHag5WpNZ0) -- -- === -- --- ### Author: **funkyfranky** +-- ### Author: **funkyfranky** LHA and LHD V/STOL additions by **Pene** -- ### Special Thanks To **Bankler** -- For his great [Recovery Trainer](https://forums.eagle.ru/showthread.php?t=221412) mission and script! -- His work was the initial inspiration for this class. Also note that this implementation uses some routines for determining the player position in Case I recoveries he developed. @@ -97548,8 +104603,7 @@ end -- -- Once the aircraft reaches the Initial, the landing pattern begins. The important steps of the pattern are shown in the image above. -- The AV-8B Harrier pattern is very similar, the only differences are as there is no angled deck there is no wake check. from the ninety you wil fly a straight approach offset 26 ft to port (left) of the tram line. --- The aim is to arrive abeam the landing spot in a stable hover at 120 ft with forward speed matched to the boat. From there the LSO will call "cleared to land". You then level cross to the tram line at the designated landing spot at land vertcally. --- +-- The aim is to arrive abeam the landing spot in a stable hover at 120 ft with forward speed matched to the boat. From there the LSO will call "cleared to land". You then level cross to the tram line at the designated landing spot at land vertcally. When you stabalise over the landing spot LSO will call Stabalise to indicate you are centered at the correct spot. -- -- ## CASE III -- @@ -97882,12 +104936,12 @@ end -- -- Furthermore, we have the cases: -- --- * 2.5 Points **B**: "Bolder", when the player landed but did not catch a wire. +-- * 2.5 Points **B**: "Bolter", when the player landed but did not catch a wire. -- * 2.0 Points **WOP**: "Pattern Wave-Off", when pilot was far away from where he should be in the pattern. -- * 2.0 Points **OWO**: "Own Wave-Off**, when pilot flies past the deck without touching it. -- * 1.0 Points **WO**: "Technique Wave-Off": Player got waved off in the final parts of the groove. -- * 1.0 Points **LIG**: "Long In the Groove", when pilot extents the downwind leg too far and screws up the timing for the following aircraft. --- * 0.0 Points **CUT**: "Cut pass", when player was waved off but landed anyway. +-- * 0.0 Points **CUT**: "Cut pass", when player was waved off but landed anyway. In addition if a V/STOL lands without having been Cleared to Land. -- -- ## Foul Deck Waveoff -- @@ -98112,7 +105166,7 @@ end -- * *Points*: Current points for the pass. -- * *Details*: Detailed grading analysis. -- ---## Lineup Error +-- ## Lineup Error -- -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_TrapSheetLUE.png) -- @@ -98205,8 +105259,6 @@ end -- -- Again, changing the file name, subtitle, subtitle duration is not required if you name the file exactly like the original one, which is this case would be "LSO-RogerBall.ogg". -- --- --- -- ## The Radio Dilemma -- -- DCS offers two (actually three) ways to send radio messages. Each one has its advantages and disadvantages and it is important to understand the differences. @@ -98448,6 +105500,8 @@ AIRBOSS = { NmaxSection = nil, NmaxStack = nil, handleai = nil, + xtVoiceOvers = nil, + xtVoiceOversAI = nil, tanker = nil, Corientation = nil, Corientlast = nil, @@ -98510,7 +105564,7 @@ AIRBOSS = { --- Aircraft types capable of landing on carrier (human+AI). -- @type AIRBOSS.AircraftCarrier --- @field #string AV8B AV-8B Night Harrier. Works only with the USS Tarawa, USS America and Juan Carlos I. +-- @field #string AV8B AV-8B Night Harrier. Works only with the HMS Hermes, USS Tarawa, USS America, and Juan Carlos I. -- @field #string A4EC A-4E Community mod. -- @field #string HORNET F/A-18C Lot 20 Hornet by Eagle Dynamics. -- @field #string F14A F-14A by Heatblur. @@ -98546,22 +105600,26 @@ AIRBOSS.AircraftCarrier={ -- @field #string TRUMAN USS Harry S. Truman (CVN-75) [Super Carrier Module] -- @field #string FORRESTAL USS Forrestal (CV-59) [Heatblur Carrier Module] -- @field #string VINSON USS Carl Vinson (CVN-70) [Obsolete] --- @field #string TARAWA USS Tarawa (LHA-1) --- @field #string AMERICA USS America (LHA-6) --- @field #string JCARLOS Juan Carlos I (L61) +-- @field #string HERMES HMS Hermes (R12) [V/STOL Carrier] +-- @field #string TARAWA USS Tarawa (LHA-1) [V/STOL Carrier] +-- @field #string AMERICA USS America (LHA-6) [V/STOL Carrier] +-- @field #string JCARLOS Juan Carlos I (L61) [V/STOL Carrier] +-- @field #string HMAS Canberra (L02) [V/STOL Carrier] -- @field #string KUZNETSOV Admiral Kuznetsov (CV 1143.5) -AIRBOSS.CarrierType={ - ROOSEVELT="CVN_71", - LINCOLN="CVN_72", - WASHINGTON="CVN_73", - TRUMAN="CVN_75", - STENNIS="Stennis", - FORRESTAL="Forrestal", - VINSON="VINSON", - TARAWA="LHA_Tarawa", - AMERICA="USS America LHA-6", - JCARLOS="L61", - KUZNETSOV="KUZNECOW", +AIRBOSS.CarrierType = { + ROOSEVELT = "CVN_71", + LINCOLN = "CVN_72", + WASHINGTON = "CVN_73", + TRUMAN = "CVN_75", + STENNIS = "Stennis", + FORRESTAL = "Forrestal", + VINSON = "VINSON", + HERMES = "HERMES81", + TARAWA = "LHA_Tarawa", + AMERICA = "USS America LHA-6", + JCARLOS = "L61", + CANBERRA = "L02", + KUZNETSOV = "KUZNECOW", } --- Carrier specific parameters. @@ -98609,7 +105667,6 @@ AIRBOSS.CarrierType={ -- @field #number LEFT LUR threshold. Default -3.0 deg. -- @field #number RIGHT LUL threshold. Default 3.0 deg. - --- Pattern steps. -- @type AIRBOSS.PatternStep -- @field #string UNDEFINED "Undefined". @@ -98641,36 +105698,36 @@ AIRBOSS.CarrierType={ -- @field #string BOLTER "Bolter Pattern". -- @field #string EMERGENCY "Emergency Landing". -- @field #string DEBRIEF "Debrief". -AIRBOSS.PatternStep={ - UNDEFINED="Undefined", - REFUELING="Refueling", - SPINNING="Spinning", - COMMENCING="Commencing", - HOLDING="Holding", - WAITING="Waiting for free Marshal stack", - PLATFORM="Platform", - ARCIN="Arc Turn In", - ARCOUT="Arc Turn Out", - DIRTYUP="Dirty Up", - BULLSEYE="Bullseye", - INITIAL="Initial", - BREAKENTRY="Break Entry", - EARLYBREAK="Early Break", - LATEBREAK="Late Break", - ABEAM="Abeam", - NINETY="Ninety", - WAKE="Wake", - FINAL="Turn Final", - GROOVE_XX="Groove X", - GROOVE_IM="Groove In the Middle", - GROOVE_IC="Groove In Close", - GROOVE_AR="Groove At the Ramp", - GROOVE_IW="Groove In the Wires", - GROOVE_AL="Groove Abeam Landing Spot", - GROOVE_LC="Groove Level Cross", - BOLTER="Bolter Pattern", - EMERGENCY="Emergency Landing", - DEBRIEF="Debrief", +AIRBOSS.PatternStep = { + UNDEFINED = "Undefined", + REFUELING = "Refueling", + SPINNING = "Spinning", + COMMENCING = "Commencing", + HOLDING = "Holding", + WAITING = "Waiting for free Marshal stack", + PLATFORM = "Platform", + ARCIN = "Arc Turn In", + ARCOUT = "Arc Turn Out", + DIRTYUP = "Dirty Up", + BULLSEYE = "Bullseye", + INITIAL = "Initial", + BREAKENTRY = "Break Entry", + EARLYBREAK = "Early Break", + LATEBREAK = "Late Break", + ABEAM = "Abeam", + NINETY = "Ninety", + WAKE = "Wake", + FINAL = "Turn Final", + GROOVE_XX = "Groove X", + GROOVE_IM = "Groove In the Middle", + GROOVE_IC = "Groove In Close", + GROOVE_AR = "Groove At the Ramp", + GROOVE_IW = "Groove In the Wires", + GROOVE_AL = "Groove Abeam Landing Spot", + GROOVE_LC = "Groove Level Cross", + BOLTER = "Bolter Pattern", + EMERGENCY = "Emergency Landing", + DEBRIEF = "Debrief", } --- Groove position. @@ -98683,15 +105740,15 @@ AIRBOSS.PatternStep={ -- @field #string AL "AL": Abeam landing position (V/STOL). -- @field #string LC "LC": Level crossing (V/STOL). -- @field #string IW "IW": In the wires. -AIRBOSS.GroovePos={ - X0="X0", - XX="XX", - IM="IM", - IC="IC", - AR="AR", - AL="AL", - LC="LC", - IW="IW", +AIRBOSS.GroovePos = { + X0 = "X0", + XX = "XX", + IM = "IM", + IC = "IC", + AR = "AR", + AL = "AL", + LC = "LC", + IW = "IW", } --- Radio. @@ -98820,16 +105877,15 @@ AIRBOSS.GroovePos={ -- @field #AIRBOSS.RadioCall CLICK Radio end transmission click sound. -- @field #AIRBOSS.RadioCall NOISE Static noise sound. - --- Difficulty level. -- @type AIRBOSS.Difficulty -- @field #string EASY Flight Student. Shows tips and hints in important phases of the approach. -- @field #string NORMAL Naval aviator. Moderate number of hints but not really zip lip. -- @field #string HARD TOPGUN graduate. For people who know what they are doing. Nearly *ziplip*. -AIRBOSS.Difficulty={ - EASY="Flight Student", - NORMAL="Naval Aviator", - HARD="TOPGUN Graduate", +AIRBOSS.Difficulty = { + EASY = "Flight Student", + NORMAL = "Naval Aviator", + HARD = "TOPGUN Graduate", } --- Recovery window parameters. @@ -98966,16 +106022,15 @@ AIRBOSS.Difficulty={ --- Main group level radio menu: F10 Other/Airboss. -- @field #table MenuF10 -AIRBOSS.MenuF10={} +AIRBOSS.MenuF10 = {} --- Airboss mission level F10 root menu. -- @field #table MenuF10Root -AIRBOSS.MenuF10Root=nil +AIRBOSS.MenuF10Root = nil --- Airboss class version. -- @field #string version -AIRBOSS.version="1.2.0" - +AIRBOSS.version = "1.2.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -99048,56 +106103,56 @@ AIRBOSS.version="1.2.0" -- @param carriername Name of the aircraft carrier unit as defined in the mission editor. -- @param alias (Optional) Alias for the carrier. This will be used for radio messages and the F10 radius menu. Default is the carrier name as defined in the mission editor. -- @return #AIRBOSS self or nil if carrier unit does not exist. -function AIRBOSS:New(carriername, alias) +function AIRBOSS:New( carriername, alias ) -- Inherit everthing from FSM class. - local self=BASE:Inherit(self, FSM:New()) -- #AIRBOSS + local self = BASE:Inherit( self, FSM:New() ) -- #AIRBOSS -- Debug. - self:F2({carriername=carriername, alias=alias}) + self:F2( { carriername = carriername, alias = alias } ) -- Set carrier unit. - self.carrier=UNIT:FindByName(carriername) + self.carrier = UNIT:FindByName( carriername ) -- Check if carrier unit exists. - if self.carrier==nil then + if self.carrier == nil then -- Error message. - local text=string.format("ERROR: Carrier unit %s could not be found! Make sure this UNIT is defined in the mission editor and check the spelling of the unit name carefully.", carriername) - MESSAGE:New(text, 120):ToAll() - self:E(text) + local text = string.format( "ERROR: Carrier unit %s could not be found! Make sure this UNIT is defined in the mission editor and check the spelling of the unit name carefully.", carriername ) + MESSAGE:New( text, 120 ):ToAll() + self:E( text ) return nil end -- Set some string id for output to DCS.log file. - self.lid=string.format("AIRBOSS %s | ", carriername) + self.lid = string.format( "AIRBOSS %s | ", carriername ) -- Current map. - self.theatre=env.mission.theatre - self:T2(self.lid..string.format("Theatre = %s.", tostring(self.theatre))) + self.theatre = env.mission.theatre + self:T2( self.lid .. string.format( "Theatre = %s.", tostring( self.theatre ) ) ) -- Get carrier type. - self.carriertype=self.carrier:GetTypeName() + self.carriertype = self.carrier:GetTypeName() -- Set alias. - self.alias=alias or carriername + self.alias = alias or carriername -- Set carrier airbase object. - self.airbase=AIRBASE:FindByName(carriername) + self.airbase = AIRBASE:FindByName( carriername ) -- Create carrier beacon. - self.beacon=BEACON:New(self.carrier) + self.beacon = BEACON:New( self.carrier ) -- Set Tower Frequency of carrier. self:_GetTowerFrequency() -- Init player scores table. - self.playerscores={} + self.playerscores = {} -- Initialize ME waypoints. self:_InitWaypoints() -- Current waypoint. - self.currentwp=1 + self.currentwp = 1 -- Patrol route. self:_PatrolRoute() @@ -99116,7 +106171,7 @@ function AIRBOSS:New(carriername, alias) self:SetLSOCallInterval() -- Radio scheduler. - self.radiotimer=SCHEDULER:New() + self.radiotimer = SCHEDULER:New() -- Set magnetic declination. self:SetMagneticDeclination() @@ -99145,6 +106200,12 @@ function AIRBOSS:New(carriername, alias) -- Set AI handling On. self:SetHandleAION() + -- No extra voiceover/calls from player by default + self:SetExtraVoiceOvers(false) + + -- No extra voiceover/calls from AI by default + self:SetExtraVoiceOversAI(false) + -- Airboss is a nice guy. self:SetAirbossNiceGuy() @@ -99152,10 +106213,10 @@ function AIRBOSS:New(carriername, alias) self:SetEmergencyLandings() -- No despawn after engine shutdown by default. - self:SetDespawnOnEngineShutdown(false) + self:SetDespawnOnEngineShutdown( false ) -- No respawning of AI groups when entering the CCA. - self:SetRespawnAI(false) + self:SetRespawnAI( false ) -- Mission uses static weather by default. self:SetStaticWeather() @@ -99176,7 +106237,7 @@ function AIRBOSS:New(carriername, alias) self:SetInitialMaxAlt() -- Default player skill EASY. - self:SetDefaultPlayerSkill(AIRBOSS.Difficulty.EASY) + self:SetDefaultPlayerSkill( AIRBOSS.Difficulty.EASY ) -- Default glideslope error thresholds. self:SetGlideslopeErrorThresholds() @@ -99191,7 +106252,7 @@ function AIRBOSS:New(carriername, alias) self:SetCarrierControlledZone() -- Carrier patrols its waypoints until the end of time. - self:SetPatrolAdInfinitum(true) + self:SetPatrolAdInfinitum( true ) -- Collision check distance. Default 5 NM. self:SetCollisionDistance() @@ -99204,46 +106265,53 @@ function AIRBOSS:New(carriername, alias) -- Menu options. self:SetMenuMarkZones() self:SetMenuSmokeZones() - self:SetMenuSingleCarrier(false) + self:SetMenuSingleCarrier( false ) -- Welcome players. - self:SetWelcomePlayers(true) + self:SetWelcomePlayers( true ) -- Coordinates - self.landingcoord=COORDINATE:New(0,0,0) --Core.Point#COORDINATE - self.sterncoord=COORDINATE:New(0, 0, 0) --Core.Point#COORDINATE - self.landingspotcoord=COORDINATE:New(0,0,0) --Core.Point#COORDINATE + self.landingcoord = COORDINATE:New( 0, 0, 0 ) -- Core.Point#COORDINATE + self.sterncoord = COORDINATE:New( 0, 0, 0 ) -- Core.Point#COORDINATE + self.landingspotcoord = COORDINATE:New( 0, 0, 0 ) -- Core.Point#COORDINATE -- Init carrier parameters. - if self.carriertype==AIRBOSS.CarrierType.STENNIS then - self:_InitStennis() - elseif self.carriertype==AIRBOSS.CarrierType.ROOSEVELT then + if self.carriertype == AIRBOSS.CarrierType.STENNIS then + --self:_InitStennis() self:_InitNimitz() - elseif self.carriertype==AIRBOSS.CarrierType.LINCOLN then + elseif self.carriertype == AIRBOSS.CarrierType.ROOSEVELT then self:_InitNimitz() - elseif self.carriertype==AIRBOSS.CarrierType.WASHINGTON then + elseif self.carriertype == AIRBOSS.CarrierType.LINCOLN then self:_InitNimitz() - elseif self.carriertype==AIRBOSS.CarrierType.TRUMAN then + elseif self.carriertype == AIRBOSS.CarrierType.WASHINGTON then self:_InitNimitz() - elseif self.carriertype==AIRBOSS.CarrierType.FORRESTAL then + elseif self.carriertype == AIRBOSS.CarrierType.TRUMAN then + self:_InitNimitz() + elseif self.carriertype == AIRBOSS.CarrierType.FORRESTAL then self:_InitForrestal() - elseif self.carriertype==AIRBOSS.CarrierType.VINSON then + elseif self.carriertype == AIRBOSS.CarrierType.VINSON then -- TODO: Carl Vinson parameters. self:_InitStennis() - elseif self.carriertype==AIRBOSS.CarrierType.TARAWA then + elseif self.carriertype == AIRBOSS.CarrierType.HERMES then + -- Hermes parameters. + self:_InitHermes() + elseif self.carriertype == AIRBOSS.CarrierType.TARAWA then -- Tarawa parameters. self:_InitTarawa() - elseif self.carriertype==AIRBOSS.CarrierType.AMERICA then + elseif self.carriertype == AIRBOSS.CarrierType.AMERICA then -- Use America parameters. self:_InitAmerica() - elseif self.carriertype==AIRBOSS.CarrierType.JCARLOS then + elseif self.carriertype == AIRBOSS.CarrierType.JCARLOS then -- Use Juan Carlos parameters. self:_InitJcarlos() - elseif self.carriertype==AIRBOSS.CarrierType.KUZNETSOV then + elseif self.carriertype == AIRBOSS.CarrierType.CANBERRA then + -- Use Juan Carlos parameters at this stage --TODO Check primary Landing spot. + self:_InitJcarlos() + elseif self.carriertype == AIRBOSS.CarrierType.KUZNETSOV then -- Kusnetsov parameters - maybe... self:_InitStennis() else - self:E(self.lid..string.format("ERROR: Unknown carrier type %s!", tostring(self.carriertype))) + self:E( self.lid .. string.format( "ERROR: Unknown carrier type %s!", tostring( self.carriertype ) ) ) return nil end @@ -99256,48 +106324,48 @@ function AIRBOSS:New(carriername, alias) -- Debug trace. if false then - self.Debug=true - BASE:TraceOnOff(true) - BASE:TraceClass(self.ClassName) - BASE:TraceLevel(3) - --self.dTstatus=0.1 + self.Debug = true + BASE:TraceOnOff( true ) + BASE:TraceClass( self.ClassName ) + BASE:TraceLevel( 3 ) + -- self.dTstatus=0.1 end -- Smoke zones. if false then - local case=3 - self.holdingoffset=30 - self:_GetZoneGroove():SmokeZone(SMOKECOLOR.Red, 5) - self:_GetZoneLineup():SmokeZone(SMOKECOLOR.Green, 5) - self:_GetZoneBullseye(case):SmokeZone(SMOKECOLOR.White, 45) - self:_GetZoneDirtyUp(case):SmokeZone(SMOKECOLOR.Orange, 45) - self:_GetZoneArcIn(case):SmokeZone(SMOKECOLOR.Blue, 45) - self:_GetZoneArcOut(case):SmokeZone(SMOKECOLOR.Blue, 45) - self:_GetZonePlatform(case):SmokeZone(SMOKECOLOR.Blue, 45) - self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) - self:_GetZoneHolding(case, 1):SmokeZone(SMOKECOLOR.White, 45) - self:_GetZoneHolding(case, 2):SmokeZone(SMOKECOLOR.White, 45) - self:_GetZoneInitial(case):SmokeZone(SMOKECOLOR.Orange, 45) - self:_GetZoneCommence(case, 1):SmokeZone(SMOKECOLOR.Red, 45) - self:_GetZoneCommence(case, 2):SmokeZone(SMOKECOLOR.Red, 45) - self:_GetZoneAbeamLandingSpot():SmokeZone(SMOKECOLOR.Red, 5) - self:_GetZoneLandingSpot():SmokeZone(SMOKECOLOR.Red, 5) + local case = 3 + self.holdingoffset = 30 + self:_GetZoneGroove():SmokeZone( SMOKECOLOR.Red, 5 ) + self:_GetZoneLineup():SmokeZone( SMOKECOLOR.Green, 5 ) + self:_GetZoneBullseye( case ):SmokeZone( SMOKECOLOR.White, 45 ) + self:_GetZoneDirtyUp( case ):SmokeZone( SMOKECOLOR.Orange, 45 ) + self:_GetZoneArcIn( case ):SmokeZone( SMOKECOLOR.Blue, 45 ) + self:_GetZoneArcOut( case ):SmokeZone( SMOKECOLOR.Blue, 45 ) + self:_GetZonePlatform( case ):SmokeZone( SMOKECOLOR.Blue, 45 ) + self:_GetZoneCorridor( case ):SmokeZone( SMOKECOLOR.Green, 45 ) + self:_GetZoneHolding( case, 1 ):SmokeZone( SMOKECOLOR.White, 45 ) + self:_GetZoneHolding( case, 2 ):SmokeZone( SMOKECOLOR.White, 45 ) + self:_GetZoneInitial( case ):SmokeZone( SMOKECOLOR.Orange, 45 ) + self:_GetZoneCommence( case, 1 ):SmokeZone( SMOKECOLOR.Red, 45 ) + self:_GetZoneCommence( case, 2 ):SmokeZone( SMOKECOLOR.Red, 45 ) + self:_GetZoneAbeamLandingSpot():SmokeZone( SMOKECOLOR.Red, 5 ) + self:_GetZoneLandingSpot():SmokeZone( SMOKECOLOR.Red, 5 ) end -- Carrier parameter debug tests. if false then - -- Stern coordinate. - local FB=self:GetFinalBearing(false) - local hdg=self:GetHeading(false) + -- Stern coordinate. + local FB = self:GetFinalBearing( false ) + local hdg = self:GetHeading( false ) -- Stern pos. - local stern=self:_GetSternCoord() + local stern = self:_GetSternCoord() -- Bow pos. - local bow=stern:Translate(self.carrierparam.totlength, hdg, true) + local bow = stern:Translate( self.carrierparam.totlength, hdg, true ) -- End of rwy. - local rwy=stern:Translate(self.carrierparam.rwylength, FB, true) + local rwy = stern:Translate( self.carrierparam.rwylength, FB, true ) --- Flare points and zones. local function flareme() @@ -99312,31 +106380,30 @@ function AIRBOSS:New(carriername, alias) bow:FlareYellow() -- Runway half width = 10 m. - local r1=stern:Translate(self.carrierparam.rwywidth*0.5, FB+90, true) - local r2=stern:Translate(self.carrierparam.rwywidth*0.5, FB-90, true) - --r1:FlareWhite() - --r2:FlareWhite() + local r1 = stern:Translate( self.carrierparam.rwywidth * 0.5, FB + 90, true ) + local r2 = stern:Translate( self.carrierparam.rwywidth * 0.5, FB - 90, true ) + -- r1:FlareWhite() + -- r2:FlareWhite() -- End of runway. rwy:FlareRed() -- Right 30 meters from stern. - local cR=stern:Translate(self.carrierparam.totwidthstarboard, hdg+90, true) - --cR:FlareYellow() + local cR = stern:Translate( self.carrierparam.totwidthstarboard, hdg + 90, true ) + -- cR:FlareYellow() -- Left 40 meters from stern. - local cL=stern:Translate(self.carrierparam.totwidthport, hdg-90, true) - --cL:FlareYellow() - + local cL = stern:Translate( self.carrierparam.totwidthport, hdg - 90, true ) + -- cL:FlareYellow() -- Carrier specific. - if self.carrier:GetTypeName()~=AIRBOSS.CarrierType.TARAWA or self.carrier:GetTypeName()~=AIRBOSS.CarrierType.AMERICA or self.carrier:GetTypeName()~=AIRBOSS.CarrierType.JCARLOS then + if self.carrier:GetTypeName() ~= AIRBOSS.CarrierType.HERMES or self.carrier:GetTypeName() ~= AIRBOSS.CarrierType.TARAWA or self.carrier:GetTypeName() ~= AIRBOSS.CarrierType.AMERICA or self.carrier:GetTypeName() ~= AIRBOSS.CarrierType.JCARLOS or self.carrier:GetTypeName() ~= AIRBOSS.CarrierType.CANBERRA then -- Flare wires. - local w1=stern:Translate(self.carrierparam.wire1, FB, true) - local w2=stern:Translate(self.carrierparam.wire2, FB, true) - local w3=stern:Translate(self.carrierparam.wire3, FB, true) - local w4=stern:Translate(self.carrierparam.wire4, FB, true) + local w1 = stern:Translate( self.carrierparam.wire1, FB, true ) + local w2 = stern:Translate( self.carrierparam.wire2, FB, true ) + local w3 = stern:Translate( self.carrierparam.wire3, FB, true ) + local w4 = stern:Translate( self.carrierparam.wire4, FB, true ) w1:FlareWhite() w2:FlareYellow() w3:FlareWhite() @@ -99345,27 +106412,27 @@ function AIRBOSS:New(carriername, alias) else -- Abeam landing spot zone. - local ALSPT=self:_GetZoneAbeamLandingSpot() - ALSPT:FlareZone(FLARECOLOR.Red, 5, nil, UTILS.FeetToMeters(120)) + local ALSPT = self:_GetZoneAbeamLandingSpot() + ALSPT:FlareZone( FLARECOLOR.Red, 5, nil, UTILS.FeetToMeters( 120 ) ) -- Primary landing spot zone. - local LSPT=self:_GetZoneLandingSpot() - LSPT:FlareZone(FLARECOLOR.Green, 5, nil, self.carrierparam.deckheight) + local LSPT = self:_GetZoneLandingSpot() + LSPT:FlareZone( FLARECOLOR.Green, 5, nil, self.carrierparam.deckheight ) -- Landing spot coordinate. - local PLSC=self:_GetLandingSpotCoordinate() + local PLSC = self:_GetLandingSpotCoordinate() PLSC:FlareWhite() end -- Flare carrier and landing runway. - local cbox=self:_GetZoneCarrierBox() - local rbox=self:_GetZoneRunwayBox() - cbox:FlareZone(FLARECOLOR.Green, 5, nil, self.carrierparam.deckheight) - rbox:FlareZone(FLARECOLOR.White, 5, nil, self.carrierparam.deckheight) + local cbox = self:_GetZoneCarrierBox() + local rbox = self:_GetZoneRunwayBox() + cbox:FlareZone( FLARECOLOR.Green, 5, nil, self.carrierparam.deckheight ) + rbox:FlareZone( FLARECOLOR.White, 5, nil, self.carrierparam.deckheight ) end -- Flare points every 3 seconds for 3 minutes. - SCHEDULER:New(nil, flareme, {}, 1, 3, nil, 180) + SCHEDULER:New( nil, flareme, {}, 1, 3, nil, 180 ) end ----------------------- @@ -99373,7 +106440,7 @@ function AIRBOSS:New(carriername, alias) ----------------------- -- Start State. - self:SetStartState("Stopped") + self:SetStartState( "Stopped" ) -- Add FSM transitions. -- From State --> Event --> To State @@ -99409,7 +106476,6 @@ function AIRBOSS:New(carriername, alias) -- @param #string Event Event. -- @param #string To To state. - --- Triggers the FSM event "Idle" that puts the carrier into state "Idle" where no recoveries are carried out. -- @function [parent=#AIRBOSS] Idle -- @param #AIRBOSS self @@ -99419,7 +106485,6 @@ function AIRBOSS:New(carriername, alias) -- @param #AIRBOSS self -- @param #number delay Delay in seconds. - --- Triggers the FSM event "RecoveryStart" that starts the recovery of aircraft. Marshalling aircraft are send to the landing pattern. -- @function [parent=#AIRBOSS] RecoveryStart -- @param #AIRBOSS self @@ -99442,7 +106507,6 @@ function AIRBOSS:New(carriername, alias) -- @param #number Case The recovery case (1, 2 or 3) to start. -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. - --- Triggers the FSM event "RecoveryStop" that stops the recovery of aircraft. -- @function [parent=#AIRBOSS] RecoveryStop -- @param #AIRBOSS self @@ -99459,7 +106523,6 @@ function AIRBOSS:New(carriername, alias) -- @param #string Event Event. -- @param #string To To state. - --- Triggers the FSM event "RecoveryPause" that pauses the recovery of aircraft. -- @function [parent=#AIRBOSS] RecoveryPause -- @param #AIRBOSS self @@ -99480,7 +106543,6 @@ function AIRBOSS:New(carriername, alias) -- @param #AIRBOSS self -- @param #number delay Delay in seconds. - --- Triggers the FSM event "RecoveryCase" that switches the aircraft recovery case. -- @function [parent=#AIRBOSS] RecoveryCase -- @param #AIRBOSS self @@ -99494,7 +106556,6 @@ function AIRBOSS:New(carriername, alias) -- @param #number Case The new recovery case (1, 2 or 3). -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. - --- Triggers the FSM event "PassingWaypoint". Called when the carrier passes a waypoint. -- @function [parent=#AIRBOSS] PassingWaypoint -- @param #AIRBOSS self @@ -99515,7 +106576,6 @@ function AIRBOSS:New(carriername, alias) -- @param #string To To state. -- @param #number waypoint Number of waypoint. - --- Triggers the FSM event "Save" that saved the player scores to a file. -- @function [parent=#AIRBOSS] Save -- @param #AIRBOSS self @@ -99538,7 +106598,6 @@ function AIRBOSS:New(carriername, alias) -- @param #string path Path where the file is saved. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized. -- @param #string filename (Optional) File name. Default is AIRBOSS-*ALIAS*_LSOgrades.csv. - --- Triggers the FSM event "Load" that loads the player scores from a file. AIRBOSS FSM must **not** be started at this point. -- @function [parent=#AIRBOSS] Load -- @param #AIRBOSS self @@ -99561,7 +106620,6 @@ function AIRBOSS:New(carriername, alias) -- @param #string path Path where the file is located. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized. -- @param #string filename (Optional) File name. Default is AIRBOSS-*ALIAS*_LSOgrades.csv. - --- Triggers the FSM event "LSOGrade". Called when the LSO grades a player -- @function [parent=#AIRBOSS] LSOGrade -- @param #AIRBOSS self @@ -99584,7 +106642,6 @@ function AIRBOSS:New(carriername, alias) -- @param #AIRBOSS.PlayerData playerData Player Data. -- @param #AIRBOSS.LSOgrade grade LSO grade. - --- Triggers the FSM event "Marshal". Called when a flight is send to the Marshal stack. -- @function [parent=#AIRBOSS] Marshal -- @param #AIRBOSS self @@ -99604,7 +106661,6 @@ function AIRBOSS:New(carriername, alias) -- @param #string To To state. -- @param #AIRBOSS.FlightGroup flight The flight group data. - --- Triggers the FSM event "Stop" that stops the airboss. Event handlers are stopped. -- @function [parent=#AIRBOSS] Stop -- @param #AIRBOSS self @@ -99623,26 +106679,25 @@ end --- Set welcome messages for players. -- @param #AIRBOSS self --- @param #boolean switch If true, display welcome message to player. +-- @param #boolean Switch If true, display welcome message to player. -- @return #AIRBOSS self -function AIRBOSS:SetWelcomePlayers(switch) +function AIRBOSS:SetWelcomePlayers( Switch ) - self.welcome=switch + self.welcome = Switch return self end - --- Set carrier controlled area (CCA). -- This is a large zone around the carrier, which is constantly updated wrt the carrier position. -- @param #AIRBOSS self --- @param #number radius Radius of zone in nautical miles (NM). Default 50 NM. +-- @param #number Radius Radius of zone in nautical miles (NM). Default 50 NM. -- @return #AIRBOSS self -function AIRBOSS:SetCarrierControlledArea(radius) +function AIRBOSS:SetCarrierControlledArea( Radius ) - radius=UTILS.NMToMeters(radius or 50) + Radius = UTILS.NMToMeters( Radius or 50 ) - self.zoneCCA=ZONE_UNIT:New("Carrier Controlled Area", self.carrier, radius) + self.zoneCCA = ZONE_UNIT:New( "Carrier Controlled Area", self.carrier, Radius ) return self end @@ -99650,37 +106705,37 @@ end --- Set carrier controlled zone (CCZ). -- This is a small zone (usually 5 NM radius) around the carrier, which is constantly updated wrt the carrier position. -- @param #AIRBOSS self --- @param #number radius Radius of zone in nautical miles (NM). Default 5 NM. +-- @param #number Radius Radius of zone in nautical miles (NM). Default 5 NM. -- @return #AIRBOSS self -function AIRBOSS:SetCarrierControlledZone(radius) +function AIRBOSS:SetCarrierControlledZone( Radius ) - radius=UTILS.NMToMeters(radius or 5) + Radius = UTILS.NMToMeters( Radius or 5 ) - self.zoneCCZ=ZONE_UNIT:New("Carrier Controlled Zone", self.carrier, radius) + self.zoneCCZ = ZONE_UNIT:New( "Carrier Controlled Zone", self.carrier, Radius ) return self end --- Set distance up to which water ahead is scanned for collisions. -- @param #AIRBOSS self --- @param #number dist Distance in NM. Default 5 NM. +-- @param #number Distance Distance in NM. Default 5 NM. -- @return #AIRBOSS self -function AIRBOSS:SetCollisionDistance(distance) - self.collisiondist=UTILS.NMToMeters(distance or 5) +function AIRBOSS:SetCollisionDistance( Distance ) + self.collisiondist = UTILS.NMToMeters( Distance or 5 ) return self end --- Set the default recovery case. -- @param #AIRBOSS self --- @param #number case Case of recovery. Either 1, 2 or 3. Default 1. +-- @param #number Case Case of recovery. Either 1, 2 or 3. Default 1. -- @return #AIRBOSS self -function AIRBOSS:SetRecoveryCase(case) +function AIRBOSS:SetRecoveryCase( Case ) -- Set default case or 1. - self.defaultcase=case or 1 + self.defaultcase = Case or 1 -- Current case init. - self.case=self.defaultcase + self.case = self.defaultcase return self end @@ -99689,37 +106744,37 @@ end -- Usually, this is +-15 or +-30 degrees. You should not use and offset angle >= 90 degrees, because this will cause a devision by zero in some of the equations used to calculate the approach corridor. -- So best stick to the defaults up to 30 degrees. -- @param #AIRBOSS self --- @param #number offset Offset angle in degrees. Default 0. +-- @param #number Offset Offset angle in degrees. Default 0. -- @return #AIRBOSS self -function AIRBOSS:SetHoldingOffsetAngle(offset) +function AIRBOSS:SetHoldingOffsetAngle( Offset ) -- Set default angle or 0. - self.defaultoffset=offset or 0 + self.defaultoffset = Offset or 0 -- Current offset init. - self.holdingoffset=self.defaultoffset + self.holdingoffset = self.defaultoffset return self end --- Enable F10 menu to manually start recoveries. -- @param #AIRBOSS self --- @param #number duration Default duration of the recovery in minutes. Default 30 min. --- @param #number windondeck Default wind on deck in knots. Default 25 knots. --- @param #boolean uturn U-turn after recovery window closes on=true or off=false/nil. Default off. --- @param #number offset Relative Marshal radial in degrees for Case II/III recoveries. Default 30°. +-- @param #number Duration Default duration of the recovery in minutes. Default 30 min. +-- @param #number WindOnDeck Default wind on deck in knots. Default 25 knots. +-- @param #boolean Uturn U-turn after recovery window closes on=true or off=false/nil. Default off. +-- @param #number Offset Relative Marshal radial in degrees for Case II/III recoveries. Default 30°. -- @return #AIRBOSS self -function AIRBOSS:SetMenuRecovery(duration, windondeck, uturn, offset) +function AIRBOSS:SetMenuRecovery( Duration, WindOnDeck, Uturn, Offset ) - self.skipperMenu=true - self.skipperTime=duration or 30 - self.skipperSpeed=windondeck or 25 - self.skipperOffset=offset or 30 + self.skipperMenu = true + self.skipperTime = Duration or 30 + self.skipperSpeed = WindOnDeck or 25 + self.skipperOffset = Offset or 30 - if uturn then - self.skipperUturn=true + if Uturn then + self.skipperUturn = true else - self.skipperUturn=false + self.skipperUturn = false end return self @@ -99735,136 +106790,135 @@ end -- @param #number speed Speed in knots during turn into wind leg. -- @param #boolean uturn If true (or nil), carrier wil perform a U-turn and go back to where it came from before resuming its route to the next waypoint. If false, it will go directly to the next waypoint. -- @return #AIRBOSS.Recovery Recovery window. -function AIRBOSS:AddRecoveryWindow(starttime, stoptime, case, holdingoffset, turnintowind, speed, uturn) +function AIRBOSS:AddRecoveryWindow( starttime, stoptime, case, holdingoffset, turnintowind, speed, uturn ) -- Absolute mission time in seconds. - local Tnow=timer.getAbsTime() + local Tnow = timer.getAbsTime() - if starttime and type(starttime)=="number" then - starttime=UTILS.SecondsToClock(Tnow+starttime) + if starttime and type( starttime ) == "number" then + starttime = UTILS.SecondsToClock( Tnow + starttime ) end - if stoptime and type(stoptime)=="number" then - stoptime=UTILS.SecondsToClock(Tnow+stoptime) + if stoptime and type( stoptime ) == "number" then + stoptime = UTILS.SecondsToClock( Tnow + stoptime ) end - -- Input or now. - starttime=starttime or UTILS.SecondsToClock(Tnow) + starttime = starttime or UTILS.SecondsToClock( Tnow ) -- Set start time. - local Tstart=UTILS.ClockToSeconds(starttime) + local Tstart = UTILS.ClockToSeconds( starttime ) -- Set stop time. - local Tstop=stoptime and UTILS.ClockToSeconds(stoptime) or Tstart+90*60 + local Tstop = stoptime and UTILS.ClockToSeconds( stoptime ) or Tstart + 90 * 60 -- Consistancy check for timing. - if Tstart>Tstop then - self:E(string.format("ERROR: Recovery stop time %s lies before recovery start time %s! Recovery window rejected.", UTILS.SecondsToClock(Tstart), UTILS.SecondsToClock(Tstop))) + if Tstart > Tstop then + self:E( string.format( "ERROR: Recovery stop time %s lies before recovery start time %s! Recovery window rejected.", UTILS.SecondsToClock( Tstart ), UTILS.SecondsToClock( Tstop ) ) ) return self end - if Tstop<=Tnow then - self:I(string.format("WARNING: Recovery stop time %s already over. Tnow=%s! Recovery window rejected.", UTILS.SecondsToClock(Tstop), UTILS.SecondsToClock(Tnow))) + if Tstop <= Tnow then + self:I( string.format( "WARNING: Recovery stop time %s already over. Tnow=%s! Recovery window rejected.", UTILS.SecondsToClock( Tstop ), UTILS.SecondsToClock( Tnow ) ) ) return self end -- Case or default value. - case=case or self.defaultcase + case = case or self.defaultcase -- Holding offset or default value. - holdingoffset=holdingoffset or self.defaultoffset + holdingoffset = holdingoffset or self.defaultoffset -- Offset zero for case I. - if case==1 then - holdingoffset=0 + if case == 1 then + holdingoffset = 0 end -- Increase counter. - self.windowcount=self.windowcount+1 + self.windowcount = self.windowcount + 1 -- Recovery window. - local recovery={} --#AIRBOSS.Recovery - recovery.START=Tstart - recovery.STOP=Tstop - recovery.CASE=case - recovery.OFFSET=holdingoffset - recovery.OPEN=false - recovery.OVER=false - recovery.WIND=turnintowind - recovery.SPEED=speed or 20 - recovery.ID=self.windowcount + local recovery = {} -- #AIRBOSS.Recovery + recovery.START = Tstart + recovery.STOP = Tstop + recovery.CASE = case + recovery.OFFSET = holdingoffset + recovery.OPEN = false + recovery.OVER = false + recovery.WIND = turnintowind + recovery.SPEED = speed or 20 + recovery.ID = self.windowcount - if uturn==nil or uturn==true then - recovery.UTURN=true + if uturn == nil or uturn == true then + recovery.UTURN = true else - recovery.UTURN=false + recovery.UTURN = false end -- Add to table - table.insert(self.recoverytimes, recovery) + table.insert( self.recoverytimes, recovery ) return recovery end --- Define a set of AI groups that are handled by the airboss. -- @param #AIRBOSS self --- @param Core.Set#SET_GROUP setgroup The set of AI groups which are handled by the airboss. +-- @param Core.Set#SET_GROUP SetGroup The set of AI groups which are handled by the airboss. -- @return #AIRBOSS self -function AIRBOSS:SetSquadronAI(setgroup) - self.squadsetAI=setgroup +function AIRBOSS:SetSquadronAI( SetGroup ) + self.squadsetAI = SetGroup return self end --- Define a set of AI groups that excluded from AI handling. Members of this set will be left allone by the airboss and not forced into the Marshal pattern. -- @param #AIRBOSS self --- @param Core.Set#SET_GROUP setgroup The set of AI groups which are excluded. +-- @param Core.Set#SET_GROUP SetGroup The set of AI groups which are excluded. -- @return #AIRBOSS self -function AIRBOSS:SetExcludeAI(setgroup) - self.excludesetAI=setgroup +function AIRBOSS:SetExcludeAI( SetGroup ) + self.excludesetAI = SetGroup return self end --- Add a group to the exclude set. If no set exists, it is created. -- @param #AIRBOSS self --- @param Wrapper.Group#GROUP group The group to be excluded. +-- @param Wrapper.Group#GROUP Group The group to be excluded. -- @return #AIRBOSS self -function AIRBOSS:AddExcludeAI(group) +function AIRBOSS:AddExcludeAI( Group ) - self.excludesetAI=self.excludesetAI or SET_GROUP:New() + self.excludesetAI = self.excludesetAI or SET_GROUP:New() - self.excludesetAI:AddGroup(group) + self.excludesetAI:AddGroup( Group ) return self end --- Close currently running recovery window and stop recovery ops. Recovery window is deleted. -- @param #AIRBOSS self --- @param #number delay (Optional) Delay in seconds before the window is deleted. -function AIRBOSS:CloseCurrentRecoveryWindow(delay) +-- @param #number Delay (Optional) Delay in seconds before the window is deleted. +function AIRBOSS:CloseCurrentRecoveryWindow( Delay ) - if delay and delay>0 then - --SCHEDULER:New(nil, self.CloseCurrentRecoveryWindow, {self}, delay) - self:ScheduleOnce(delay, self.CloseCurrentRecoveryWindow, self) + if Delay and Delay > 0 then + -- SCHEDULER:New(nil, self.CloseCurrentRecoveryWindow, {self}, delay) + self:ScheduleOnce( Delay, self.CloseCurrentRecoveryWindow, self ) else if self:IsRecovering() and self.recoverywindow and self.recoverywindow.OPEN then self:RecoveryStop() - self.recoverywindow.OPEN=false - self.recoverywindow.OVER=true - self:DeleteRecoveryWindow(self.recoverywindow) + self.recoverywindow.OPEN = false + self.recoverywindow.OVER = true + self:DeleteRecoveryWindow( self.recoverywindow ) end end end --- Delete all recovery windows. -- @param #AIRBOSS self --- @param #number delay (Optional) Delay in seconds before the windows are deleted. +-- @param #number Delay (Optional) Delay in seconds before the windows are deleted. -- @return #AIRBOSS self -function AIRBOSS:DeleteAllRecoveryWindows(delay) +function AIRBOSS:DeleteAllRecoveryWindows( Delay ) -- Loop over all recovery windows. - for _,recovery in pairs(self.recoverytimes) do - self:I(self.lid..string.format("Deleting recovery window ID %s", tostring(recovery.ID))) - self:DeleteRecoveryWindow(recovery, delay) + for _, recovery in pairs( self.recoverytimes ) do + self:I( self.lid .. string.format( "Deleting recovery window ID %s", tostring( recovery.ID ) ) ) + self:DeleteRecoveryWindow( recovery, Delay ) end return self @@ -99874,11 +106928,11 @@ end -- @param #AIRBOSS self -- @param #number id The ID of the recovery window. -- @return #AIRBOSS.Recovery Recovery window with the right ID or nil if no such window exists. -function AIRBOSS:GetRecoveryWindowByID(id) +function AIRBOSS:GetRecoveryWindowByID( id ) if id then - for _,_window in pairs(self.recoverytimes) do - local window=_window --#AIRBOSS.Recovery - if window and window.ID==id then + for _, _window in pairs( self.recoverytimes ) do + local window = _window -- #AIRBOSS.Recovery + if window and window.ID == id then return window end end @@ -99888,25 +106942,25 @@ end --- Delete a recovery window. If the window is currently open, it is closed and the recovery stopped. -- @param #AIRBOSS self --- @param #AIRBOSS.Recovery window Recovery window. --- @param #number delay Delay in seconds, before the window is deleted. -function AIRBOSS:DeleteRecoveryWindow(window, delay) +-- @param #AIRBOSS.Recovery Window Recovery window. +-- @param #number Delay Delay in seconds, before the window is deleted. +function AIRBOSS:DeleteRecoveryWindow( Window, Delay ) - if delay and delay>0 then + if Delay and Delay > 0 then -- Delayed call. - --SCHEDULER:New(nil, self.DeleteRecoveryWindow, {self, window}, delay) - self:ScheduleOnce(delay, self.DeleteRecoveryWindow, self, window) + -- SCHEDULER:New(nil, self.DeleteRecoveryWindow, {self, window}, delay) + self:ScheduleOnce( Delay, self.DeleteRecoveryWindow, self, Window ) else - for i,_recovery in pairs(self.recoverytimes) do - local recovery=_recovery --#AIRBOSS.Recovery + for i, _recovery in pairs( self.recoverytimes ) do + local recovery = _recovery -- #AIRBOSS.Recovery - if window and window.ID==recovery.ID then - if window.OPEN then + if Window and Window.ID == recovery.ID then + if Window.OPEN then -- Window is currently open. self:RecoveryStop() else - table.remove(self.recoverytimes, i) + table.remove( self.recoverytimes, i ) end end @@ -99916,10 +106970,10 @@ end --- Set time before carrier turns and recovery window opens. -- @param #AIRBOSS self --- @param #number interval Time interval in seconds. Default 300 sec. +-- @param #number Interval Time interval in seconds. Default 300 sec. -- @return #AIRBOSS self -function AIRBOSS:SetRecoveryTurnTime(interval) - self.dTturn=interval or 300 +function AIRBOSS:SetRecoveryTurnTime( Interval ) + self.dTturn = Interval or 300 return self end @@ -99927,145 +106981,142 @@ end -- @param #AIRBOSS self -- @param #number Dcorr Correction distance in meters. Default 12 m. -- @return #AIRBOSS self -function AIRBOSS:SetMPWireCorrection(Dcorr) - self.mpWireCorrection=Dcorr or 12 +function AIRBOSS:SetMPWireCorrection( Dcorr ) + self.mpWireCorrection = Dcorr or 12 return self end --- Set time interval for updating queues and other stuff. -- @param #AIRBOSS self --- @param #number interval Time interval in seconds. Default 30 sec. +-- @param #number TimeInterval Time interval in seconds. Default 30 sec. -- @return #AIRBOSS self -function AIRBOSS:SetQueueUpdateTime(interval) - self.dTqueue=interval or 30 +function AIRBOSS:SetQueueUpdateTime( TimeInterval ) + self.dTqueue = TimeInterval or 30 return self end --- Set time interval between LSO calls. Optimal time in the groove is ~16 seconds. So the default of 4 seconds gives around 3-4 correction calls in the groove. -- @param #AIRBOSS self --- @param #number interval Time interval in seconds between LSO calls. Default 4 sec. +-- @param #number TimeInterval Time interval in seconds between LSO calls. Default 4 sec. -- @return #AIRBOSS self -function AIRBOSS:SetLSOCallInterval(timeinterval) - self.LSOdT=timeinterval or 4 +function AIRBOSS:SetLSOCallInterval( TimeInterval ) + self.LSOdT = TimeInterval or 4 return self end --- Airboss is a rather nice guy and not strictly following the rules. Fore example, he does allow you into the landing pattern if you are not coming from the Marshal stack. -- @param #AIRBOSS self --- @param #boolean switch If true or nil, Airboss bends the rules a bit. +-- @param #boolean Switch If true or nil, Airboss bends the rules a bit. -- @return #AIRBOSS self -function AIRBOSS:SetAirbossNiceGuy(switch) - if switch==true or switch==nil then - self.airbossnice=true +function AIRBOSS:SetAirbossNiceGuy( Switch ) + if Switch == true or Switch == nil then + self.airbossnice = true else - self.airbossnice=false + self.airbossnice = false end return self end --- Allow emergency landings, i.e. bypassing any pattern and go directly to final approach. -- @param #AIRBOSS self --- @param #boolean switch If true or nil, emergency landings are okay. +-- @param #boolean Switch If true or nil, emergency landings are okay. -- @return #AIRBOSS self -function AIRBOSS:SetEmergencyLandings(switch) - if switch==true or switch==nil then - self.emergency=true +function AIRBOSS:SetEmergencyLandings( Switch ) + if Switch == true or Switch == nil then + self.emergency = true else - self.emergency=false + self.emergency = false end return self end - --- Despawn AI groups after they they shut down their engines. -- @param #AIRBOSS self --- @param #boolean switch If true or nil, AI groups are despawned. +-- @param #boolean Switch If true or nil, AI groups are despawned. -- @return #AIRBOSS self -function AIRBOSS:SetDespawnOnEngineShutdown(switch) - if switch==true or switch==nil then - self.despawnshutdown=true +function AIRBOSS:SetDespawnOnEngineShutdown( Switch ) + if Switch == true or Switch == nil then + self.despawnshutdown = true else - self.despawnshutdown=false + self.despawnshutdown = false end return self end --- Respawn AI groups once they reach the CCA. Clears any attached airbases and allows making them land on the carrier via script. -- @param #AIRBOSS self --- @param #boolean switch If true or nil, AI groups are respawned. +-- @param #boolean Switch If true or nil, AI groups are respawned. -- @return #AIRBOSS self -function AIRBOSS:SetRespawnAI(switch) - if switch==true or switch==nil then - self.respawnAI=true +function AIRBOSS:SetRespawnAI( Switch ) + if Switch == true or Switch == nil then + self.respawnAI = true else - self.respawnAI=false + self.respawnAI = false end return self end --- Give AI aircraft the refueling task if a recovery tanker is present or send them to the nearest divert airfield. -- @param #AIRBOSS self --- @param #number lowfuelthreshold Low fuel threshold in percent. AI will go refueling if their fuel level drops below this value. Default 10 %. +-- @param #number LowFuelThreshold Low fuel threshold in percent. AI will go refueling if their fuel level drops below this value. Default 10 %. -- @return #AIRBOSS self -function AIRBOSS:SetRefuelAI(lowfuelthreshold) - self.lowfuelAI=lowfuelthreshold or 10 +function AIRBOSS:SetRefuelAI( LowFuelThreshold ) + self.lowfuelAI = LowFuelThreshold or 10 return self end --- Set max alitude to register flights in the initial zone. Aircraft above this altitude will not be registerered. -- @param #AIRBOSS self --- @param #number altitude Max alitude in feet. Default 1300 ft. +-- @param #number MaxAltitude Max alitude in feet. Default 1300 ft. -- @return #AIRBOSS self -function AIRBOSS:SetInitialMaxAlt(altitude) - self.initialmaxalt=UTILS.FeetToMeters(altitude or 1300) +function AIRBOSS:SetInitialMaxAlt( MaxAltitude ) + self.initialmaxalt = UTILS.FeetToMeters( MaxAltitude or 1300 ) return self end - ---- Set folder where the airboss sound files are located **within you mission (miz) file**. +--- Set folder path where the airboss sound files are located **within you mission (miz) file**. -- The default path is "l10n/DEFAULT/" but sound files simply copied there will be removed by DCS the next time you save the mission. -- However, if you create a new folder inside the miz file, which contains the sounds, it will not be deleted and can be used. -- @param #AIRBOSS self --- @param #string folderpath The path to the sound files, e.g. "Airboss Soundfiles/". +-- @param #string FolderPath The path to the sound files, e.g. "Airboss Soundfiles/". -- @return #AIRBOSS self -function AIRBOSS:SetSoundfilesFolder(folderpath) +function AIRBOSS:SetSoundfilesFolder( FolderPath ) -- Check that it ends with / - if folderpath then - local lastchar=string.sub(folderpath, -1) - if lastchar~="/" then - folderpath=folderpath.."/" + if FolderPath then + local lastchar = string.sub( FolderPath, -1 ) + if lastchar ~= "/" then + FolderPath = FolderPath .. "/" end end -- Folderpath. - self.soundfolder=folderpath + self.soundfolder = FolderPath -- Info message. - self:I(self.lid..string.format("Setting sound files folder to: %s", self.soundfolder)) + self:I( self.lid .. string.format( "Setting sound files folder to: %s", self.soundfolder ) ) return self end --- Set time interval for updating player status and other things. -- @param #AIRBOSS self --- @param #number interval Time interval in seconds. Default 0.5 sec. +-- @param #number TimeInterval Time interval in seconds. Default 0.5 sec. -- @return #AIRBOSS self -function AIRBOSS:SetStatusUpdateTime(interval) - self.dTstatus=interval or 0.5 +function AIRBOSS:SetStatusUpdateTime( TimeInterval ) + self.dTstatus = TimeInterval or 0.5 return self end --- Set duration how long messages are displayed to players. -- @param #AIRBOSS self --- @param #number duration Duration in seconds. Default 10 sec. +-- @param #number Duration Duration in seconds. Default 10 sec. -- @return #AIRBOSS self -function AIRBOSS:SetDefaultMessageDuration(duration) - self.Tmessage=duration or 10 +function AIRBOSS:SetDefaultMessageDuration( Duration ) + self.Tmessage = Duration or 10 return self end - --- Set glideslope error thresholds. -- @param #AIRBOSS self -- @param #number _max @@ -100075,13 +107126,29 @@ end -- @param #number Low -- @param #number LOW -- @return #AIRBOSS self + function AIRBOSS:SetGlideslopeErrorThresholds(_max,_min, High, HIGH, Low, LOW) + + --Check if V/STOL Carrier + if self.carriertype == AIRBOSS.CarrierType.HERMES or self.carriertype == AIRBOSS.CarrierType.TARAWA or self.carriertype == AIRBOSS.CarrierType.AMERICA or self.carriertype == AIRBOSS.CarrierType.JCARLOS or self.carriertype == AIRBOSS.CarrierType.CANBERRA then + + -- allow a larger GSE for V/STOL operations --Pene Testing + self.gle._max=_max or 0.7 + self.gle.High=High or 1.4 + self.gle.HIGH=HIGH or 1.9 + self.gle._min=_min or -0.5 + self.gle.Low=Low or -1.2 + self.gle.LOW=LOW or -1.5 + -- CVN values + else self.gle._max=_max or 0.4 self.gle.High=High or 0.8 self.gle.HIGH=HIGH or 1.5 self.gle._min=_min or -0.3 self.gle.Low=Low or -0.6 self.gle.LOW=LOW or -0.9 + end + return self end @@ -100096,7 +107163,23 @@ end -- @param #number RightMed -- @param #number RIGHT -- @return #AIRBOSS self + function AIRBOSS:SetLineupErrorThresholds(_max,_min, Left, LeftMed, LEFT, Right, RightMed, RIGHT) + + --Check if V/STOL Carrier -- Pene testing + if self.carriertype == AIRBOSS.CarrierType.HERMES or self.carriertype == AIRBOSS.CarrierType.TARAWA or self.carriertype == AIRBOSS.CarrierType.AMERICA or self.carriertype == AIRBOSS.CarrierType.JCARLOS or self.carriertype == AIRBOSS.CarrierType.CANBERRA then + + -- V/STOL Values -- allow a larger LUE for V/STOL operations + self.lue._max=_max or 1.8 + self.lue._min=_min or -1.8 + self.lue.Left=Left or -2.8 + self.lue.LeftMed=LeftMed or -3.8 + self.lue.LEFT=LEFT or -4.5 + self.lue.Right=Right or 2.8 + self.lue.RightMed=RightMed or 3.8 + self.lue.RIGHT=RIGHT or 4.5 + -- CVN Values + else self.lue._max=_max or 0.5 self.lue._min=_min or -0.5 self.lue.Left=Left or -1.0 @@ -100105,109 +107188,110 @@ function AIRBOSS:SetLineupErrorThresholds(_max,_min, Left, LeftMed, LEFT, Right, self.lue.Right=Right or 1.0 self.lue.RightMed=RightMed or 2.0 self.lue.RIGHT=RIGHT or 3.0 + end + return self end --- Set Case I Marshal radius. This is the radius of the valid zone around "the post" aircraft are supposed to be holding in the Case I Marshal stack. -- The post is 2.5 NM port of the carrier. -- @param #AIRBOSS self --- @param #number Radius in NM. Default 2.8 NM, which gives a diameter of 5.6 NM. +-- @param #number Radius Radius in NM. Default 2.8 NM, which gives a diameter of 5.6 NM. -- @return #AIRBOSS self -function AIRBOSS:SetMarshalRadius(radius) - self.marshalradius=UTILS.NMToMeters(radius or 2.8) +function AIRBOSS:SetMarshalRadius( Radius ) + self.marshalradius = UTILS.NMToMeters( Radius or 2.8 ) return self end --- Optimized F10 radio menu for a single carrier. The menu entries will be stored directly under F10 Other/Airboss/ and not F10 Other/Airboss/"Carrier Alias"/. -- **WARNING**: If you use this with two airboss objects/carriers, the radio menu will be screwed up! -- @param #AIRBOSS self --- @param #boolean switch If true or nil single menu is enabled. If false, menu is for multiple carriers in the mission. +-- @param #boolean Switch If true or nil single menu is enabled. If false, menu is for multiple carriers in the mission. -- @return #AIRBOSS self -function AIRBOSS:SetMenuSingleCarrier(switch) - if switch==true or switch==nil then - self.menusingle=true +function AIRBOSS:SetMenuSingleCarrier( Switch ) + if Switch == true or Switch == nil then + self.menusingle = true else - self.menusingle=false + self.menusingle = false end return self end --- Enable or disable F10 radio menu for marking zones via smoke or flares. -- @param #AIRBOSS self --- @param #boolean switch If true or nil, menu is enabled. If false, menu is not available to players. +-- @param #boolean Switch If true or nil, menu is enabled. If false, menu is not available to players. -- @return #AIRBOSS self -function AIRBOSS:SetMenuMarkZones(switch) - if switch==nil or switch==true then - self.menumarkzones=true +function AIRBOSS:SetMenuMarkZones( Switch ) + if Switch == nil or Switch == true then + self.menumarkzones = true else - self.menumarkzones=false + self.menumarkzones = false end return self end --- Enable or disable F10 radio menu for marking zones via smoke. -- @param #AIRBOSS self --- @param #boolean switch If true or nil, menu is enabled. If false, menu is not available to players. +-- @param #boolean Switch If true or nil, menu is enabled. If false, menu is not available to players. -- @return #AIRBOSS self -function AIRBOSS:SetMenuSmokeZones(switch) - if switch==nil or switch==true then - self.menusmokezones=true +function AIRBOSS:SetMenuSmokeZones( Switch ) + if Switch == nil or Switch == true then + self.menusmokezones = true else - self.menusmokezones=false + self.menusmokezones = false end return self end --- Enable saving of player's trap sheets and specify an optional directory path. -- @param #AIRBOSS self --- @param #string path (Optional) Path where to save the trap sheets. --- @param #string prefix (Optional) Prefix for trap sheet files. File name will be saved as *prefix_aircrafttype-0001.csv*, *prefix_aircrafttype-0002.csv*, etc. +-- @param #string Path (Optional) Path where to save the trap sheets. +-- @param #string Prefix (Optional) Prefix for trap sheet files. File name will be saved as *prefix_aircrafttype-0001.csv*, *prefix_aircrafttype-0002.csv*, etc. -- @return #AIRBOSS self -function AIRBOSS:SetTrapSheet(path, prefix) +function AIRBOSS:SetTrapSheet( Path, Prefix ) if io then - self.trapsheet=true - self.trappath=path - self.trapprefix=prefix + self.trapsheet = true + self.trappath = Path + self.trapprefix = Prefix else - self:E(self.lid.."ERROR: io is not desanitized. Cannot save trap sheet.") + self:E( self.lid .. "ERROR: io is not desanitized. Cannot save trap sheet." ) end return self end --- Specify weather the mission has set static or dynamic weather. -- @param #AIRBOSS self --- @param #boolean switch If true or nil, mission uses static weather. If false, dynamic weather is used in this mission. +-- @param #boolean Switch If true or nil, mission uses static weather. If false, dynamic weather is used in this mission. -- @return #AIRBOSS self -function AIRBOSS:SetStaticWeather(switch) - if switch==nil or switch==true then - self.staticweather=true +function AIRBOSS:SetStaticWeather( Switch ) + if Switch == nil or Switch == true then + self.staticweather = true else - self.staticweather=false + self.staticweather = false end return self end - --- Disable automatic TACAN activation -- @param #AIRBOSS self -- @return #AIRBOSS self function AIRBOSS:SetTACANoff() - self.TACANon=false + self.TACANon = false return self end ---- Set TACAN channel of carrier. +--- Set TACAN channel of carrier and switches TACAN on. -- @param #AIRBOSS self --- @param #number channel TACAN channel. Default 74. --- @param #string mode TACAN mode, i.e. "X" or "Y". Default "X". --- @param #string morsecode Morse code identifier. Three letters, e.g. "STN". +-- @param #number Channel (Optional) TACAN channel. Default 74. +-- @param #string Mode (Optional) TACAN mode, i.e. "X" or "Y". Default "X". +-- @param #string MorseCode (Optional) Morse code identifier. Three letters, e.g. "STN". Default "STN". -- @return #AIRBOSS self -function AIRBOSS:SetTACAN(channel, mode, morsecode) +function AIRBOSS:SetTACAN( Channel, Mode, MorseCode ) - self.TACANchannel=channel or 74 - self.TACANmode=mode or "X" - self.TACANmorse=morsecode or "STN" - self.TACANon=true + self.TACANchannel = Channel or 74 + self.TACANmode = Mode or "X" + self.TACANmorse = MorseCode or "STN" + self.TACANon = true return self end @@ -100216,79 +107300,78 @@ end -- @param #AIRBOSS self -- @return #AIRBOSS self function AIRBOSS:SetICLSoff() - self.ICLSon=false + self.ICLSon = false return self end --- Set ICLS channel of carrier. -- @param #AIRBOSS self --- @param #number channel ICLS channel. Default 1. --- @param #string morsecode Morse code identifier. Three letters, e.g. "STN". Default "STN". +-- @param #number Channel (Optional) ICLS channel. Default 1. +-- @param #string MorseCode (Optional) Morse code identifier. Three letters, e.g. "STN". Default "STN". -- @return #AIRBOSS self -function AIRBOSS:SetICLS(channel, morsecode) +function AIRBOSS:SetICLS( Channel, MorseCode ) - self.ICLSchannel=channel or 1 - self.ICLSmorse=morsecode or "STN" - self.ICLSon=true + self.ICLSchannel = Channel or 1 + self.ICLSmorse = MorseCode or "STN" + self.ICLSon = true return self end - --- Set beacon (TACAN/ICLS) time refresh interfal in case the beacons die. -- @param #AIRBOSS self --- @param #number interval Time interval in seconds. Default 1200 sec = 20 min. +-- @param #number TimeInterval (Optional) Time interval in seconds. Default 1200 sec = 20 min. -- @return #AIRBOSS self -function AIRBOSS:SetBeaconRefresh(interval) - self.dTbeacon=interval or 20*60 + +function AIRBOSS:SetBeaconRefresh( TimeInterval ) + self.dTbeacon = TimeInterval or (20 * 60) return self end - --- Set LSO radio frequency and modulation. Default frequency is 264 MHz AM. -- @param #AIRBOSS self --- @param #number frequency Frequency in MHz. Default 264 MHz. --- @param #string modulation Modulation, i.e. "AM" (default) or "FM". +-- @param #number Frequency (Optional) Frequency in MHz. Default 264 MHz. +-- @param #string Modulation (Optional) Modulation, "AM" or "FM". Default "AM". -- @return #AIRBOSS self -function AIRBOSS:SetLSORadio(frequency, modulation) +function AIRBOSS:SetLSORadio( Frequency, Modulation ) - self.LSOFreq=(frequency or 264) - modulation=modulation or "AM" + self.LSOFreq = (Frequency or 264) + Modulation = Modulation or "AM" - if modulation=="FM" then - self.LSOModu=radio.modulation.FM + if Modulation == "FM" then + self.LSOModu = radio.modulation.FM else - self.LSOModu=radio.modulation.AM + self.LSOModu = radio.modulation.AM end - self.LSORadio={} --#AIRBOSS.Radio - self.LSORadio.frequency=self.LSOFreq - self.LSORadio.modulation=self.LSOModu - self.LSORadio.alias="LSO" + self.LSORadio = {} -- #AIRBOSS.Radio + self.LSORadio.frequency = self.LSOFreq + self.LSORadio.modulation = self.LSOModu + self.LSORadio.alias = "LSO" return self end --- Set carrier radio frequency and modulation. Default frequency is 305 MHz AM. -- @param #AIRBOSS self --- @param #number frequency Frequency in MHz. Default 305 MHz. --- @param #string modulation Modulation, i.e. "AM" (default) or "FM". +-- @param #number Frequency (Optional) Frequency in MHz. Default 305 MHz. +-- @param #string Modulation (Optional) Modulation, "AM" or "FM". Default "AM". -- @return #AIRBOSS self -function AIRBOSS:SetMarshalRadio(frequency, modulation) +function AIRBOSS:SetMarshalRadio( Frequency, Modulation ) - self.MarshalFreq=frequency or 305 - modulation=modulation or "AM" + self.MarshalFreq = Frequency or 305 + Modulation = Modulation or "AM" - if modulation=="FM" then - self.MarshalModu=radio.modulation.FM + if Modulation == "FM" then + self.MarshalModu = radio.modulation.FM else - self.MarshalModu=radio.modulation.AM + self.MarshalModu = radio.modulation.AM end - self.MarshalRadio={} --#AIRBOSS.Radio - self.MarshalRadio.frequency=self.MarshalFreq - self.MarshalRadio.modulation=self.MarshalModu - self.MarshalRadio.alias="MARSHAL" + self.MarshalRadio = {} -- #AIRBOSS.Radio + self.MarshalRadio.frequency = self.MarshalFreq + self.MarshalRadio.modulation = self.MarshalModu + self.MarshalRadio.alias = "MARSHAL" return self end @@ -100297,8 +107380,8 @@ end -- @param #AIRBOSS self -- @param #string unitname Name of the unit. -- @return #AIRBOSS self -function AIRBOSS:SetRadioUnitName(unitname) - self.senderac=unitname +function AIRBOSS:SetRadioUnitName( unitname ) + self.senderac = unitname return self end @@ -100306,8 +107389,8 @@ end -- @param #AIRBOSS self -- @param #string unitname Name of the unit. -- @return #AIRBOSS self -function AIRBOSS:SetRadioRelayLSO(unitname) - self.radiorelayLSO=unitname +function AIRBOSS:SetRadioRelayLSO( unitname ) + self.radiorelayLSO = unitname return self end @@ -100315,17 +107398,16 @@ end -- @param #AIRBOSS self -- @param #string unitname Name of the unit. -- @return #AIRBOSS self -function AIRBOSS:SetRadioRelayMarshal(unitname) - self.radiorelayMSH=unitname +function AIRBOSS:SetRadioRelayMarshal( unitname ) + self.radiorelayMSH = unitname return self end - --- Use user sound output instead of radio transmission for messages. Might be handy if radio transmissions are broken. -- @param #AIRBOSS self -- @return #AIRBOSS self function AIRBOSS:SetUserSoundRadio() - self.usersoundradio=true + self.usersoundradio = true return self end @@ -100333,34 +107415,33 @@ end -- @param #AIRBOSS self -- @param #number delay Delay in seconds be sound check starts. -- @return #AIRBOSS self -function AIRBOSS:SoundCheckLSO(delay) +function AIRBOSS:SoundCheckLSO( delay ) - if delay and delay>0 then + if delay and delay > 0 then -- Delayed call. - --SCHEDULER:New(nil, AIRBOSS.SoundCheckLSO, {self}, delay) - self:ScheduleOnce(delay, AIRBOSS.SoundCheckLSO, self) + -- SCHEDULER:New(nil, AIRBOSS.SoundCheckLSO, {self}, delay) + self:ScheduleOnce( delay, AIRBOSS.SoundCheckLSO, self ) else + local text = "Playing LSO sound files:" - local text="Playing LSO sound files:" - - for _name,_call in pairs(self.LSOCall) do - local call=_call --#AIRBOSS.RadioCall + for _name, _call in pairs( self.LSOCall ) do + local call = _call -- #AIRBOSS.RadioCall -- Debug text. - text=text..string.format("\nFile=%s.%s, duration=%.2f sec, loud=%s, subtitle=\"%s\".", call.file, call.suffix, call.duration, tostring(call.loud), call.subtitle) + text = text .. string.format( "\nFile=%s.%s, duration=%.2f sec, loud=%s, subtitle=\"%s\".", call.file, call.suffix, call.duration, tostring( call.loud ), call.subtitle ) -- Radio transmission to queue. - self:RadioTransmission(self.LSORadio, call, false) + self:RadioTransmission( self.LSORadio, call, false ) -- Also play the loud version. if call.loud then - self:RadioTransmission(self.LSORadio, call, true) + self:RadioTransmission( self.LSORadio, call, true ) end end -- Debug message. - self:I(self.lid..text) + self:I( self.lid .. text ) end end @@ -100369,34 +107450,33 @@ end -- @param #AIRBOSS self -- @param #number delay Delay in seconds be sound check starts. -- @return #AIRBOSS self -function AIRBOSS:SoundCheckMarshal(delay) +function AIRBOSS:SoundCheckMarshal( delay ) - if delay and delay>0 then + if delay and delay > 0 then -- Delayed call. - --SCHEDULER:New(nil, AIRBOSS.SoundCheckMarshal, {self}, delay) - self:ScheduleOnce(delay, AIRBOSS.SoundCheckMarshal, self) + -- SCHEDULER:New(nil, AIRBOSS.SoundCheckMarshal, {self}, delay) + self:ScheduleOnce( delay, AIRBOSS.SoundCheckMarshal, self ) else + local text = "Playing Marshal sound files:" - local text="Playing Marshal sound files:" - - for _name,_call in pairs(self.MarshalCall) do - local call=_call --#AIRBOSS.RadioCall + for _name, _call in pairs( self.MarshalCall ) do + local call = _call -- #AIRBOSS.RadioCall -- Debug text. - text=text..string.format("\nFile=%s.%s, duration=%.2f sec, loud=%s, subtitle=\"%s\".", call.file, call.suffix, call.duration, tostring(call.loud), call.subtitle) + text = text .. string.format( "\nFile=%s.%s, duration=%.2f sec, loud=%s, subtitle=\"%s\".", call.file, call.suffix, call.duration, tostring( call.loud ), call.subtitle ) -- Radio transmission to queue. - self:RadioTransmission(self.MarshalRadio, call, false) + self:RadioTransmission( self.MarshalRadio, call, false ) -- Also play the loud version. if call.loud then - self:RadioTransmission(self.MarshalRadio, call, true) + self:RadioTransmission( self.MarshalRadio, call, true ) end end -- Debug message. - self:I(self.lid..text) + self:I( self.lid .. text ) end end @@ -100405,11 +107485,11 @@ end -- @param #AIRBOSS self -- @param #number nmax Max number. Default 4. Minimum is 1, maximum is 6. -- @return #AIRBOSS self -function AIRBOSS:SetMaxLandingPattern(nmax) - nmax=nmax or 4 - nmax=math.max(nmax,1) - nmax=math.min(nmax,6) - self.Nmaxpattern=nmax +function AIRBOSS:SetMaxLandingPattern( nmax ) + nmax = nmax or 4 + nmax = math.max( nmax, 1 ) + nmax = math.min( nmax, 6 ) + self.Nmaxpattern = nmax return self end @@ -100418,9 +107498,9 @@ end -- @param #AIRBOSS self -- @param #number nmax Max number of stacks available to players and AI flights. Default 3, i.e. angels 2, 3, 4. Minimum is 1. -- @return #AIRBOSS self -function AIRBOSS:SetMaxMarshalStacks(nmax) - self.Nmaxmarshal=nmax or 3 - self.Nmaxmarshal=math.max(self.Nmaxmarshal, 1) +function AIRBOSS:SetMaxMarshalStacks( nmax ) + self.Nmaxmarshal = nmax or 3 + self.Nmaxmarshal = math.max( self.Nmaxmarshal, 1 ) return self end @@ -100428,11 +107508,11 @@ end -- @param #AIRBOSS self -- @param #number nmax Number of max allowed members including the lead itself. For example, Nmax=2 means a section lead plus one member. -- @return #AIRBOSS self -function AIRBOSS:SetMaxSectionSize(nmax) - nmax=nmax or 2 - nmax=math.max(nmax,1) - nmax=math.min(nmax,4) - self.NmaxSection=nmax-1 -- We substract one because internally the section lead is not counted! +function AIRBOSS:SetMaxSectionSize( nmax ) + nmax = nmax or 2 + nmax = math.max( nmax, 1 ) + nmax = math.min( nmax, 4 ) + self.NmaxSection = nmax - 1 -- We substract one because internally the section lead is not counted! return self end @@ -100440,38 +107520,54 @@ end -- @param #AIRBOSS self -- @param #number nmax Number of max allowed flights per stack. Default is two. Minimum is one, maximum is 4. -- @return #AIRBOSS self -function AIRBOSS:SetMaxFlightsPerStack(nmax) - nmax=nmax or 2 - nmax=math.max(nmax,1) - nmax=math.min(nmax,4) - self.NmaxStack=nmax +function AIRBOSS:SetMaxFlightsPerStack( nmax ) + nmax = nmax or 2 + nmax = math.max( nmax, 1 ) + nmax = math.min( nmax, 4 ) + self.NmaxStack = nmax return self end - --- Handle AI aircraft. -- @param #AIRBOSS self -- @return #AIRBOSS self function AIRBOSS:SetHandleAION() - self.handleai=true + self.handleai = true return self end +--- Will play the inbound calls, commencing, initial, etc. from the player when requesteing marshal +-- @param #AIRBOSS self +-- @param #AIRBOSS status Boolean to activate (true) / deactivate (false) the radio inbound calls (default is ON) +-- @return #AIRBOSS self +function AIRBOSS:SetExtraVoiceOvers(status) + self.xtVoiceOvers=status + return self +end + +--- Will simulate the inbound call, commencing, initial, etc from the AI when requested by Airboss +-- @param #AIRBOSS self +-- @param #AIRBOSS status Boolean to activate (true) / deactivate (false) the radio inbound calls (default is ON) +-- @return #AIRBOSS self +function AIRBOSS:SetExtraVoiceOversAI(status) + self.xtVoiceOversAI=status + return self +end + --- Do not handle AI aircraft. -- @param #AIRBOSS self -- @return #AIRBOSS self function AIRBOSS:SetHandleAIOFF() - self.handleai=false + self.handleai = false return self end - --- Define recovery tanker associated with the carrier. -- @param #AIRBOSS self -- @param Ops.RecoveryTanker#RECOVERYTANKER recoverytanker Recovery tanker object. -- @return #AIRBOSS self -function AIRBOSS:SetRecoveryTanker(recoverytanker) - self.tanker=recoverytanker +function AIRBOSS:SetRecoveryTanker( recoverytanker ) + self.tanker = recoverytanker return self end @@ -100479,8 +107575,8 @@ end -- @param #AIRBOSS self -- @param Ops.RecoveryTanker#RECOVERYTANKER awacs AWACS (recovery tanker) object. -- @return #AIRBOSS self -function AIRBOSS:SetAWACS(awacs) - self.awacs=awacs +function AIRBOSS:SetAWACS( awacs ) + self.awacs = awacs return self end @@ -100492,23 +107588,23 @@ end -- @param #AIRBOSS self -- @param #string skill Player skill. Default "Naval Aviator". -- @return #AIRBOSS self -function AIRBOSS:SetDefaultPlayerSkill(skill) +function AIRBOSS:SetDefaultPlayerSkill( skill ) -- Set skill or normal. - self.defaultskill=skill or AIRBOSS.Difficulty.NORMAL + self.defaultskill = skill or AIRBOSS.Difficulty.NORMAL -- Check that defualt skill is valid. - local gotit=false - for _,_skill in pairs(AIRBOSS.Difficulty) do - if _skill==self.defaultskill then - gotit=true + local gotit = false + for _, _skill in pairs( AIRBOSS.Difficulty ) do + if _skill == self.defaultskill then + gotit = true end end -- If invalid user input, fall back to normal. if not gotit then - self.defaultskill=AIRBOSS.Difficulty.NORMAL - self:E(self.lid..string.format("ERROR: Invalid default skill = %s. Resetting to Naval Aviator.", tostring(skill))) + self.defaultskill = AIRBOSS.Difficulty.NORMAL + self:E( self.lid .. string.format( "ERROR: Invalid default skill = %s. Resetting to Naval Aviator.", tostring( skill ) ) ) end return self @@ -100519,10 +107615,10 @@ end -- @param #string path Path where to save the asset data file. Default is the DCS root installation directory or your "Saved Games\\DCS" folder if lfs was desanitized. -- @param #string filename File name. Default is generated automatically from airboss carrier name/alias. -- @return #AIRBOSS self -function AIRBOSS:SetAutoSave(path, filename) - self.autosave=true - self.autosavepath=path - self.autosavefile=filename +function AIRBOSS:SetAutoSave( path, filename ) + self.autosave = true + self.autosavepath = path + self.autosavefile = filename return self end @@ -100530,7 +107626,7 @@ end -- @param #AIRBOSS self -- @return #AIRBOSS self function AIRBOSS:SetDebugModeON() - self.Debug=true + self.Debug = true return self end @@ -100538,11 +107634,11 @@ end -- @param #AIRBOSS self -- @param #boolean switch If true or nil, patrol until the end of time. If false, go along the waypoints once and stop. -- @return #AIRBOSS self -function AIRBOSS:SetPatrolAdInfinitum(switch) - if switch==false then - self.adinfinitum=false +function AIRBOSS:SetPatrolAdInfinitum( switch ) + if switch == false then + self.adinfinitum = false else - self.adinfinitum=true + self.adinfinitum = true end return self end @@ -100551,8 +107647,8 @@ end -- @param #AIRBOSS self -- @param #number declination Declination in degrees or nil for default declination of the map. -- @return #AIRBOSS self -function AIRBOSS:SetMagneticDeclination(declination) - self.magvar=declination or UTILS.GetMagneticDeclination() +function AIRBOSS:SetMagneticDeclination( declination ) + self.magvar = declination or UTILS.GetMagneticDeclination() return self end @@ -100560,7 +107656,7 @@ end -- @param #AIRBOSS self -- @return #AIRBOSS self function AIRBOSS:SetDebugModeOFF() - self.Debug=false + self.Debug = false return self end @@ -100569,12 +107665,12 @@ end -- @param #boolean InSeconds If true, abs. mission time seconds is returned. Default is a clock #string. -- @return #string Clock start (or start time in abs. seconds). -- @return #string Clock stop (or stop time in abs. seconds). -function AIRBOSS:GetNextRecoveryTime(InSeconds) +function AIRBOSS:GetNextRecoveryTime( InSeconds ) if self.recoverywindow then if InSeconds then return self.recoverywindow.START, self.recoverywindow.STOP else - return UTILS.SecondsToClock(self.recoverywindow.START), UTILS.SecondsToClock(self.recoverywindow.STOP) + return UTILS.SecondsToClock( self.recoverywindow.START ), UTILS.SecondsToClock( self.recoverywindow.STOP ) end else if InSeconds then @@ -100589,42 +107685,42 @@ end -- @param #AIRBOSS self -- @return #boolean If true, time slot for recovery is open. function AIRBOSS:IsRecovering() - return self:is("Recovering") + return self:is( "Recovering" ) end --- Check if carrier is idle, i.e. no operations are carried out. -- @param #AIRBOSS self -- @return #boolean If true, carrier is in idle state. function AIRBOSS:IsIdle() - return self:is("Idle") + return self:is( "Idle" ) end --- Check if recovery of aircraft is paused. -- @param #AIRBOSS self -- @return #boolean If true, recovery is paused function AIRBOSS:IsPaused() - return self:is("Paused") + return self:is( "Paused" ) end --- Activate TACAN and ICLS beacons. -- @param #AIRBOSS self function AIRBOSS:_ActivateBeacons() - self:T(self.lid..string.format("Activating Beacons (TACAN=%s, ICLS=%s)", tostring(self.TACANon), tostring(self.ICLSon))) + self:T( self.lid .. string.format( "Activating Beacons (TACAN=%s, ICLS=%s)", tostring( self.TACANon ), tostring( self.ICLSon ) ) ) -- Activate TACAN. if self.TACANon then - self:I(self.lid..string.format("Activating TACAN Channel %d%s (%s)", self.TACANchannel, self.TACANmode, self.TACANmorse)) - self.beacon:ActivateTACAN(self.TACANchannel, self.TACANmode, self.TACANmorse, true) + self:I( self.lid .. string.format( "Activating TACAN Channel %d%s (%s)", self.TACANchannel, self.TACANmode, self.TACANmorse ) ) + self.beacon:ActivateTACAN( self.TACANchannel, self.TACANmode, self.TACANmorse, true ) end -- Activate ICLS. if self.ICLSon then - self:I(self.lid..string.format("Activating ICLS Channel %d (%s)", self.ICLSchannel, self.ICLSmorse)) - self.beacon:ActivateICLS(self.ICLSchannel, self.ICLSmorse) + self:I( self.lid .. string.format( "Activating ICLS Channel %d (%s)", self.ICLSchannel, self.ICLSmorse ) ) + self.beacon:ActivateICLS( self.ICLSchannel, self.ICLSmorse ) end -- Set time stamp. - self.Tbeacon=timer.getTime() + self.Tbeacon = timer.getTime() end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -100636,63 +107732,63 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function AIRBOSS:onafterStart(From, Event, To) +function AIRBOSS:onafterStart( From, Event, To ) -- Events are handled my MOOSE. - self:I(self.lid..string.format("Starting AIRBOSS v%s for carrier unit %s of type %s on map %s", AIRBOSS.version, self.carrier:GetName(), self.carriertype, self.theatre)) + self:I( self.lid .. string.format( "Starting AIRBOSS v%s for carrier unit %s of type %s on map %s", AIRBOSS.version, self.carrier:GetName(), self.carriertype, self.theatre ) ) -- Activate TACAN and ICLS if desired. self:_ActivateBeacons() -- Schedule radio queue checks. - --self.RQLid=self.radiotimer:Schedule(nil, AIRBOSS._CheckRadioQueue, {self, self.RQLSO, "LSO"}, 1, 0.1) - --self.RQMid=self.radiotimer:Schedule(nil, AIRBOSS._CheckRadioQueue, {self, self.RQMarshal, "MARSHAL"}, 1, 0.1) + -- self.RQLid=self.radiotimer:Schedule(nil, AIRBOSS._CheckRadioQueue, {self, self.RQLSO, "LSO"}, 1, 0.1) + -- self.RQMid=self.radiotimer:Schedule(nil, AIRBOSS._CheckRadioQueue, {self, self.RQMarshal, "MARSHAL"}, 1, 0.1) - --self:I("FF: starting timer.scheduleFunction") - --timer.scheduleFunction(AIRBOSS._CheckRadioQueueT, {airboss=self, radioqueue=self.RQLSO, name="LSO"}, timer.getTime()+1) - --timer.scheduleFunction(AIRBOSS._CheckRadioQueueT, {airboss=self, radioqueue=self.RQMarshal, name="MARSHAL"}, timer.getTime()+1) + -- self:I("FF: starting timer.scheduleFunction") + -- timer.scheduleFunction(AIRBOSS._CheckRadioQueueT, {airboss=self, radioqueue=self.RQLSO, name="LSO"}, timer.getTime()+1) + -- timer.scheduleFunction(AIRBOSS._CheckRadioQueueT, {airboss=self, radioqueue=self.RQMarshal, name="MARSHAL"}, timer.getTime()+1) -- Initial carrier position and orientation. - self.Cposition=self:GetCoordinate() - self.Corientation=self.carrier:GetOrientationX() - self.Corientlast=self.Corientation - self.Tpupdate=timer.getTime() + self.Cposition = self:GetCoordinate() + self.Corientation = self.carrier:GetOrientationX() + self.Corientlast = self.Corientation + self.Tpupdate = timer.getTime() -- Check if no recovery window is set. DISABLED! - if #self.recoverytimes==0 and false then + if #self.recoverytimes == 0 and false then -- Open window in 15 minutes for 3 hours. - local Topen=timer.getAbsTime()+15*60 - local Tclose=Topen+3*60*60 + local Topen = timer.getAbsTime() + 15 * 60 + local Tclose = Topen + 3 * 60 * 60 -- Add window. - self:AddRecoveryWindow(UTILS.SecondsToClock(Topen), UTILS.SecondsToClock(Tclose)) + self:AddRecoveryWindow( UTILS.SecondsToClock( Topen ), UTILS.SecondsToClock( Tclose ) ) end -- Check Recovery time.s self:_CheckRecoveryTimes() -- Time stamp for checking queues. We substract 60 seconds so the routine is called right after status is called the first time. - self.Tqueue=timer.getTime()-60 + self.Tqueue = timer.getTime() - 60 -- Handle events. - self:HandleEvent(EVENTS.Birth) - self:HandleEvent(EVENTS.Land) - self:HandleEvent(EVENTS.EngineShutdown) - self:HandleEvent(EVENTS.Takeoff) - self:HandleEvent(EVENTS.Crash) - self:HandleEvent(EVENTS.Ejection) - self:HandleEvent(EVENTS.PlayerLeaveUnit, self._PlayerLeft) - self:HandleEvent(EVENTS.MissionEnd) - self:HandleEvent(EVENTS.RemoveUnit) + self:HandleEvent( EVENTS.Birth ) + self:HandleEvent( EVENTS.Land ) + self:HandleEvent( EVENTS.EngineShutdown ) + self:HandleEvent( EVENTS.Takeoff ) + self:HandleEvent( EVENTS.Crash ) + self:HandleEvent( EVENTS.Ejection ) + self:HandleEvent( EVENTS.PlayerLeaveUnit, self._PlayerLeft ) + self:HandleEvent( EVENTS.MissionEnd ) + self:HandleEvent( EVENTS.RemoveUnit ) - --self.StatusScheduler=SCHEDULER:New(self) - --self.StatusScheduler:Schedule(self, self._Status, {}, 1, 0.5) + -- self.StatusScheduler=SCHEDULER:New(self) + -- self.StatusScheduler:Schedule(self, self._Status, {}, 1, 0.5) - self.StatusTimer=TIMER:New(self._Status, self):Start(2, 0.5) + self.StatusTimer = TIMER:New( self._Status, self ):Start( 2, 0.5 ) -- Start status check in 1 second. - self:__Status(1) + self:__Status( 1 ) end --- On after Status event. Checks for new flights, updates queue and checks player status. @@ -100700,65 +107796,64 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function AIRBOSS:onafterStatus(From, Event, To) +function AIRBOSS:onafterStatus( From, Event, To ) -- Get current time. - local time=timer.getTime() + local time = timer.getTime() -- Update marshal and pattern queue every 30 seconds. - if time-self.Tqueue>self.dTqueue then + if time - self.Tqueue > self.dTqueue then -- Get time. - local clock=UTILS.SecondsToClock(timer.getAbsTime()) - local eta=UTILS.SecondsToClock(self:_GetETAatNextWP()) + local clock = UTILS.SecondsToClock( timer.getAbsTime() ) + local eta = UTILS.SecondsToClock( self:_GetETAatNextWP() ) -- Current heading and position of the carrier. - local hdg=self:GetHeading() - local pos=self:GetCoordinate() - local speed=self.carrier:GetVelocityKNOTS() + local hdg = self:GetHeading() + local pos = self:GetCoordinate() + local speed = self.carrier:GetVelocityKNOTS() -- Check water is ahead. - local collision=false --self:_CheckCollisionCoord(pos:Translate(self.collisiondist, hdg)) + local collision = false -- self:_CheckCollisionCoord(pos:Translate(self.collisiondist, hdg)) - local holdtime=0 + local holdtime = 0 if self.holdtimestamp then - holdtime=timer.getTime()-self.holdtimestamp + holdtime = timer.getTime() - self.holdtimestamp end -- Check if carrier is stationary. - local NextWP=self:_GetNextWaypoint() - local ExpectedSpeed=UTILS.MpsToKnots(NextWP:GetVelocity()) - if speed<0.5 and ExpectedSpeed>0 and not (self.detour or self.turnintowind) then + local NextWP = self:_GetNextWaypoint() + local ExpectedSpeed = UTILS.MpsToKnots( NextWP:GetVelocity() ) + if speed < 0.5 and ExpectedSpeed > 0 and not (self.detour or self.turnintowind) then if not self.holdtimestamp then - self:E(self.lid..string.format("Carrier came to an unexpected standstill. Trying to re-route in 3 min. Speed=%.1f knots, expected=%.1f knots", speed, ExpectedSpeed)) - self.holdtimestamp=timer.getTime() + self:E( self.lid .. string.format( "Carrier came to an unexpected standstill. Trying to re-route in 3 min. Speed=%.1f knots, expected=%.1f knots", speed, ExpectedSpeed ) ) + self.holdtimestamp = timer.getTime() else - if holdtime>3*60 then - local coord=self:GetCoordinate():Translate(500, hdg+10) - --coord:MarkToAll("Re-route after standstill.") - self:CarrierResumeRoute(coord) - self.holdtimestamp=nil + if holdtime > 3 * 60 then + local coord = self:GetCoordinate():Translate( 500, hdg + 10 ) + -- coord:MarkToAll("Re-route after standstill.") + self:CarrierResumeRoute( coord ) + self.holdtimestamp = nil end end end -- Debug info. - local text=string.format("Time %s - Status %s (case=%d) - Speed=%.1f kts - Heading=%d - WP=%d - ETA=%s - Turning=%s - Collision Warning=%s - Detour=%s - Turn Into Wind=%s - Holdtime=%d sec", - clock, self:GetState(), self.case, speed, hdg, self.currentwp, eta, tostring(self.turning), tostring(collision), tostring(self.detour), tostring(self.turnintowind), holdtime) - self:T(self.lid..text) + local text = string.format( "Time %s - Status %s (case=%d) - Speed=%.1f kts - Heading=%d - WP=%d - ETA=%s - Turning=%s - Collision Warning=%s - Detour=%s - Turn Into Wind=%s - Holdtime=%d sec", clock, self:GetState(), self.case, speed, hdg, self.currentwp, eta, tostring( self.turning ), tostring( collision ), tostring( self.detour ), tostring( self.turnintowind ), holdtime ) + self:T( self.lid .. text ) -- Players online: - text="Players:" - local i=0 - for _name,_player in pairs(self.players) do - i=i+1 - local player=_player --#AIRBOSS.FlightGroup - text=text..string.format("\n%d.) %s: Step=%s, Unit=%s, Airframe=%s", i, tostring(player.name), tostring(player.step), tostring(player.unitname), tostring(player.actype)) + text = "Players:" + local i = 0 + for _name, _player in pairs( self.players ) do + i = i + 1 + local player = _player -- #AIRBOSS.FlightGroup + text = text .. string.format( "\n%d.) %s: Step=%s, Unit=%s, Airframe=%s", i, tostring( player.name ), tostring( player.step ), tostring( player.unitname ), tostring( player.actype ) ) end - if i==0 then - text=text.." none" + if i == 0 then + text = text .. " none" end - self:I(self.lid..text) + self:I( self.lid .. text ) -- Check for collision. if collision then @@ -100767,24 +107862,23 @@ function AIRBOSS:onafterStatus(From, Event, To) if self.turnintowind then -- Carrier resumes its initial route. This disables turnintowind switch. - self:CarrierResumeRoute(self.Creturnto) + self:CarrierResumeRoute( self.Creturnto ) -- Since current window would stay open, we disable the WIND switch. if self:IsRecovering() and self.recoverywindow and self.recoverywindow.WIND then -- Disable turn into the wind for this window so that we do not do this all over again. - self.recoverywindow.WIND=false + self.recoverywindow.WIND = false end end end - -- Check recovery times and start/stop recovery mode if necessary. self:_CheckRecoveryTimes() -- Remove dead/zombie flight groups. Player leaving the server whilst in pattern etc. - --self:_RemoveDeadFlightGroups() + -- self:_RemoveDeadFlightGroups() -- Scan carrier zone for new aircraft. self:_ScanCarrierZone() @@ -100799,16 +107893,16 @@ function AIRBOSS:onafterStatus(From, Event, To) self:_CheckPatternUpdate() -- Time stamp. - self.Tqueue=time + self.Tqueue = time end -- (Re-)activate TACAN and ICLS channels. - if time-self.Tbeacon>self.dTbeacon then + if time - self.Tbeacon > self.dTbeacon then self:_ActivateBeacons() end -- Call status every ~0.5 seconds. - self:__Status(-30) + self:__Status( -30 ) end @@ -100829,27 +107923,27 @@ end function AIRBOSS:_CheckAIStatus() -- Loop over all flights in Marshal stack. - for _,_flight in pairs(self.Qmarshal) do - local flight=_flight --#AIRBOSS.FlightGroup + for _, _flight in pairs( self.Qmarshal ) do + local flight = _flight -- #AIRBOSS.FlightGroup -- Only AI! if flight.ai then -- Get fuel amount in %. - local fuel=flight.group:GetFuelMin()*100 + local fuel = flight.group:GetFuelMin() * 100 -- Debug text. - local text=string.format("Group %s fuel=%.1f %%", flight.groupname, fuel) - self:T3(self.lid..text) + local text = string.format( "Group %s fuel=%.1f %%", flight.groupname, fuel ) + self:T3( self.lid .. text ) -- Check if flight is low on fuel and not yet refueling. - if self.lowfuelAI and fuel=recovery.START then + if time >= recovery.START then -- Start time has passed. - if time0 then + if npattern > 0 then -- Extend recovery time. 5 min per flight. - local extmin=5*npattern - recovery.STOP=recovery.STOP+extmin*60 + local extmin = 5 * npattern + recovery.STOP = recovery.STOP + extmin * 60 - local text=string.format("We still got flights in the pattern.\nRecovery time prolonged by %d minutes.\nNow get your act together and no more bolters!", extmin) - self:MessageToPattern(text, "AIRBOSS", "99", 10, false, nil) + local text = string.format( "We still got flights in the pattern.\nRecovery time prolonged by %d minutes.\nNow get your act together and no more bolters!", extmin ) + self:MessageToPattern( text, "AIRBOSS", "99", 10, false, nil ) else -- Set carrier to idle. self:RecoveryStop() - state="closing now" + state = "closing now" -- Closed. - recovery.OPEN=false + recovery.OPEN = false -- Window just closed. - recovery.OVER=true + recovery.OVER = true end else -- Carrier is already idle. - state="closed" + state = "closed" end end else -- This recovery is in the future. - state="in the future" + state = "in the future" -- This is the next to come as we sorted by start time. - if nextwindow==nil then - nextwindow=recovery - state="next in line" + if nextwindow == nil then + nextwindow = recovery + state = "next in line" end end -- Debug text. - text=text..string.format("\n- Start=%s Stop=%s Case=%d Offset=%d Open=%s Closed=%s Status=\"%s\"", Cstart, Cstop, recovery.CASE, recovery.OFFSET, tostring(recovery.OPEN), tostring(recovery.OVER), state) + text = text .. string.format( "\n- Start=%s Stop=%s Case=%d Offset=%d Open=%s Closed=%s Status=\"%s\"", Cstart, Cstop, recovery.CASE, recovery.OFFSET, tostring( recovery.OPEN ), tostring( recovery.OVER ), state ) end -- Debug output. - self:T(self.lid..text) + self:T( self.lid .. text ) -- Current recovery window. - self.recoverywindow=nil - + self.recoverywindow = nil if self:IsIdle() then ----------------------------------------------------------------------------------------------------------------- @@ -101142,48 +108237,48 @@ function AIRBOSS:_CheckRecoveryTimes() if nextwindow then -- Set case and offset of the next window. - self:RecoveryCase(nextwindow.CASE, nextwindow.OFFSET) + self:RecoveryCase( nextwindow.CASE, nextwindow.OFFSET ) -- Check if time is less than 5 minutes. - if nextwindow.WIND and nextwindow.START-time 5° different from the current heading. - local hdg=self:GetHeading() - local wind=self:GetHeadingIntoWind() - local delta=self:_GetDeltaHeading(hdg, wind) - local uturn=delta>5 + local hdg = self:GetHeading() + local wind = self:GetHeadingIntoWind() + local delta = self:_GetDeltaHeading( hdg, wind ) + local uturn = delta > 5 -- Check if wind is actually blowing (0.1 m/s = 0.36 km/h = 0.2 knots) - local _,vwind=self:GetWind() - if vwind<0.1 then - uturn=false + local _, vwind = self:GetWind() + if vwind < 0.1 then + uturn = false end -- U-turn disabled by user input. if not nextwindow.UTURN then - uturn=false + uturn = false end - --Debug info - self:T(self.lid..string.format("Heading=%03d°, Wind=%03d° %.1f kts, Delta=%03d° ==> U-turn=%s", hdg, wind,UTILS.MpsToKnots(vwind), delta, tostring(uturn))) + -- Debug info + self:T( self.lid .. string.format( "Heading=%03d°, Wind=%03d° %.1f kts, Delta=%03d° ==> U-turn=%s", hdg, wind, UTILS.MpsToKnots( vwind ), delta, tostring( uturn ) ) ) -- Time into the wind 1 day or if longer recovery time + the 5 min early. - local t=math.max(nextwindow.STOP-nextwindow.START+self.dTturn, 60*60*24) + local t = math.max( nextwindow.STOP - nextwindow.START + self.dTturn, 60 * 60 * 24 ) -- Recovery wind on deck in knots. - local v=UTILS.KnotsToMps(nextwindow.SPEED) + local v = UTILS.KnotsToMps( nextwindow.SPEED ) -- Check that we do not go above max possible speed. - local vmax=self.carrier:GetSpeedMax()/3.6 -- convert to m/s - v=math.min(v,vmax) + local vmax = self.carrier:GetSpeedMax() / 3.6 -- convert to m/s + v = math.min( v, vmax ) -- Route carrier into the wind. Sets self.turnintowind=true - self:CarrierTurnIntoWind(t, v, uturn) + self:CarrierTurnIntoWind( t, v, uturn ) end -- Set current recovery window. - self.recoverywindow=nextwindow + self.recoverywindow = nextwindow else -- No next window. Set default values. @@ -101196,29 +108291,29 @@ function AIRBOSS:_CheckRecoveryTimes() ------------------------------------------------------------------------------------- if currwindow then - self.recoverywindow=currwindow + self.recoverywindow = currwindow else - self.recoverywindow=nextwindow + self.recoverywindow = nextwindow end end - self:T2({"FF", recoverywindow=self.recoverywindow}) + self:T2( { "FF", recoverywindow = self.recoverywindow } ) end --- Get section lead of a flight. ---@param #AIRBOSS self ---@param #AIRBOSS.FlightGroup flight ---@return #AIRBOSS.FlightGroup The leader of the section. Could be the flight itself. ---@return #boolean If true, flight is lead. -function AIRBOSS:_GetFlightLead(flight) +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight +-- @return #AIRBOSS.FlightGroup The leader of the section. Could be the flight itself. +-- @return #boolean If true, flight is lead. +function AIRBOSS:_GetFlightLead( flight ) - if flight.name~=flight.seclead then + if flight.name ~= flight.seclead then -- Section lead of flight. - local lead=self.players[flight.seclead] - return lead,false + local lead = self.players[flight.seclead] + return lead, false else -- Flight without section or section lead. - return flight,true + return flight, true end end @@ -101230,15 +108325,15 @@ end -- @param #string To To state. -- @param #number Case The recovery case (1, 2 or 3) to switch to. -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. -function AIRBOSS:onbeforeRecoveryCase(From, Event, To, Case, Offset) +function AIRBOSS:onbeforeRecoveryCase( From, Event, To, Case, Offset ) -- Input or default value. - Case=Case or self.defaultcase + Case = Case or self.defaultcase -- Input or default value - Offset=Offset or self.defaultoffset + Offset = Offset or self.defaultoffset - if Case==self.case and Offset==self.holdingoffset then + if Case == self.case and Offset == self.holdingoffset then return false end @@ -101252,46 +108347,46 @@ end -- @param #string To To state. -- @param #number Case The recovery case (1, 2 or 3) to switch to. -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. -function AIRBOSS:onafterRecoveryCase(From, Event, To, Case, Offset) +function AIRBOSS:onafterRecoveryCase( From, Event, To, Case, Offset ) -- Input or default value. - Case=Case or self.defaultcase + Case = Case or self.defaultcase -- Input or default value - Offset=Offset or self.defaultoffset + Offset = Offset or self.defaultoffset -- Debug output. - local text=string.format("Switching recovery case %d ==> %d", self.case, Case) - if Case>1 then - text=text..string.format(" Holding offset angle %d degrees.", Offset) + local text = string.format( "Switching recovery case %d ==> %d", self.case, Case ) + if Case > 1 then + text = text .. string.format( " Holding offset angle %d degrees.", Offset ) end - MESSAGE:New(text, 20, self.alias):ToAllIf(self.Debug) - self:T(self.lid..text) + MESSAGE:New( text, 20, self.alias ):ToAllIf( self.Debug ) + self:T( self.lid .. text ) -- Set new recovery case. - self.case=Case + self.case = Case -- Set holding offset. - self.holdingoffset=Offset + self.holdingoffset = Offset -- Update case of all flights not in Marshal or Pattern queue. - for _,_flight in pairs(self.flights) do - local flight=_flight --#AIRBOSS.FlightGroup - if not (self:_InQueue(self.Qmarshal, flight.group) or self:_InQueue(self.Qpattern, flight.group)) then + for _, _flight in pairs( self.flights ) do + local flight = _flight -- #AIRBOSS.FlightGroup + if not (self:_InQueue( self.Qmarshal, flight.group ) or self:_InQueue( self.Qpattern, flight.group )) then -- Also not for section members. These are not in the marshal or pattern queue if the lead is. - if flight.name~=flight.seclead then - local lead=self.players[flight.seclead] + if flight.name ~= flight.seclead then + local lead = self.players[flight.seclead] - if lead and not (self:_InQueue(self.Qmarshal, lead.group) or self:_InQueue(self.Qpattern, lead.group)) then + if lead and not (self:_InQueue( self.Qmarshal, lead.group ) or self:_InQueue( self.Qpattern, lead.group )) then -- This is section member and the lead is not in the Marshal or Pattern queue. - flight.case=self.case + flight.case = self.case end else -- This is a flight without section or the section lead. - flight.case=self.case + flight.case = self.case end @@ -101306,19 +108401,19 @@ end -- @param #string To To state. -- @param #number Case The recovery case (1, 2 or 3) to start. -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. -function AIRBOSS:onafterRecoveryStart(From, Event, To, Case, Offset) +function AIRBOSS:onafterRecoveryStart( From, Event, To, Case, Offset ) -- Input or default value. - Case=Case or self.defaultcase + Case = Case or self.defaultcase -- Input or default value. - Offset=Offset or self.defaultoffset + Offset = Offset or self.defaultoffset -- Radio message: "99, starting aircraft recovery case X ops. (Marshal radial XYZ degrees)" - self:_MarshalCallRecoveryStart(Case) + self:_MarshalCallRecoveryStart( Case ) -- Switch to case. - self:RecoveryCase(Case, Offset) + self:RecoveryCase( Case, Offset ) end --- On after "RecoveryStop" event. Recovery of aircraft is stopped and carrier switches to state "Idle". Running recovery window is deleted. @@ -101326,65 +108421,64 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function AIRBOSS:onafterRecoveryStop(From, Event, To) +function AIRBOSS:onafterRecoveryStop( From, Event, To ) -- Debug output. - self:T(self.lid..string.format("Stopping aircraft recovery.")) + self:T( self.lid .. string.format( "Stopping aircraft recovery." ) ) -- Recovery ops stopped message. - self:_MarshalCallRecoveryStopped(self.case) + self:_MarshalCallRecoveryStopped( self.case ) -- If carrier is currently heading into the wind, we resume the original route. if self.turnintowind then -- Coordinate to return to. - local coord=self.Creturnto + local coord = self.Creturnto -- No U-turn. - if self.recoverywindow and self.recoverywindow.UTURN==false then - coord=nil + if self.recoverywindow and self.recoverywindow.UTURN == false then + coord = nil end -- Carrier resumes route. - self:CarrierResumeRoute(coord) + self:CarrierResumeRoute( coord ) end -- Delete current recovery window if open. - if self.recoverywindow and self.recoverywindow.OPEN==true then - self.recoverywindow.OPEN=false - self.recoverywindow.OVER=true - self:DeleteRecoveryWindow(self.recoverywindow) + if self.recoverywindow and self.recoverywindow.OPEN == true then + self.recoverywindow.OPEN = false + self.recoverywindow.OVER = true + self:DeleteRecoveryWindow( self.recoverywindow ) end -- Check recovery windows. This sets self.recoverywindow to the next window. self:_CheckRecoveryTimes() end - --- On after "RecoveryPause" event. Recovery of aircraft is paused. Marshal queue stays intact. -- @param #AIRBOSS self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -- @param #number duration Duration of pause in seconds. After that recovery is resumed automatically. -function AIRBOSS:onafterRecoveryPause(From, Event, To, duration) +function AIRBOSS:onafterRecoveryPause( From, Event, To, duration ) -- Debug output. - self:T(self.lid..string.format("Pausing aircraft recovery.")) + self:T( self.lid .. string.format( "Pausing aircraft recovery." ) ) -- Message text if duration then -- Auto resume. - self:__RecoveryUnpause(duration) + self:__RecoveryUnpause( duration ) -- Time to resume. - local clock=UTILS.SecondsToClock(timer.getAbsTime()+duration) + local clock = UTILS.SecondsToClock( timer.getAbsTime() + duration ) -- Marshal call: "99, aircraft recovery paused and will be resume at XX:YY." - self:_MarshalCallRecoveryPausedResumedAt(clock) + self:_MarshalCallRecoveryPausedResumedAt( clock ) else - local text=string.format("aircraft recovery is paused until further notice.") + local text = string.format( "aircraft recovery is paused until further notice." ) -- Marshal call: "99, aircraft recovery paused until further notice." self:_MarshalCallRecoveryPausedNotice() @@ -101398,9 +108492,9 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function AIRBOSS:onafterRecoveryUnpause(From, Event, To) +function AIRBOSS:onafterRecoveryUnpause( From, Event, To ) -- Debug output. - self:T(self.lid..string.format("Unpausing aircraft recovery.")) + self:T( self.lid .. string.format( "Unpausing aircraft recovery." ) ) -- Resume recovery. self:_MarshalCallResumeRecovery() @@ -101413,9 +108507,9 @@ end -- @param #string Event Event. -- @param #string To To state. -- @param #number n Number of waypoint that was passed. -function AIRBOSS:onafterPassingWaypoint(From, Event, To, n) +function AIRBOSS:onafterPassingWaypoint( From, Event, To, n ) -- Debug output. - self:I(self.lid..string.format("Carrier passed waypoint %d.", n)) + self:I( self.lid .. string.format( "Carrier passed waypoint %d.", n ) ) end --- On after "Idle" event. Carrier goes to state "Idle". @@ -101423,9 +108517,9 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function AIRBOSS:onafterIdle(From, Event, To) +function AIRBOSS:onafterIdle( From, Event, To ) -- Debug output. - self:T(self.lid..string.format("Carrier goes to idle.")) + self:T( self.lid .. string.format( "Carrier goes to idle." ) ) end --- On after Stop event. Unhandle events. @@ -101433,18 +108527,18 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function AIRBOSS:onafterStop(From, Event, To) - self:I(self.lid..string.format("Stopping airboss script.")) +function AIRBOSS:onafterStop( From, Event, To ) + self:I( self.lid .. string.format( "Stopping airboss script." ) ) -- Unhandle events. - self:UnHandleEvent(EVENTS.Birth) - self:UnHandleEvent(EVENTS.Land) - self:UnHandleEvent(EVENTS.EngineShutdown) - self:UnHandleEvent(EVENTS.Takeoff) - self:UnHandleEvent(EVENTS.Crash) - self:UnHandleEvent(EVENTS.Ejection) - self:UnHandleEvent(EVENTS.PlayerLeaveUnit) - self:UnHandleEvent(EVENTS.MissionEnd) + self:UnHandleEvent( EVENTS.Birth ) + self:UnHandleEvent( EVENTS.Land ) + self:UnHandleEvent( EVENTS.EngineShutdown ) + self:UnHandleEvent( EVENTS.Takeoff ) + self:UnHandleEvent( EVENTS.Crash ) + self:UnHandleEvent( EVENTS.Ejection ) + self:UnHandleEvent( EVENTS.PlayerLeaveUnit ) + self:UnHandleEvent( EVENTS.MissionEnd ) self.CallScheduler:Clear() end @@ -101458,146 +108552,145 @@ end function AIRBOSS:_InitStennis() -- Carrier Parameters. - self.carrierparam.sterndist =-153 - self.carrierparam.deckheight = 19.06 + self.carrierparam.sterndist = -153 + self.carrierparam.deckheight = 19.06 -- Total size of the carrier (approx as rectangle). - self.carrierparam.totlength=310 -- Wiki says 332.8 meters overall length. - self.carrierparam.totwidthport=40 -- Wiki says 76.8 meters overall beam. - self.carrierparam.totwidthstarboard=30 + self.carrierparam.totlength = 310 -- Wiki says 332.8 meters overall length. + self.carrierparam.totwidthport = 40 -- Wiki says 76.8 meters overall beam. + self.carrierparam.totwidthstarboard = 30 -- Landing runway. - self.carrierparam.rwyangle = -9.1359 - self.carrierparam.rwylength = 225 - self.carrierparam.rwywidth = 20 + self.carrierparam.rwyangle = -9.1359 + self.carrierparam.rwylength = 225 + self.carrierparam.rwywidth = 20 -- Wires. - self.carrierparam.wire1 = 46 -- Distance from stern to first wire. - self.carrierparam.wire2 = 46+12 - self.carrierparam.wire3 = 46+24 - self.carrierparam.wire4 = 46+35 -- Last wire is strangely one meter closer. - + self.carrierparam.wire1 = 46 -- Distance from stern to first wire. + self.carrierparam.wire2 = 46 + 12 + self.carrierparam.wire3 = 46 + 24 + self.carrierparam.wire4 = 46 + 35 -- Last wire is strangely one meter closer. -- Platform at 5k. Reduce descent rate to 2000 ft/min to 1200 dirty up level flight. - self.Platform.name="Platform 5k" - self.Platform.Xmin=-UTILS.NMToMeters(22) -- Not more than 22 NM behind the boat. Last check was at 21 NM. - self.Platform.Xmax =nil - self.Platform.Zmin=-UTILS.NMToMeters(30) -- Not more than 30 NM port of boat. - self.Platform.Zmax= UTILS.NMToMeters(30) -- Not more than 30 NM starboard of boat. - self.Platform.LimitXmin=nil -- Limits via zone - self.Platform.LimitXmax=nil - self.Platform.LimitZmin=nil - self.Platform.LimitZmax=nil + self.Platform.name = "Platform 5k" + self.Platform.Xmin = -UTILS.NMToMeters( 22 ) -- Not more than 22 NM behind the boat. Last check was at 21 NM. + self.Platform.Xmax = nil + self.Platform.Zmin = -UTILS.NMToMeters( 30 ) -- Not more than 30 NM port of boat. + self.Platform.Zmax = UTILS.NMToMeters( 30 ) -- Not more than 30 NM starboard of boat. + self.Platform.LimitXmin = nil -- Limits via zone + self.Platform.LimitXmax = nil + self.Platform.LimitZmin = nil + self.Platform.LimitZmax = nil -- Level out at 1200 ft and dirty up. - self.DirtyUp.name="Dirty Up" - self.DirtyUp.Xmin=-UTILS.NMToMeters(21) -- Not more than 21 NM behind the boat. - self.DirtyUp.Xmax= nil - self.DirtyUp.Zmin=-UTILS.NMToMeters(30) -- Not more than 30 NM port of boat. - self.DirtyUp.Zmax= UTILS.NMToMeters(30) -- Not more than 30 NM starboard of boat. - self.DirtyUp.LimitXmin=nil -- Limits via zone - self.DirtyUp.LimitXmax=nil - self.DirtyUp.LimitZmin=nil - self.DirtyUp.LimitZmax=nil + self.DirtyUp.name = "Dirty Up" + self.DirtyUp.Xmin = -UTILS.NMToMeters( 21 ) -- Not more than 21 NM behind the boat. + self.DirtyUp.Xmax = nil + self.DirtyUp.Zmin = -UTILS.NMToMeters( 30 ) -- Not more than 30 NM port of boat. + self.DirtyUp.Zmax = UTILS.NMToMeters( 30 ) -- Not more than 30 NM starboard of boat. + self.DirtyUp.LimitXmin = nil -- Limits via zone + self.DirtyUp.LimitXmax = nil + self.DirtyUp.LimitZmin = nil + self.DirtyUp.LimitZmax = nil -- Intercept glide slope and follow bullseye. - self.Bullseye.name="Bullseye" - self.Bullseye.Xmin=-UTILS.NMToMeters(11) -- Not more than 11 NM behind the boat. Last check was at 10 NM. - self.Bullseye.Xmax= nil - self.Bullseye.Zmin=-UTILS.NMToMeters(30) -- Not more than 30 NM port. - self.Bullseye.Zmax= UTILS.NMToMeters(30) -- Not more than 30 NM starboard. - self.Bullseye.LimitXmin=nil -- Limits via zone. - self.Bullseye.LimitXmax=nil - self.Bullseye.LimitZmin=nil - self.Bullseye.LimitZmax=nil + self.Bullseye.name = "Bullseye" + self.Bullseye.Xmin = -UTILS.NMToMeters( 11 ) -- Not more than 11 NM behind the boat. Last check was at 10 NM. + self.Bullseye.Xmax = nil + self.Bullseye.Zmin = -UTILS.NMToMeters( 30 ) -- Not more than 30 NM port. + self.Bullseye.Zmax = UTILS.NMToMeters( 30 ) -- Not more than 30 NM starboard. + self.Bullseye.LimitXmin = nil -- Limits via zone. + self.Bullseye.LimitXmax = nil + self.Bullseye.LimitZmin = nil + self.Bullseye.LimitZmax = nil -- Break entry. - self.BreakEntry.name="Break Entry" - self.BreakEntry.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. Check for initial is at 3 NM with a radius of 500 m and 100 m starboard. - self.BreakEntry.Xmax= nil - self.BreakEntry.Zmin=-UTILS.NMToMeters(0.5) -- Not more than 0.5 NM port of boat. - self.BreakEntry.Zmax= UTILS.NMToMeters(1.5) -- Not more than 1.5 NM starboard. - self.BreakEntry.LimitXmin=0 -- Check and next step when at carrier and starboard of carrier. - self.BreakEntry.LimitXmax=nil - self.BreakEntry.LimitZmin=nil - self.BreakEntry.LimitZmax=nil + self.BreakEntry.name = "Break Entry" + self.BreakEntry.Xmin = -UTILS.NMToMeters( 4 ) -- Not more than 4 NM behind the boat. Check for initial is at 3 NM with a radius of 500 m and 100 m starboard. + self.BreakEntry.Xmax = nil + self.BreakEntry.Zmin = -UTILS.NMToMeters( 0.5 ) -- Not more than 0.5 NM port of boat. + self.BreakEntry.Zmax = UTILS.NMToMeters( 1.5 ) -- Not more than 1.5 NM starboard. + self.BreakEntry.LimitXmin = 0 -- Check and next step when at carrier and starboard of carrier. + self.BreakEntry.LimitXmax = nil + self.BreakEntry.LimitZmin = nil + self.BreakEntry.LimitZmax = nil -- Early break. - self.BreakEarly.name="Early Break" - self.BreakEarly.Xmin=-UTILS.NMToMeters(1) -- Not more than 1 NM behind the boat. Last check was at 0. - self.BreakEarly.Xmax= UTILS.NMToMeters(5) -- Not more than 5 NM in front of the boat. Enough for late breaks? - self.BreakEarly.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port. - self.BreakEarly.Zmax= UTILS.NMToMeters(1) -- Not more than 1 NM starboard. - self.BreakEarly.LimitXmin= 0 -- Check and next step 0.2 NM port and in front of boat. - self.BreakEarly.LimitXmax= nil - self.BreakEarly.LimitZmin=-UTILS.NMToMeters(0.2) -- -370 m port - self.BreakEarly.LimitZmax= nil + self.BreakEarly.name = "Early Break" + self.BreakEarly.Xmin = -UTILS.NMToMeters( 1 ) -- Not more than 1 NM behind the boat. Last check was at 0. + self.BreakEarly.Xmax = UTILS.NMToMeters( 5 ) -- Not more than 5 NM in front of the boat. Enough for late breaks? + self.BreakEarly.Zmin = -UTILS.NMToMeters( 2 ) -- Not more than 2 NM port. + self.BreakEarly.Zmax = UTILS.NMToMeters( 1 ) -- Not more than 1 NM starboard. + self.BreakEarly.LimitXmin = 0 -- Check and next step 0.2 NM port and in front of boat. + self.BreakEarly.LimitXmax = nil + self.BreakEarly.LimitZmin = -UTILS.NMToMeters( 0.2 ) -- -370 m port + self.BreakEarly.LimitZmax = nil -- Late break. - self.BreakLate.name="Late Break" - self.BreakLate.Xmin=-UTILS.NMToMeters(1) -- Not more than 1 NM behind the boat. Last check was at 0. - self.BreakLate.Xmax= UTILS.NMToMeters(5) -- Not more than 5 NM in front of the boat. Enough for late breaks? - self.BreakLate.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port. - self.BreakLate.Zmax= UTILS.NMToMeters(1) -- Not more than 1 NM starboard. - self.BreakLate.LimitXmin= 0 -- Check and next step 0.8 NM port and in front of boat. - self.BreakLate.LimitXmax= nil - self.BreakLate.LimitZmin=-UTILS.NMToMeters(0.8) -- -1470 m port - self.BreakLate.LimitZmax= nil + self.BreakLate.name = "Late Break" + self.BreakLate.Xmin = -UTILS.NMToMeters( 1 ) -- Not more than 1 NM behind the boat. Last check was at 0. + self.BreakLate.Xmax = UTILS.NMToMeters( 5 ) -- Not more than 5 NM in front of the boat. Enough for late breaks? + self.BreakLate.Zmin = -UTILS.NMToMeters( 2 ) -- Not more than 2 NM port. + self.BreakLate.Zmax = UTILS.NMToMeters( 1 ) -- Not more than 1 NM starboard. + self.BreakLate.LimitXmin = 0 -- Check and next step 0.8 NM port and in front of boat. + self.BreakLate.LimitXmax = nil + self.BreakLate.LimitZmin = -UTILS.NMToMeters( 0.8 ) -- -1470 m port + self.BreakLate.LimitZmax = nil -- Abeam position. - self.Abeam.name="Abeam Position" - self.Abeam.Xmin=-UTILS.NMToMeters(5) -- Not more then 5 NM astern of boat. Should be LIG call anyway. - self.Abeam.Xmax= UTILS.NMToMeters(5) -- Not more then 5 NM ahead of boat. - self.Abeam.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port. - self.Abeam.Zmax= 500 -- Not more than 500 m starboard. Must be port! - self.Abeam.LimitXmin=-200 -- Check and next step 200 meters behind the ship. - self.Abeam.LimitXmax= nil - self.Abeam.LimitZmin= nil - self.Abeam.LimitZmax= nil + self.Abeam.name = "Abeam Position" + self.Abeam.Xmin = -UTILS.NMToMeters( 5 ) -- Not more then 5 NM astern of boat. Should be LIG call anyway. + self.Abeam.Xmax = UTILS.NMToMeters( 5 ) -- Not more then 5 NM ahead of boat. + self.Abeam.Zmin = -UTILS.NMToMeters( 2 ) -- Not more than 2 NM port. + self.Abeam.Zmax = 500 -- Not more than 500 m starboard. Must be port! + self.Abeam.LimitXmin = -200 -- Check and next step 200 meters behind the ship. + self.Abeam.LimitXmax = nil + self.Abeam.LimitZmin = nil + self.Abeam.LimitZmax = nil -- At the Ninety. - self.Ninety.name="Ninety" - self.Ninety.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. LIG check anyway. - self.Ninety.Xmax= 0 -- Must be behind the boat. - self.Ninety.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port of boat. - self.Ninety.Zmax= nil - self.Ninety.LimitXmin=nil - self.Ninety.LimitXmax=nil - self.Ninety.LimitZmin=nil - self.Ninety.LimitZmax=-UTILS.NMToMeters(0.6) -- Check and next step when 0.6 NM port. + self.Ninety.name = "Ninety" + self.Ninety.Xmin = -UTILS.NMToMeters( 4 ) -- Not more than 4 NM behind the boat. LIG check anyway. + self.Ninety.Xmax = 0 -- Must be behind the boat. + self.Ninety.Zmin = -UTILS.NMToMeters( 2 ) -- Not more than 2 NM port of boat. + self.Ninety.Zmax = nil + self.Ninety.LimitXmin = nil + self.Ninety.LimitXmax = nil + self.Ninety.LimitZmin = nil + self.Ninety.LimitZmax = -UTILS.NMToMeters( 0.6 ) -- Check and next step when 0.6 NM port. -- At the Wake. - self.Wake.name="Wake" - self.Wake.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. - self.Wake.Xmax= 0 -- Must be behind the boat. - self.Wake.Zmin=-2000 -- Not more than 2 km port of boat. - self.Wake.Zmax= nil - self.Wake.LimitXmin=nil - self.Wake.LimitXmax=nil - self.Wake.LimitZmin=0 -- Check and next step when directly behind the boat. - self.Wake.LimitZmax=nil + self.Wake.name = "Wake" + self.Wake.Xmin = -UTILS.NMToMeters( 4 ) -- Not more than 4 NM behind the boat. + self.Wake.Xmax = 0 -- Must be behind the boat. + self.Wake.Zmin = -2000 -- Not more than 2 km port of boat. + self.Wake.Zmax = nil + self.Wake.LimitXmin = nil + self.Wake.LimitXmax = nil + self.Wake.LimitZmin = 0 -- Check and next step when directly behind the boat. + self.Wake.LimitZmax = nil -- Turn to final. - self.Final.name="Final" - self.Final.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. - self.Final.Xmax= 0 -- Must be behind the boat. - self.Final.Zmin=-2000 -- Not more than 2 km port. - self.Final.Zmax= nil - self.Final.LimitXmin=nil -- No limits. Check is carried out differently. - self.Final.LimitXmax=nil - self.Final.LimitZmin=nil - self.Final.LimitZmax=nil + self.Final.name = "Final" + self.Final.Xmin = -UTILS.NMToMeters( 4 ) -- Not more than 4 NM behind the boat. + self.Final.Xmax = 0 -- Must be behind the boat. + self.Final.Zmin = -2000 -- Not more than 2 km port. + self.Final.Zmax = nil + self.Final.LimitXmin = nil -- No limits. Check is carried out differently. + self.Final.LimitXmax = nil + self.Final.LimitZmin = nil + self.Final.LimitZmax = nil -- In the Groove. - self.Groove.name="Groove" - self.Groove.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. - self.Groove.Xmax= nil - self.Groove.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port - self.Groove.Zmax= UTILS.NMToMeters(2) -- Not more than 2 NM starboard. - self.Groove.LimitXmin=nil -- No limits. Check is carried out differently. - self.Groove.LimitXmax=nil - self.Groove.LimitZmin=nil - self.Groove.LimitZmax=nil + self.Groove.name = "Groove" + self.Groove.Xmin = -UTILS.NMToMeters( 4 ) -- Not more than 4 NM behind the boat. + self.Groove.Xmax = nil + self.Groove.Zmin = -UTILS.NMToMeters( 2 ) -- Not more than 2 NM port + self.Groove.Zmax = UTILS.NMToMeters( 2 ) -- Not more than 2 NM starboard. + self.Groove.LimitXmin = nil -- No limits. Check is carried out differently. + self.Groove.LimitXmax = nil + self.Groove.LimitZmin = nil + self.Groove.LimitZmax = nil end @@ -101609,24 +108702,24 @@ function AIRBOSS:_InitNimitz() self:_InitStennis() -- Carrier Parameters. - self.carrierparam.sterndist =-164 - self.carrierparam.deckheight = 20.1494 --DCS World OpenBeta\CoreMods\tech\USS_Nimitz\Database\USS_CVN_7X.lua + self.carrierparam.sterndist = -164 + self.carrierparam.deckheight = 20.1494 -- DCS World OpenBeta\CoreMods\tech\USS_Nimitz\Database\USS_CVN_7X.lua -- Total size of the carrier (approx as rectangle). - self.carrierparam.totlength=332.8 -- Wiki says 332.8 meters overall length. - self.carrierparam.totwidthport=45 -- Wiki says 76.8 meters overall beam. - self.carrierparam.totwidthstarboard=35 + self.carrierparam.totlength = 332.8 -- Wiki says 332.8 meters overall length. + self.carrierparam.totwidthport = 45 -- Wiki says 76.8 meters overall beam. + self.carrierparam.totwidthstarboard = 35 -- Landing runway. - self.carrierparam.rwyangle = -9.1359 --DCS World OpenBeta\CoreMods\tech\USS_Nimitz\scripts\USS_Nimitz_RunwaysAndRoutes.lua - self.carrierparam.rwylength = 250 - self.carrierparam.rwywidth = 25 + self.carrierparam.rwyangle = -9.1359 -- DCS World OpenBeta\CoreMods\tech\USS_Nimitz\scripts\USS_Nimitz_RunwaysAndRoutes.lua + self.carrierparam.rwylength = 250 + self.carrierparam.rwywidth = 25 -- Wires. - self.carrierparam.wire1 = 55 -- Distance from stern to first wire. - self.carrierparam.wire2 = 67 - self.carrierparam.wire3 = 79 - self.carrierparam.wire4 = 92 + self.carrierparam.wire1 = 55 -- Distance from stern to first wire. + self.carrierparam.wire2 = 67 + self.carrierparam.wire3 = 79 + self.carrierparam.wire4 = 92 end @@ -101638,24 +108731,64 @@ function AIRBOSS:_InitForrestal() self:_InitNimitz() -- Carrier Parameters. - self.carrierparam.sterndist =-135.5 - self.carrierparam.deckheight = 20 --20.1494 --DCS World OpenBeta\CoreMods\tech\USS_Nimitz\Database\USS_CVN_7X.lua + self.carrierparam.sterndist = -135.5 + self.carrierparam.deckheight = 20 -- 20.1494 --DCS World OpenBeta\CoreMods\tech\USS_Nimitz\Database\USS_CVN_7X.lua -- Total size of the carrier (approx as rectangle). - self.carrierparam.totlength=315 -- Wiki says 325 meters overall length. - self.carrierparam.totwidthport=45 -- Wiki says 73 meters overall beam. - self.carrierparam.totwidthstarboard=35 + self.carrierparam.totlength = 315 -- Wiki says 325 meters overall length. + self.carrierparam.totwidthport = 45 -- Wiki says 73 meters overall beam. + self.carrierparam.totwidthstarboard = 35 -- Landing runway. - self.carrierparam.rwyangle = -9.1359 --DCS World OpenBeta\CoreMods\tech\USS_Nimitz\scripts\USS_Nimitz_RunwaysAndRoutes.lua - self.carrierparam.rwylength = 212 - self.carrierparam.rwywidth = 25 + self.carrierparam.rwyangle = -9.1359 -- DCS World OpenBeta\CoreMods\tech\USS_Nimitz\scripts\USS_Nimitz_RunwaysAndRoutes.lua + self.carrierparam.rwylength = 212 + self.carrierparam.rwywidth = 25 -- Wires. - self.carrierparam.wire1 = 44 -- Distance from stern to first wire. Original from Frank - 42 - self.carrierparam.wire2 = 54 --51.5 - self.carrierparam.wire3 = 64 --62 - self.carrierparam.wire4 = 74 --72.5 + self.carrierparam.wire1 = 44 -- Distance from stern to first wire. Original from Frank - 42 + self.carrierparam.wire2 = 54 -- 51.5 + self.carrierparam.wire3 = 64 -- 62 + self.carrierparam.wire4 = 74 -- 72.5 + +end + +--- Init parameters for R12 HMS Hermes carrier. +-- @param #AIRBOSS self +function AIRBOSS:_InitHermes() + + -- Init Stennis as default. + self:_InitStennis() + + -- Carrier Parameters. + self.carrierparam.sterndist = -105 + self.carrierparam.deckheight = 12 -- From model viewer WL0. + + -- Total size of the carrier (approx as rectangle). + self.carrierparam.totlength = 228.19 + self.carrierparam.totwidthport = 20.5 + self.carrierparam.totwidthstarboard = 24.5 + + -- Landing runway. + self.carrierparam.rwyangle = 0 + self.carrierparam.rwylength = 215 + self.carrierparam.rwywidth = 13 + + -- Wires. + self.carrierparam.wire1 = nil + self.carrierparam.wire2 = nil + self.carrierparam.wire3 = nil + self.carrierparam.wire4 = nil + + -- Late break. + self.BreakLate.name = "Late Break" + self.BreakLate.Xmin = -UTILS.NMToMeters( 1 ) -- Not more than 1 NM behind the boat. Last check was at 0. + self.BreakLate.Xmax = UTILS.NMToMeters( 5 ) -- Not more than 5 NM in front of the boat. Enough for late breaks? + self.BreakLate.Zmin = -UTILS.NMToMeters( 0.25 ) -- Not more than 0.25 NM port. + self.BreakLate.Zmax = UTILS.NMToMeters( 0.5 ) -- Not more than 0.5 NM starboard. + self.BreakLate.LimitXmin = 0 -- Check and next step 0.8 NM port and in front of boat. + self.BreakLate.LimitXmax = nil + self.BreakLate.LimitZmin = -UTILS.NMToMeters( 0.5 ) -- 926 m port, closer than the stennis as abeam is 0.8-1.0 rather than 1.2 + self.BreakLate.LimitZmax = nil end @@ -101667,35 +108800,35 @@ function AIRBOSS:_InitTarawa() self:_InitStennis() -- Carrier Parameters. - self.carrierparam.sterndist =-125 - self.carrierparam.deckheight = 21 --69 ft + self.carrierparam.sterndist = -125 + self.carrierparam.deckheight = 21 -- 69 ft -- Total size of the carrier (approx as rectangle). - self.carrierparam.totlength=245 - self.carrierparam.totwidthport=10 - self.carrierparam.totwidthstarboard=25 + self.carrierparam.totlength = 245 + self.carrierparam.totwidthport = 10 + self.carrierparam.totwidthstarboard = 25 -- Landing runway. - self.carrierparam.rwyangle = 0 + self.carrierparam.rwyangle = 0 self.carrierparam.rwylength = 225 - self.carrierparam.rwywidth = 15 + self.carrierparam.rwywidth = 15 -- Wires. - self.carrierparam.wire1=nil - self.carrierparam.wire2=nil - self.carrierparam.wire3=nil - self.carrierparam.wire4=nil + self.carrierparam.wire1 = nil + self.carrierparam.wire2 = nil + self.carrierparam.wire3 = nil + self.carrierparam.wire4 = nil -- Late break. - self.BreakLate.name="Late Break" - self.BreakLate.Xmin=-UTILS.NMToMeters(1) -- Not more than 1 NM behind the boat. Last check was at 0. - self.BreakLate.Xmax= UTILS.NMToMeters(5) -- Not more than 5 NM in front of the boat. Enough for late breaks? - self.BreakLate.Zmin=-UTILS.NMToMeters(1.6) -- Not more than 1.6 NM port. - self.BreakLate.Zmax= UTILS.NMToMeters(1) -- Not more than 1 NM starboard. - self.BreakLate.LimitXmin= 0 -- Check and next step 0.8 NM port and in front of boat. - self.BreakLate.LimitXmax= nil - self.BreakLate.LimitZmin=-UTILS.NMToMeters(0.5) -- 926 m port, closer than the stennis as abeam is 0.8-1.0 rather than 1.2 - self.BreakLate.LimitZmax= nil + self.BreakLate.name = "Late Break" + self.BreakLate.Xmin = -UTILS.NMToMeters( 1 ) -- Not more than 1 NM behind the boat. Last check was at 0. + self.BreakLate.Xmax = UTILS.NMToMeters( 5 ) -- Not more than 5 NM in front of the boat. Enough for late breaks? + self.BreakLate.Zmin = -UTILS.NMToMeters( 1.6 ) -- Not more than 1.6 NM port. + self.BreakLate.Zmax = UTILS.NMToMeters( 1 ) -- Not more than 1 NM starboard. + self.BreakLate.LimitXmin = 0 -- Check and next step 0.8 NM port and in front of boat. + self.BreakLate.LimitXmax = nil + self.BreakLate.LimitZmin = -UTILS.NMToMeters( 0.5 ) -- 926 m port, closer than the stennis as abeam is 0.8-1.0 rather than 1.2 + self.BreakLate.LimitZmax = nil end @@ -101707,35 +108840,35 @@ function AIRBOSS:_InitAmerica() self:_InitStennis() -- Carrier Parameters. - self.carrierparam.sterndist =-125 - self.carrierparam.deckheight = 20 --67 ft + self.carrierparam.sterndist = -125 + self.carrierparam.deckheight = 20 -- 67 ft -- Total size of the carrier (approx as rectangle). - self.carrierparam.totlength=257 - self.carrierparam.totwidthport=11 - self.carrierparam.totwidthstarboard=25 + self.carrierparam.totlength = 257 + self.carrierparam.totwidthport = 11 + self.carrierparam.totwidthstarboard = 25 -- Landing runway. - self.carrierparam.rwyangle = 0 + self.carrierparam.rwyangle = 0 self.carrierparam.rwylength = 240 - self.carrierparam.rwywidth = 15 + self.carrierparam.rwywidth = 15 -- Wires. - self.carrierparam.wire1=nil - self.carrierparam.wire2=nil - self.carrierparam.wire3=nil - self.carrierparam.wire4=nil + self.carrierparam.wire1 = nil + self.carrierparam.wire2 = nil + self.carrierparam.wire3 = nil + self.carrierparam.wire4 = nil -- Late break. - self.BreakLate.name="Late Break" - self.BreakLate.Xmin=-UTILS.NMToMeters(1) -- Not more than 1 NM behind the boat. Last check was at 0. - self.BreakLate.Xmax= UTILS.NMToMeters(5) -- Not more than 5 NM in front of the boat. Enough for late breaks? - self.BreakLate.Zmin=-UTILS.NMToMeters(1.6) -- Not more than 1.6 NM port. - self.BreakLate.Zmax= UTILS.NMToMeters(1) -- Not more than 1 NM starboard. - self.BreakLate.LimitXmin= 0 -- Check and next step 0.8 NM port and in front of boat. - self.BreakLate.LimitXmax= nil - self.BreakLate.LimitZmin=-UTILS.NMToMeters(0.5) -- 926 m port, closer than the stennis as abeam is 0.8-1.0 rather than 1.2 - self.BreakLate.LimitZmax= nil + self.BreakLate.name = "Late Break" + self.BreakLate.Xmin = -UTILS.NMToMeters( 1 ) -- Not more than 1 NM behind the boat. Last check was at 0. + self.BreakLate.Xmax = UTILS.NMToMeters( 5 ) -- Not more than 5 NM in front of the boat. Enough for late breaks? + self.BreakLate.Zmin = -UTILS.NMToMeters( 1.6 ) -- Not more than 1.6 NM port. + self.BreakLate.Zmax = UTILS.NMToMeters( 1 ) -- Not more than 1 NM starboard. + self.BreakLate.LimitXmin = 0 -- Check and next step 0.8 NM port and in front of boat. + self.BreakLate.LimitXmax = nil + self.BreakLate.LimitZmin = -UTILS.NMToMeters( 0.5 ) -- 926 m port, closer than the stennis as abeam is 0.8-1.0 rather than 1.2 + self.BreakLate.LimitZmax = nil end @@ -101747,338 +108880,334 @@ function AIRBOSS:_InitJcarlos() self:_InitStennis() -- Carrier Parameters. - self.carrierparam.sterndist =-125 - self.carrierparam.deckheight = 20 --67 ft + self.carrierparam.sterndist = -125 + self.carrierparam.deckheight = 20 -- 67 ft -- Total size of the carrier (approx as rectangle). - self.carrierparam.totlength=231 - self.carrierparam.totwidthport=10 - self.carrierparam.totwidthstarboard=22 + self.carrierparam.totlength = 231 + self.carrierparam.totwidthport = 10 + self.carrierparam.totwidthstarboard = 22 -- Landing runway. - self.carrierparam.rwyangle = 0 + self.carrierparam.rwyangle = 0 self.carrierparam.rwylength = 202 - self.carrierparam.rwywidth = 14 + self.carrierparam.rwywidth = 14 -- Wires. - self.carrierparam.wire1=nil - self.carrierparam.wire2=nil - self.carrierparam.wire3=nil - self.carrierparam.wire4=nil + self.carrierparam.wire1 = nil + self.carrierparam.wire2 = nil + self.carrierparam.wire3 = nil + self.carrierparam.wire4 = nil -- Late break. - self.BreakLate.name="Late Break" - self.BreakLate.Xmin=-UTILS.NMToMeters(1) -- Not more than 1 NM behind the boat. Last check was at 0. - self.BreakLate.Xmax= UTILS.NMToMeters(5) -- Not more than 5 NM in front of the boat. Enough for late breaks? - self.BreakLate.Zmin=-UTILS.NMToMeters(1.6) -- Not more than 1.6 NM port. - self.BreakLate.Zmax= UTILS.NMToMeters(1) -- Not more than 1 NM starboard. - self.BreakLate.LimitXmin= 0 -- Check and next step 0.8 NM port and in front of boat. - self.BreakLate.LimitXmax= nil - self.BreakLate.LimitZmin=-UTILS.NMToMeters(0.5) -- 926 m port, closer than the stennis as abeam is 0.8-1.0 rather than 1.2 - self.BreakLate.LimitZmax= nil + self.BreakLate.name = "Late Break" + self.BreakLate.Xmin = -UTILS.NMToMeters( 1 ) -- Not more than 1 NM behind the boat. Last check was at 0. + self.BreakLate.Xmax = UTILS.NMToMeters( 5 ) -- Not more than 5 NM in front of the boat. Enough for late breaks? + self.BreakLate.Zmin = -UTILS.NMToMeters( 1.6 ) -- Not more than 1.6 NM port. + self.BreakLate.Zmax = UTILS.NMToMeters( 1 ) -- Not more than 1 NM starboard. + self.BreakLate.LimitXmin = 0 -- Check and next step 0.8 NM port and in front of boat. + self.BreakLate.LimitXmax = nil + self.BreakLate.LimitZmin = -UTILS.NMToMeters( 0.5 ) -- 926 m port, closer than the stennis as abeam is 0.8-1.0 rather than 1.2 + self.BreakLate.LimitZmax = nil end --- Init parameters for Marshal Voice overs *Gabriella* by HighwaymanEd. -- @param #AIRBOSS self -- @param #string mizfolder (Optional) Folder within miz file where the sound files are located. -function AIRBOSS:SetVoiceOversMarshalByGabriella(mizfolder) +function AIRBOSS:SetVoiceOversMarshalByGabriella( mizfolder ) -- Set sound files folder. if mizfolder then - local lastchar=string.sub(mizfolder, -1) - if lastchar~="/" then - mizfolder=mizfolder.."/" + local lastchar = string.sub( mizfolder, -1 ) + if lastchar ~= "/" then + mizfolder = mizfolder .. "/" end - self.soundfolderMSH=mizfolder + self.soundfolderMSH = mizfolder else -- Default is the general folder. - self.soundfolderMSH=self.soundfolder + self.soundfolderMSH = self.soundfolder end -- Report for duty. - self:I(self.lid..string.format("Marshal Gabriella reporting for duty! Soundfolder=%s", tostring(self.soundfolderMSH))) + self:I( self.lid .. string.format( "Marshal Gabriella reporting for duty! Soundfolder=%s", tostring( self.soundfolderMSH ) ) ) - self.MarshalCall.AFFIRMATIVE.duration=0.65 - self.MarshalCall.ALTIMETER.duration=0.60 - self.MarshalCall.BRC.duration=0.67 - self.MarshalCall.CARRIERTURNTOHEADING.duration=1.62 - self.MarshalCall.CASE.duration=0.30 - self.MarshalCall.CHARLIETIME.duration=0.77 - self.MarshalCall.CLEAREDFORRECOVERY.duration=0.93 - self.MarshalCall.DECKCLOSED.duration=0.73 - self.MarshalCall.DEGREES.duration=0.48 - self.MarshalCall.EXPECTED.duration=0.50 - self.MarshalCall.FLYNEEDLES.duration=0.89 - self.MarshalCall.HOLDATANGELS.duration=0.81 - self.MarshalCall.HOURS.duration=0.41 - self.MarshalCall.MARSHALRADIAL.duration=0.95 - self.MarshalCall.N0.duration=0.41 - self.MarshalCall.N1.duration=0.30 - self.MarshalCall.N2.duration=0.34 - self.MarshalCall.N3.duration=0.31 - self.MarshalCall.N4.duration=0.34 - self.MarshalCall.N5.duration=0.30 - self.MarshalCall.N6.duration=0.33 - self.MarshalCall.N7.duration=0.38 - self.MarshalCall.N8.duration=0.35 - self.MarshalCall.N9.duration=0.35 - self.MarshalCall.NEGATIVE.duration=0.60 - self.MarshalCall.NEWFB.duration=0.95 - self.MarshalCall.OPS.duration=0.23 - self.MarshalCall.POINT.duration=0.38 - self.MarshalCall.RADIOCHECK.duration=1.27 - self.MarshalCall.RECOVERY.duration=0.60 - self.MarshalCall.RECOVERYOPSSTOPPED.duration=1.25 - self.MarshalCall.RECOVERYPAUSEDNOTICE.duration=2.55 - self.MarshalCall.RECOVERYPAUSEDRESUMED.duration=2.55 - self.MarshalCall.REPORTSEEME.duration=0.87 - self.MarshalCall.RESUMERECOVERY.duration=1.55 - self.MarshalCall.ROGER.duration=0.50 - self.MarshalCall.SAYNEEDLES.duration=0.82 - self.MarshalCall.STACKFULL.duration=5.70 - self.MarshalCall.STARTINGRECOVERY.duration=1.61 + self.MarshalCall.AFFIRMATIVE.duration = 0.65 + self.MarshalCall.ALTIMETER.duration = 0.60 + self.MarshalCall.BRC.duration = 0.67 + self.MarshalCall.CARRIERTURNTOHEADING.duration = 1.62 + self.MarshalCall.CASE.duration = 0.30 + self.MarshalCall.CHARLIETIME.duration = 0.77 + self.MarshalCall.CLEAREDFORRECOVERY.duration = 0.93 + self.MarshalCall.DECKCLOSED.duration = 0.73 + self.MarshalCall.DEGREES.duration = 0.48 + self.MarshalCall.EXPECTED.duration = 0.50 + self.MarshalCall.FLYNEEDLES.duration = 0.89 + self.MarshalCall.HOLDATANGELS.duration = 0.81 + self.MarshalCall.HOURS.duration = 0.41 + self.MarshalCall.MARSHALRADIAL.duration = 0.95 + self.MarshalCall.N0.duration = 0.41 + self.MarshalCall.N1.duration = 0.30 + self.MarshalCall.N2.duration = 0.34 + self.MarshalCall.N3.duration = 0.31 + self.MarshalCall.N4.duration = 0.34 + self.MarshalCall.N5.duration = 0.30 + self.MarshalCall.N6.duration = 0.33 + self.MarshalCall.N7.duration = 0.38 + self.MarshalCall.N8.duration = 0.35 + self.MarshalCall.N9.duration = 0.35 + self.MarshalCall.NEGATIVE.duration = 0.60 + self.MarshalCall.NEWFB.duration = 0.95 + self.MarshalCall.OPS.duration = 0.23 + self.MarshalCall.POINT.duration = 0.38 + self.MarshalCall.RADIOCHECK.duration = 1.27 + self.MarshalCall.RECOVERY.duration = 0.60 + self.MarshalCall.RECOVERYOPSSTOPPED.duration = 1.25 + self.MarshalCall.RECOVERYPAUSEDNOTICE.duration = 2.55 + self.MarshalCall.RECOVERYPAUSEDRESUMED.duration = 2.55 + self.MarshalCall.REPORTSEEME.duration = 0.87 + self.MarshalCall.RESUMERECOVERY.duration = 1.55 + self.MarshalCall.ROGER.duration = 0.50 + self.MarshalCall.SAYNEEDLES.duration = 0.82 + self.MarshalCall.STACKFULL.duration = 5.70 + self.MarshalCall.STARTINGRECOVERY.duration = 1.61 end - - --- Init parameters for Marshal Voice overs by *Raynor*. -- @param #AIRBOSS self -- @param #string mizfolder (Optional) Folder within miz file where the sound files are located. -function AIRBOSS:SetVoiceOversMarshalByRaynor(mizfolder) +function AIRBOSS:SetVoiceOversMarshalByRaynor( mizfolder ) -- Set sound files folder. if mizfolder then - local lastchar=string.sub(mizfolder, -1) - if lastchar~="/" then - mizfolder=mizfolder.."/" + local lastchar = string.sub( mizfolder, -1 ) + if lastchar ~= "/" then + mizfolder = mizfolder .. "/" end - self.soundfolderMSH=mizfolder + self.soundfolderMSH = mizfolder else -- Default is the general folder. - self.soundfolderMSH=self.soundfolder + self.soundfolderMSH = self.soundfolder end -- Report for duty. - self:I(self.lid..string.format("Marshal Raynor reporting for duty! Soundfolder=%s", tostring(self.soundfolderMSH))) + self:I( self.lid .. string.format( "Marshal Raynor reporting for duty! Soundfolder=%s", tostring( self.soundfolderMSH ) ) ) - self.MarshalCall.AFFIRMATIVE.duration=0.70 - self.MarshalCall.ALTIMETER.duration=0.60 - self.MarshalCall.BRC.duration=0.60 - self.MarshalCall.CARRIERTURNTOHEADING.duration=1.87 - self.MarshalCall.CASE.duration=0.60 - self.MarshalCall.CHARLIETIME.duration=0.81 - self.MarshalCall.CLEAREDFORRECOVERY.duration=1.21 - self.MarshalCall.DECKCLOSED.duration=0.86 - self.MarshalCall.DEGREES.duration=0.55 - self.MarshalCall.EXPECTED.duration=0.61 - self.MarshalCall.FLYNEEDLES.duration=0.90 - self.MarshalCall.HOLDATANGELS.duration=0.91 - self.MarshalCall.HOURS.duration=0.54 - self.MarshalCall.MARSHALRADIAL.duration=0.80 - self.MarshalCall.N0.duration=0.38 - self.MarshalCall.N1.duration=0.30 - self.MarshalCall.N2.duration=0.30 - self.MarshalCall.N3.duration=0.30 - self.MarshalCall.N4.duration=0.32 - self.MarshalCall.N5.duration=0.41 - self.MarshalCall.N6.duration=0.48 - self.MarshalCall.N7.duration=0.51 - self.MarshalCall.N8.duration=0.38 - self.MarshalCall.N9.duration=0.34 - self.MarshalCall.NEGATIVE.duration=0.60 - self.MarshalCall.NEWFB.duration=1.10 - self.MarshalCall.OPS.duration=0.46 - self.MarshalCall.POINT.duration=0.21 - self.MarshalCall.RADIOCHECK.duration=0.95 - self.MarshalCall.RECOVERY.duration=0.63 - self.MarshalCall.RECOVERYOPSSTOPPED.duration=1.36 - self.MarshalCall.RECOVERYPAUSEDNOTICE.duration=2.8 -- Strangely the file is actually a shorter ~2.4 sec. - self.MarshalCall.RECOVERYPAUSEDRESUMED.duration=2.75 - self.MarshalCall.REPORTSEEME.duration=1.06 --0.96 - self.MarshalCall.RESUMERECOVERY.duration=1.41 - self.MarshalCall.ROGER.duration=0.41 - self.MarshalCall.SAYNEEDLES.duration=0.79 - self.MarshalCall.STACKFULL.duration=4.70 - self.MarshalCall.STARTINGRECOVERY.duration=2.06 + self.MarshalCall.AFFIRMATIVE.duration = 0.70 + self.MarshalCall.ALTIMETER.duration = 0.60 + self.MarshalCall.BRC.duration = 0.60 + self.MarshalCall.CARRIERTURNTOHEADING.duration = 1.87 + self.MarshalCall.CASE.duration = 0.60 + self.MarshalCall.CHARLIETIME.duration = 0.81 + self.MarshalCall.CLEAREDFORRECOVERY.duration = 1.21 + self.MarshalCall.DECKCLOSED.duration = 0.86 + self.MarshalCall.DEGREES.duration = 0.55 + self.MarshalCall.EXPECTED.duration = 0.61 + self.MarshalCall.FLYNEEDLES.duration = 0.90 + self.MarshalCall.HOLDATANGELS.duration = 0.91 + self.MarshalCall.HOURS.duration = 0.54 + self.MarshalCall.MARSHALRADIAL.duration = 0.80 + self.MarshalCall.N0.duration = 0.38 + self.MarshalCall.N1.duration = 0.30 + self.MarshalCall.N2.duration = 0.30 + self.MarshalCall.N3.duration = 0.30 + self.MarshalCall.N4.duration = 0.32 + self.MarshalCall.N5.duration = 0.41 + self.MarshalCall.N6.duration = 0.48 + self.MarshalCall.N7.duration = 0.51 + self.MarshalCall.N8.duration = 0.38 + self.MarshalCall.N9.duration = 0.34 + self.MarshalCall.NEGATIVE.duration = 0.60 + self.MarshalCall.NEWFB.duration = 1.10 + self.MarshalCall.OPS.duration = 0.46 + self.MarshalCall.POINT.duration = 0.21 + self.MarshalCall.RADIOCHECK.duration = 0.95 + self.MarshalCall.RECOVERY.duration = 0.63 + self.MarshalCall.RECOVERYOPSSTOPPED.duration = 1.36 + self.MarshalCall.RECOVERYPAUSEDNOTICE.duration = 2.8 -- Strangely the file is actually a shorter ~2.4 sec. + self.MarshalCall.RECOVERYPAUSEDRESUMED.duration = 2.75 + self.MarshalCall.REPORTSEEME.duration = 1.06 -- 0.96 + self.MarshalCall.RESUMERECOVERY.duration = 1.41 + self.MarshalCall.ROGER.duration = 0.41 + self.MarshalCall.SAYNEEDLES.duration = 0.79 + self.MarshalCall.STACKFULL.duration = 4.70 + self.MarshalCall.STARTINGRECOVERY.duration = 2.06 end --- Set parameters for LSO Voice overs by *Raynor*. -- @param #AIRBOSS self -- @param #string mizfolder (Optional) Folder within miz file where the sound files are located. -function AIRBOSS:SetVoiceOversLSOByRaynor(mizfolder) +function AIRBOSS:SetVoiceOversLSOByRaynor( mizfolder ) -- Set sound files folder. if mizfolder then - local lastchar=string.sub(mizfolder, -1) - if lastchar~="/" then - mizfolder=mizfolder.."/" + local lastchar = string.sub( mizfolder, -1 ) + if lastchar ~= "/" then + mizfolder = mizfolder .. "/" end - self.soundfolderLSO=mizfolder + self.soundfolderLSO = mizfolder else -- Default is the general folder. - self.soundfolderLSO=self.soundfolder + self.soundfolderLSO = self.soundfolder end -- Report for duty. - self:I(self.lid..string.format("LSO Raynor reporting for duty! Soundfolder=%s", tostring(self.soundfolderLSO))) + self:I( self.lid .. string.format( "LSO Raynor reporting for duty! Soundfolder=%s", tostring( self.soundfolderLSO ) ) ) - self.LSOCall.BOLTER.duration=0.75 - self.LSOCall.CALLTHEBALL.duration=0.625 - self.LSOCall.CHECK.duration=0.40 - self.LSOCall.CLEAREDTOLAND.duration=0.85 - self.LSOCall.COMELEFT.duration=0.60 - self.LSOCall.DEPARTANDREENTER.duration=1.10 - self.LSOCall.EXPECTHEAVYWAVEOFF.duration=1.30 - self.LSOCall.EXPECTSPOT75.duration=1.85 - self.LSOCall.EXPECTSPOT5.duration=1.3 - self.LSOCall.FAST.duration=0.75 - self.LSOCall.FOULDECK.duration=0.75 - self.LSOCall.HIGH.duration=0.65 - self.LSOCall.IDLE.duration=0.40 - self.LSOCall.LONGINGROOVE.duration=1.25 - self.LSOCall.LOW.duration=0.60 - self.LSOCall.N0.duration=0.38 - self.LSOCall.N1.duration=0.30 - self.LSOCall.N2.duration=0.30 - self.LSOCall.N3.duration=0.30 - self.LSOCall.N4.duration=0.32 - self.LSOCall.N5.duration=0.41 - self.LSOCall.N6.duration=0.48 - self.LSOCall.N7.duration=0.51 - self.LSOCall.N8.duration=0.38 - self.LSOCall.N9.duration=0.34 - self.LSOCall.PADDLESCONTACT.duration=0.91 - self.LSOCall.POWER.duration=0.45 - self.LSOCall.RADIOCHECK.duration=0.90 - self.LSOCall.RIGHTFORLINEUP.duration=0.70 - self.LSOCall.ROGERBALL.duration=0.72 - self.LSOCall.SLOW.duration=0.63 - --self.LSOCall.SLOW.duration=0.59 --TODO - self.LSOCall.STABILIZED.duration=0.75 - self.LSOCall.WAVEOFF.duration=0.55 - self.LSOCall.WELCOMEABOARD.duration=0.80 + self.LSOCall.BOLTER.duration = 0.75 + self.LSOCall.CALLTHEBALL.duration = 0.625 + self.LSOCall.CHECK.duration = 0.40 + self.LSOCall.CLEAREDTOLAND.duration = 0.85 + self.LSOCall.COMELEFT.duration = 0.60 + self.LSOCall.DEPARTANDREENTER.duration = 1.10 + self.LSOCall.EXPECTHEAVYWAVEOFF.duration = 1.30 + self.LSOCall.EXPECTSPOT75.duration = 1.85 + self.LSOCall.EXPECTSPOT5.duration = 1.3 + self.LSOCall.FAST.duration = 0.75 + self.LSOCall.FOULDECK.duration = 0.75 + self.LSOCall.HIGH.duration = 0.65 + self.LSOCall.IDLE.duration = 0.40 + self.LSOCall.LONGINGROOVE.duration = 1.25 + self.LSOCall.LOW.duration = 0.60 + self.LSOCall.N0.duration = 0.38 + self.LSOCall.N1.duration = 0.30 + self.LSOCall.N2.duration = 0.30 + self.LSOCall.N3.duration = 0.30 + self.LSOCall.N4.duration = 0.32 + self.LSOCall.N5.duration = 0.41 + self.LSOCall.N6.duration = 0.48 + self.LSOCall.N7.duration = 0.51 + self.LSOCall.N8.duration = 0.38 + self.LSOCall.N9.duration = 0.34 + self.LSOCall.PADDLESCONTACT.duration = 0.91 + self.LSOCall.POWER.duration = 0.45 + self.LSOCall.RADIOCHECK.duration = 0.90 + self.LSOCall.RIGHTFORLINEUP.duration = 0.70 + self.LSOCall.ROGERBALL.duration = 0.72 + self.LSOCall.SLOW.duration = 0.63 + -- self.LSOCall.SLOW.duration=0.59 --TODO + self.LSOCall.STABILIZED.duration = 0.75 + self.LSOCall.WAVEOFF.duration = 0.55 + self.LSOCall.WELCOMEABOARD.duration = 0.80 end - - --- Set parameters for LSO Voice overs by *funkyfranky*. -- @param #AIRBOSS self -- @param #string mizfolder (Optional) Folder within miz file where the sound files are located. -function AIRBOSS:SetVoiceOversLSOByFF(mizfolder) +function AIRBOSS:SetVoiceOversLSOByFF( mizfolder ) -- Set sound files folder. if mizfolder then - local lastchar=string.sub(mizfolder, -1) - if lastchar~="/" then - mizfolder=mizfolder.."/" + local lastchar = string.sub( mizfolder, -1 ) + if lastchar ~= "/" then + mizfolder = mizfolder .. "/" end - self.soundfolderLSO=mizfolder + self.soundfolderLSO = mizfolder else -- Default is the general folder. - self.soundfolderLSO=self.soundfolder + self.soundfolderLSO = self.soundfolder end -- Report for duty. - self:I(self.lid..string.format("LSO FF reporting for duty! Soundfolder=%s", tostring(self.soundfolderLSO))) + self:I( self.lid .. string.format( "LSO FF reporting for duty! Soundfolder=%s", tostring( self.soundfolderLSO ) ) ) - self.LSOCall.BOLTER.duration=0.75 - self.LSOCall.CALLTHEBALL.duration=0.60 - self.LSOCall.CHECK.duration=0.45 - self.LSOCall.CLEAREDTOLAND.duration=1.00 - self.LSOCall.COMELEFT.duration=0.60 - self.LSOCall.DEPARTANDREENTER.duration=1.10 - self.LSOCall.EXPECTHEAVYWAVEOFF.duration=1.20 - self.LSOCall.EXPECTSPOT75.duration=2.00 - self.LSOCall.EXPECTSPOT5.duration=1.3 - self.LSOCall.FAST.duration=0.70 - self.LSOCall.FOULDECK.duration=0.62 - self.LSOCall.HIGH.duration=0.65 - self.LSOCall.IDLE.duration=0.45 - self.LSOCall.LONGINGROOVE.duration=1.20 - self.LSOCall.LOW.duration=0.50 - self.LSOCall.N0.duration=0.40 - self.LSOCall.N1.duration=0.25 - self.LSOCall.N2.duration=0.37 - self.LSOCall.N3.duration=0.37 - self.LSOCall.N4.duration=0.39 - self.LSOCall.N5.duration=0.39 - self.LSOCall.N6.duration=0.40 - self.LSOCall.N7.duration=0.40 - self.LSOCall.N8.duration=0.37 - self.LSOCall.N9.duration=0.40 - self.LSOCall.PADDLESCONTACT.duration=1.00 - self.LSOCall.POWER.duration=0.50 - self.LSOCall.RADIOCHECK.duration=1.10 - self.LSOCall.RIGHTFORLINEUP.duration=0.80 - self.LSOCall.ROGERBALL.duration=1.00 - self.LSOCall.SLOW.duration=0.65 - self.LSOCall.SLOW.duration=0.59 - self.LSOCall.STABILIZED.duration=0.90 - self.LSOCall.WAVEOFF.duration=0.60 - self.LSOCall.WELCOMEABOARD.duration=1.00 + self.LSOCall.BOLTER.duration = 0.75 + self.LSOCall.CALLTHEBALL.duration = 0.60 + self.LSOCall.CHECK.duration = 0.45 + self.LSOCall.CLEAREDTOLAND.duration = 1.00 + self.LSOCall.COMELEFT.duration = 0.60 + self.LSOCall.DEPARTANDREENTER.duration = 1.10 + self.LSOCall.EXPECTHEAVYWAVEOFF.duration = 1.20 + self.LSOCall.EXPECTSPOT75.duration = 2.00 + self.LSOCall.EXPECTSPOT5.duration = 1.3 + self.LSOCall.FAST.duration = 0.70 + self.LSOCall.FOULDECK.duration = 0.62 + self.LSOCall.HIGH.duration = 0.65 + self.LSOCall.IDLE.duration = 0.45 + self.LSOCall.LONGINGROOVE.duration = 1.20 + self.LSOCall.LOW.duration = 0.50 + self.LSOCall.N0.duration = 0.40 + self.LSOCall.N1.duration = 0.25 + self.LSOCall.N2.duration = 0.37 + self.LSOCall.N3.duration = 0.37 + self.LSOCall.N4.duration = 0.39 + self.LSOCall.N5.duration = 0.39 + self.LSOCall.N6.duration = 0.40 + self.LSOCall.N7.duration = 0.40 + self.LSOCall.N8.duration = 0.37 + self.LSOCall.N9.duration = 0.40 + self.LSOCall.PADDLESCONTACT.duration = 1.00 + self.LSOCall.POWER.duration = 0.50 + self.LSOCall.RADIOCHECK.duration = 1.10 + self.LSOCall.RIGHTFORLINEUP.duration = 0.80 + self.LSOCall.ROGERBALL.duration = 1.00 + self.LSOCall.SLOW.duration = 0.65 + self.LSOCall.SLOW.duration = 0.59 + self.LSOCall.STABILIZED.duration = 0.90 + self.LSOCall.WAVEOFF.duration = 0.60 + self.LSOCall.WELCOMEABOARD.duration = 1.00 end --- Intit parameters for Marshal Voice overs by *funkyfranky*. -- @param #AIRBOSS self -- @param #string mizfolder (Optional) Folder within miz file where the sound files are located. -function AIRBOSS:SetVoiceOversMarshalByFF(mizfolder) +function AIRBOSS:SetVoiceOversMarshalByFF( mizfolder ) -- Set sound files folder. if mizfolder then - local lastchar=string.sub(mizfolder, -1) - if lastchar~="/" then - mizfolder=mizfolder.."/" + local lastchar = string.sub( mizfolder, -1 ) + if lastchar ~= "/" then + mizfolder = mizfolder .. "/" end - self.soundfolderMSH=mizfolder + self.soundfolderMSH = mizfolder else -- Default is the general folder. - self.soundfolderMSH=self.soundfolder + self.soundfolderMSH = self.soundfolder end -- Report for duty. - self:I(self.lid..string.format("Marshal FF reporting for duty! Soundfolder=%s", tostring(self.soundfolderMSH))) + self:I( self.lid .. string.format( "Marshal FF reporting for duty! Soundfolder=%s", tostring( self.soundfolderMSH ) ) ) - self.MarshalCall.AFFIRMATIVE.duration=0.90 - self.MarshalCall.ALTIMETER.duration=0.85 - self.MarshalCall.BRC.duration=0.80 - self.MarshalCall.CARRIERTURNTOHEADING.duration=2.48 - self.MarshalCall.CASE.duration=0.40 - self.MarshalCall.CHARLIETIME.duration=0.90 - self.MarshalCall.CLEAREDFORRECOVERY.duration=1.25 - self.MarshalCall.DECKCLOSED.duration=1.10 - self.MarshalCall.DEGREES.duration=0.60 - self.MarshalCall.EXPECTED.duration=0.55 - self.MarshalCall.FLYNEEDLES.duration=0.90 - self.MarshalCall.HOLDATANGELS.duration=1.10 - self.MarshalCall.HOURS.duration=0.60 - self.MarshalCall.MARSHALRADIAL.duration=1.10 - self.MarshalCall.N0.duration=0.40 - self.MarshalCall.N1.duration=0.25 - self.MarshalCall.N2.duration=0.37 - self.MarshalCall.N3.duration=0.37 - self.MarshalCall.N4.duration=0.39 - self.MarshalCall.N5.duration=0.39 - self.MarshalCall.N6.duration=0.40 - self.MarshalCall.N7.duration=0.40 - self.MarshalCall.N8.duration=0.37 - self.MarshalCall.N9.duration=0.40 - self.MarshalCall.NEGATIVE.duration=0.80 - self.MarshalCall.NEWFB.duration=1.35 - self.MarshalCall.OPS.duration=0.48 - self.MarshalCall.POINT.duration=0.33 - self.MarshalCall.RADIOCHECK.duration=1.20 - self.MarshalCall.RECOVERY.duration=0.70 - self.MarshalCall.RECOVERYOPSSTOPPED.duration=1.65 - self.MarshalCall.RECOVERYPAUSEDNOTICE.duration=2.9 -- Strangely the file is actually a shorter ~2.4 sec. - self.MarshalCall.RECOVERYPAUSEDRESUMED.duration=3.40 - self.MarshalCall.REPORTSEEME.duration=0.95 - self.MarshalCall.RESUMERECOVERY.duration=1.75 - self.MarshalCall.ROGER.duration=0.53 - self.MarshalCall.SAYNEEDLES.duration=0.90 - self.MarshalCall.STACKFULL.duration=6.35 - self.MarshalCall.STARTINGRECOVERY.duration=2.65 + self.MarshalCall.AFFIRMATIVE.duration = 0.90 + self.MarshalCall.ALTIMETER.duration = 0.85 + self.MarshalCall.BRC.duration = 0.80 + self.MarshalCall.CARRIERTURNTOHEADING.duration = 2.48 + self.MarshalCall.CASE.duration = 0.40 + self.MarshalCall.CHARLIETIME.duration = 0.90 + self.MarshalCall.CLEAREDFORRECOVERY.duration = 1.25 + self.MarshalCall.DECKCLOSED.duration = 1.10 + self.MarshalCall.DEGREES.duration = 0.60 + self.MarshalCall.EXPECTED.duration = 0.55 + self.MarshalCall.FLYNEEDLES.duration = 0.90 + self.MarshalCall.HOLDATANGELS.duration = 1.10 + self.MarshalCall.HOURS.duration = 0.60 + self.MarshalCall.MARSHALRADIAL.duration = 1.10 + self.MarshalCall.N0.duration = 0.40 + self.MarshalCall.N1.duration = 0.25 + self.MarshalCall.N2.duration = 0.37 + self.MarshalCall.N3.duration = 0.37 + self.MarshalCall.N4.duration = 0.39 + self.MarshalCall.N5.duration = 0.39 + self.MarshalCall.N6.duration = 0.40 + self.MarshalCall.N7.duration = 0.40 + self.MarshalCall.N8.duration = 0.37 + self.MarshalCall.N9.duration = 0.40 + self.MarshalCall.NEGATIVE.duration = 0.80 + self.MarshalCall.NEWFB.duration = 1.35 + self.MarshalCall.OPS.duration = 0.48 + self.MarshalCall.POINT.duration = 0.33 + self.MarshalCall.RADIOCHECK.duration = 1.20 + self.MarshalCall.RECOVERY.duration = 0.70 + self.MarshalCall.RECOVERYOPSSTOPPED.duration = 1.65 + self.MarshalCall.RECOVERYPAUSEDNOTICE.duration = 2.9 -- Strangely the file is actually a shorter ~2.4 sec. + self.MarshalCall.RECOVERYPAUSEDRESUMED.duration = 3.40 + self.MarshalCall.REPORTSEEME.duration = 0.95 + self.MarshalCall.RESUMERECOVERY.duration = 1.75 + self.MarshalCall.ROGER.duration = 0.53 + self.MarshalCall.SAYNEEDLES.duration = 0.90 + self.MarshalCall.STACKFULL.duration = 6.35 + self.MarshalCall.STARTINGRECOVERY.duration = 2.65 end @@ -102091,291 +109220,44 @@ function AIRBOSS:_InitVoiceOvers() --------------- -- LSO Radio Calls. - self.LSOCall={ - BOLTER={ - file="LSO-BolterBolter", - suffix="ogg", - loud=false, - subtitle="Bolter, Bolter", - duration=0.75, - subduration=5, - }, - CALLTHEBALL={ - file="LSO-CallTheBall", - suffix="ogg", - loud=false, - subtitle="Call the ball", - duration=0.6, - subduration=2, - }, - CHECK={ - file="LSO-Check", - suffix="ogg", - loud=false, - subtitle="Check", - duration=0.45, - subduration=2.5, - }, - CLEAREDTOLAND={ - file="LSO-ClearedToLand", - suffix="ogg", - loud=false, - subtitle="Cleared to land", - duration=1.0, - subduration=5, - }, - COMELEFT={ - file="LSO-ComeLeft", - suffix="ogg", - loud=true, - subtitle="Come left", - duration=0.60, - subduration=1, - }, - RADIOCHECK={ - file="LSO-RadioCheck", - suffix="ogg", - loud=false, - subtitle="Paddles, radio check", - duration=1.1, - subduration=5, - }, - RIGHTFORLINEUP={ - file="LSO-RightForLineup", - suffix="ogg", - loud=true, - subtitle="Right for line up", - duration=0.80, - subduration=1, - }, - HIGH={ - file="LSO-High", - suffix="ogg", - loud=true, - subtitle="You're high", - duration=0.65, - subduration=1, - }, - LOW={ - file="LSO-Low", - suffix="ogg", - loud=true, - subtitle="You're low", - duration=0.50, - subduration=1, - }, - POWER={ - file="LSO-Power", - suffix="ogg", - loud=true, - subtitle="Power", - duration=0.50, --0.45 was too short - subduration=1, - }, - SLOW={ - file="LSO-Slow", - suffix="ogg", - loud=true, - subtitle="You're slow", - duration=0.65, - subduration=1, - }, - FAST={ - file="LSO-Fast", - suffix="ogg", - loud=true, - subtitle="You're fast", - duration=0.70, - subduration=1, - }, - ROGERBALL={ - file="LSO-RogerBall", - suffix="ogg", - loud=false, - subtitle="Roger ball", - duration=1.00, - subduration=2, - }, - WAVEOFF={ - file="LSO-WaveOff", - suffix="ogg", - loud=false, - subtitle="Wave off", - duration=0.6, - subduration=5, - }, - LONGINGROOVE={ - file="LSO-LongInTheGroove", - suffix="ogg", - loud=false, - subtitle="You're long in the groove", - duration=1.2, - subduration=5, - }, - FOULDECK={ - file="LSO-FoulDeck", - suffix="ogg", - loud=false, - subtitle="Foul deck", - duration=0.62, - subduration=5, - }, - DEPARTANDREENTER={ - file="LSO-DepartAndReenter", - suffix="ogg", - loud=false, - subtitle="Depart and re-enter", - duration=1.1, - subduration=5, - }, - PADDLESCONTACT={ - file="LSO-PaddlesContact", - suffix="ogg", - loud=false, - subtitle="Paddles, contact", - duration=1.0, - subduration=5, - }, - WELCOMEABOARD={ - file="LSO-WelcomeAboard", - suffix="ogg", - loud=false, - subtitle="Welcome aboard", - duration=1.0, - subduration=5, - }, - EXPECTHEAVYWAVEOFF={ - file="LSO-ExpectHeavyWaveoff", - suffix="ogg", - loud=false, - subtitle="Expect heavy waveoff", - duration=1.2, - subduration=5, - }, - EXPECTSPOT75={ - file="LSO-ExpectSpot75", - suffix="ogg", - loud=false, - subtitle="Expect spot 7.5", - duration=2.0, - subduration=5, - }, - EXPECTSPOT5={ - file="LSO-ExpectSpot5", - suffix="ogg", - loud=false, - subtitle="Expect spot 5", - duration=1.3, - subduration=5, - }, - STABILIZED={ - file="LSO-Stabilized", - suffix="ogg", - loud=false, - subtitle="Stabilized", - duration=0.9, - subduration=5, - }, - IDLE={ - file="LSO-Idle", - suffix="ogg", - loud=false, - subtitle="Idle", - duration=0.45, - subduration=5, - }, - N0={ - file="LSO-N0", - suffix="ogg", - loud=false, - subtitle="", - duration=0.40, - }, - N1={ - file="LSO-N1", - suffix="ogg", - loud=false, - subtitle="", - duration=0.25, - }, - N2={ - file="LSO-N2", - suffix="ogg", - loud=false, - subtitle="", - duration=0.37, - }, - N3={ - file="LSO-N3", - suffix="ogg", - loud=false, - subtitle="", - duration=0.37, - }, - N4={ - file="LSO-N4", - suffix="ogg", - loud=false, - subtitle="", - duration=0.39, - }, - N5={ - file="LSO-N5", - suffix="ogg", - loud=false, - subtitle="", - duration=0.39, - }, - N6={ - file="LSO-N6", - suffix="ogg", - loud=false, - subtitle="", - duration=0.40, - }, - N7={ - file="LSO-N7", - suffix="ogg", - loud=false, - subtitle="", - duration=0.40, - }, - N8={ - file="LSO-N8", - suffix="ogg", - loud=false, - subtitle="", - duration=0.37, - }, - N9={ - file="LSO-N9", - suffix="ogg", - loud=false, - subtitle="", - duration=0.40, - }, - CLICK={ - file="AIRBOSS-RadioClick", - suffix="ogg", - loud=false, - subtitle="", - duration=0.35, - }, - NOISE={ - file="AIRBOSS-Noise", - suffix="ogg", - loud=false, - subtitle="", - duration=3.6, - }, - SPINIT={ - file="AIRBOSS-SpinIt", - suffix="ogg", - loud=false, - subtitle="", - duration=0.73, - subduration=5, - }, + self.LSOCall = { + BOLTER = { file = "LSO-BolterBolter", suffix = "ogg", loud = false, subtitle = "Bolter, Bolter", duration = 0.75, subduration = 5 }, + CALLTHEBALL = { file = "LSO-CallTheBall", suffix = "ogg", loud = false, subtitle = "Call the ball", duration = 0.6, subduration = 2 }, + CHECK = { file = "LSO-Check", suffix = "ogg", loud = false, subtitle = "Check", duration = 0.45, subduration = 2.5 }, + CLEAREDTOLAND = { file = "LSO-ClearedToLand", suffix = "ogg", loud = false, subtitle = "Cleared to land", duration = 1.0, subduration = 5 }, + COMELEFT = { file = "LSO-ComeLeft", suffix = "ogg", loud = true, subtitle = "Come left", duration = 0.60, subduration = 1 }, + RADIOCHECK = { file = "LSO-RadioCheck", suffix = "ogg", loud = false, subtitle = "Paddles, radio check", duration = 1.1, subduration = 5 }, + RIGHTFORLINEUP = { file = "LSO-RightForLineup", suffix = "ogg", loud = true, subtitle = "Right for line up", duration = 0.80, subduration = 1 }, + HIGH = { file = "LSO-High", suffix = "ogg", loud = true, subtitle = "You're high", duration = 0.65, subduration = 1 }, + LOW = { file = "LSO-Low", suffix = "ogg", loud = true, subtitle = "You're low", duration = 0.50, subduration = 1 }, + POWER = { file = "LSO-Power", suffix = "ogg", loud = true, subtitle = "Power", duration = 0.50, subduration = 1 }, -- duration 0.45 was too short + SLOW = { file = "LSO-Slow", suffix = "ogg", loud = true, subtitle = "You're slow", duration = 0.65, subduration = 1 }, + FAST = { file = "LSO-Fast", suffix = "ogg", loud = true, subtitle = "You're fast", duration = 0.70, subduration = 1 }, + ROGERBALL = { file = "LSO-RogerBall", suffix = "ogg", loud = false, subtitle = "Roger ball", duration = 1.00, subduration = 2 }, + WAVEOFF = { file = "LSO-WaveOff", suffix = "ogg", loud = false, subtitle = "Wave off", duration = 0.6, subduration = 5 }, + LONGINGROOVE = { file = "LSO-LongInTheGroove", suffix = "ogg", loud = false, subtitle = "You're long in the groove", duration = 1.2, subduration = 5 }, + FOULDECK = { file = "LSO-FoulDeck", suffix = "ogg", loud = false, subtitle = "Foul deck", duration = 0.62, subduration = 5 }, + DEPARTANDREENTER = { file = "LSO-DepartAndReenter", suffix = "ogg", loud = false, subtitle = "Depart and re-enter", duration = 1.1, subduration = 5 }, + PADDLESCONTACT = { file = "LSO-PaddlesContact", suffix = "ogg", loud = false, subtitle = "Paddles, contact", duration = 1.0, subduration = 5 }, + WELCOMEABOARD = { file = "LSO-WelcomeAboard", suffix = "ogg", loud = false, subtitle = "Welcome aboard", duration = 1.0, subduration = 5 }, + EXPECTHEAVYWAVEOFF = { file = "LSO-ExpectHeavyWaveoff", suffix = "ogg", loud = false, subtitle = "Expect heavy waveoff", duration = 1.2, subduration = 5 }, + EXPECTSPOT75 = { file = "LSO-ExpectSpot75", suffix = "ogg", loud = false, subtitle = "Expect spot 7.5", duration = 2.0, subduration = 5 }, + EXPECTSPOT5 = { file = "LSO-ExpectSpot5", suffix = "ogg", loud = false, subtitle = "Expect spot 5", duration = 1.3, subduration = 5 }, + STABILIZED = { file = "LSO-Stabilized", suffix = "ogg", loud = false, subtitle = "Stabilized", duration = 0.9, subduration = 5 }, + IDLE = { file = "LSO-Idle", suffix = "ogg", loud = false, subtitle = "Idle", duration = 0.45, subduration = 5 }, + N0 = { file = "LSO-N0", suffix = "ogg", loud = false, subtitle = "", duration = 0.40 }, + N1 = { file = "LSO-N1", suffix = "ogg", loud = false, subtitle = "", duration = 0.25 }, + N2 = { file = "LSO-N2", suffix = "ogg", loud = false, subtitle = "", duration = 0.37 }, + N3 = { file = "LSO-N3", suffix = "ogg", loud = false, subtitle = "", duration = 0.37 }, + N4 = { file = "LSO-N4", suffix = "ogg", loud = false, subtitle = "", duration = 0.39 }, + N5 = { file = "LSO-N5", suffix = "ogg", loud = false, subtitle = "", duration = 0.39 }, + N6 = { file = "LSO-N6", suffix = "ogg", loud = false, subtitle = "", duration = 0.40 }, + N7 = { file = "LSO-N7", suffix = "ogg", loud = false, subtitle = "", duration = 0.40 }, + N8 = { file = "LSO-N8", suffix = "ogg", loud = false, subtitle = "", duration = 0.37 }, + N9 = { file = "LSO-N9", suffix = "ogg", loud = false, subtitle = "", duration = 0.40 }, + CLICK = { file = "AIRBOSS-RadioClick", suffix = "ogg", loud = false, subtitle = "", duration = 0.35 }, + NOISE = { file = "AIRBOSS-Noise", suffix = "ogg", loud = false, subtitle = "", duration = 3.6 }, + SPINIT = { file = "AIRBOSS-SpinIt", suffix = "ogg", loud = false, subtitle = "", duration = 0.73, subduration = 5 }, } ----------------- @@ -102383,161 +109265,28 @@ function AIRBOSS:_InitVoiceOvers() ----------------- -- Pilot Radio Calls. - self.PilotCall={ - N0={ - file="PILOT-N0", - suffix="ogg", - loud=false, - subtitle="", - duration=0.40, - }, - N1={ - file="PILOT-N1", - suffix="ogg", - loud=false, - subtitle="", - duration=0.25, - }, - N2={ - file="PILOT-N2", - suffix="ogg", - loud=false, - subtitle="", - duration=0.37, - }, - N3={ - file="PILOT-N3", - suffix="ogg", - loud=false, - subtitle="", - duration=0.37, - }, - N4={ - file="PILOT-N4", - suffix="ogg", - loud=false, - subtitle="", - duration=0.39, - }, - N5={ - file="PILOT-N5", - suffix="ogg", - loud=false, - subtitle="", - duration=0.39, - }, - N6={ - file="PILOT-N6", - suffix="ogg", - loud=false, - subtitle="", - duration=0.40, - }, - N7={ - file="PILOT-N7", - suffix="ogg", - loud=false, - subtitle="", - duration=0.40, - }, - N8={ - file="PILOT-N8", - suffix="ogg", - loud=false, - subtitle="", - duration=0.37, - }, - N9={ - file="PILOT-N9", - suffix="ogg", - loud=false, - subtitle="", - duration=0.40, - }, - POINT={ - file="PILOT-Point", - suffix="ogg", - loud=false, - subtitle="", - duration=0.33, - }, - SKYHAWK={ - file="PILOT-Skyhawk", - suffix="ogg", - loud=false, - subtitle="", - duration=0.95, - subduration=5, - }, - HARRIER={ - file="PILOT-Harrier", - suffix="ogg", - loud=false, - subtitle="", - duration=0.58, - subduration=5, - }, - HAWKEYE={ - file="PILOT-Hawkeye", - suffix="ogg", - loud=false, - subtitle="", - duration=0.63, - subduration=5, - }, - TOMCAT={ - file="PILOT-Tomcat", - suffix="ogg", - loud=false, - subtitle="", - duration=0.66, - subduration=5, - }, - HORNET={ - file="PILOT-Hornet", - suffix="ogg", - loud=false, - subtitle="", - duration=0.56, - subduration=5, - }, - VIKING={ - file="PILOT-Viking", - suffix="ogg", - loud=false, - subtitle="", - duration=0.61, - subduration=5, - }, - BALL={ - file="PILOT-Ball", - suffix="ogg", - loud=false, - subtitle="", - duration=0.50, - subduration=5, - }, - BINGOFUEL={ - file="PILOT-BingoFuel", - suffix="ogg", - loud=false, - subtitle="", - duration=0.80, - }, - GASATDIVERT={ - file="PILOT-GasAtDivert", - suffix="ogg", - loud=false, - subtitle="", - duration=1.80, - }, - GASATTANKER={ - file="PILOT-GasAtTanker", - suffix="ogg", - loud=false, - subtitle="", - duration=1.95, - }, + self.PilotCall = { + N0 = { file = "PILOT-N0", suffix = "ogg", loud = false, subtitle = "", duration = 0.40 }, + N1 = { file = "PILOT-N1", suffix = "ogg", loud = false, subtitle = "", duration = 0.25 }, + N2 = { file = "PILOT-N2", suffix = "ogg", loud = false, subtitle = "", duration = 0.37 }, + N3 = { file = "PILOT-N3", suffix = "ogg", loud = false, subtitle = "", duration = 0.37 }, + N4 = { file = "PILOT-N4", suffix = "ogg", loud = false, subtitle = "", duration = 0.39 }, + N5 = { file = "PILOT-N5", suffix = "ogg", loud = false, subtitle = "", duration = 0.39 }, + N6 = { file = "PILOT-N6", suffix = "ogg", loud = false, subtitle = "", duration = 0.40 }, + N7 = { file = "PILOT-N7", suffix = "ogg", loud = false, subtitle = "", duration = 0.40 }, + N8 = { file = "PILOT-N8", suffix = "ogg", loud = false, subtitle = "", duration = 0.37 }, + N9 = { file = "PILOT-N9", suffix = "ogg", loud = false, subtitle = "", duration = 0.40 }, + POINT = { file = "PILOT-Point", suffix = "ogg", loud = false, subtitle = "", duration = 0.33 }, + SKYHAWK = { file = "PILOT-Skyhawk", suffix = "ogg", loud = false, subtitle = "", duration = 0.95, subduration = 5 }, + HARRIER = { file = "PILOT-Harrier", suffix = "ogg", loud = false, subtitle = "", duration = 0.58, subduration = 5 }, + HAWKEYE = { file = "PILOT-Hawkeye", suffix = "ogg", loud = false, subtitle = "", duration = 0.63, subduration = 5 }, + TOMCAT = { file = "PILOT-Tomcat", suffix = "ogg", loud = false, subtitle = "", duration = 0.66, subduration = 5 }, + HORNET = { file = "PILOT-Hornet", suffix = "ogg", loud = false, subtitle = "", duration = 0.56, subduration = 5 }, + VIKING = { file = "PILOT-Viking", suffix = "ogg", loud = false, subtitle = "", duration = 0.61, subduration = 5 }, + BALL = { file = "PILOT-Ball", suffix = "ogg", loud = false, subtitle = "", duration = 0.50, subduration = 5 }, + BINGOFUEL = { file = "PILOT-BingoFuel", suffix = "ogg", loud = false, subtitle = "", duration = 0.80 }, + GASATDIVERT = { file = "PILOT-GasAtDivert", suffix = "ogg", loud = false, subtitle = "", duration = 1.80 }, + GASATTANKER = { file = "PILOT-GasAtTanker", suffix = "ogg", loud = false, subtitle = "", duration = 1.95 }, } ------------------- @@ -102545,309 +109294,48 @@ function AIRBOSS:_InitVoiceOvers() ------------------- -- MARSHAL Radio Calls. - self.MarshalCall={ - AFFIRMATIVE={ - file="MARSHAL-Affirmative", - suffix="ogg", - loud=false, - subtitle="", - duration=0.90, - }, - ALTIMETER={ - file="MARSHAL-Altimeter", - suffix="ogg", - loud=false, - subtitle="", - duration=0.85, - }, - BRC={ - file="MARSHAL-BRC", - suffix="ogg", - loud=false, - subtitle="", - duration=0.80, - }, - CARRIERTURNTOHEADING={ - file="MARSHAL-CarrierTurnToHeading", - suffix="ogg", - loud=false, - subtitle="", - duration=2.48, - subduration=5, - }, - CASE={ - file="MARSHAL-Case", - suffix="ogg", - loud=false, - subtitle="", - duration=0.40, - }, - CHARLIETIME={ - file="MARSHAL-CharlieTime", - suffix="ogg", - loud=false, - subtitle="", - duration=0.90, - }, - CLEAREDFORRECOVERY={ - file="MARSHAL-ClearedForRecovery", - suffix="ogg", - loud=false, - subtitle="", - duration=1.25, - }, - DECKCLOSED={ - file="MARSHAL-DeckClosed", - suffix="ogg", - loud=false, - subtitle="", - duration=1.10, - subduration=5, - }, - DEGREES={ - file="MARSHAL-Degrees", - suffix="ogg", - loud=false, - subtitle="", - duration=0.60, - }, - EXPECTED={ - file="MARSHAL-Expected", - suffix="ogg", - loud=false, - subtitle="", - duration=0.55, - }, - FLYNEEDLES={ - file="MARSHAL-FlyYourNeedles", - suffix="ogg", - loud=false, - subtitle="Fly your needles", - duration=0.9, - subduration=5, - }, - HOLDATANGELS={ - file="MARSHAL-HoldAtAngels", - suffix="ogg", - loud=false, - subtitle="", - duration=1.10, - }, - HOURS={ - file="MARSHAL-Hours", - suffix="ogg", - loud=false, - subtitle="", - duration=0.60, - subduration=5, - }, - MARSHALRADIAL={ - file="MARSHAL-MarshalRadial", - suffix="ogg", - loud=false, - subtitle="", - duration=1.10, - }, - N0={ - file="MARSHAL-N0", - suffix="ogg", - loud=false, - subtitle="", - duration=0.40, - }, - N1={ - file="MARSHAL-N1", - suffix="ogg", - loud=false, - subtitle="", - duration=0.25, - }, - N2={ - file="MARSHAL-N2", - suffix="ogg", - loud=false, - subtitle="", - duration=0.37, - }, - N3={ - file="MARSHAL-N3", - suffix="ogg", - loud=false, - subtitle="", - duration=0.37, - }, - N4={ - file="MARSHAL-N4", - suffix="ogg", - loud=false, - subtitle="", - duration=0.39, - }, - N5={ - file="MARSHAL-N5", - suffix="ogg", - loud=false, - subtitle="", - duration=0.39, - }, - N6={ - file="MARSHAL-N6", - suffix="ogg", - loud=false, - subtitle="", - duration=0.40, - }, - N7={ - file="MARSHAL-N7", - suffix="ogg", - loud=false, - subtitle="", - duration=0.40, - }, - N8={ - file="MARSHAL-N8", - suffix="ogg", - loud=false, - subtitle="", - duration=0.37, - }, - N9={ - file="MARSHAL-N9", - suffix="ogg", - loud=false, - subtitle="", - duration=0.40, - }, - NEGATIVE={ - file="MARSHAL-Negative", - suffix="ogg", - loud=false, - subtitle="", - duration=0.80, - subduration=5, - }, - NEWFB={ - file="MARSHAL-NewFB", - suffix="ogg", - loud=false, - subtitle="", - duration=1.35, - }, - OPS={ - file="MARSHAL-Ops", - suffix="ogg", - loud=false, - subtitle="", - duration=0.48, - }, - POINT={ - file="MARSHAL-Point", - suffix="ogg", - loud=false, - subtitle="", - duration=0.33, - }, - RADIOCHECK={ - file="MARSHAL-RadioCheck", - suffix="ogg", - loud=false, - subtitle="Radio check", - duration=1.20, - subduration=5, - }, - RECOVERY={ - file="MARSHAL-Recovery", - suffix="ogg", - loud=false, - subtitle="", - duration=0.70, - subduration=5, - }, - RECOVERYOPSSTOPPED={ - file="MARSHAL-RecoveryOpsStopped", - suffix="ogg", - loud=false, - subtitle="", - duration=1.65, - subduration=5, - }, - RECOVERYPAUSEDNOTICE={ - file="MARSHAL-RecoveryPausedNotice", - suffix="ogg", - loud=false, - subtitle="aircraft recovery paused until further notice", - duration=2.90, - subduration=5, - }, - RECOVERYPAUSEDRESUMED={ - file="MARSHAL-RecoveryPausedResumed", - suffix="ogg", - loud=false, - subtitle="", - duration=3.40, - subduration=5, - }, - REPORTSEEME={ - file="MARSHAL-ReportSeeMe", - suffix="ogg", - loud=false, - subtitle="", - duration=0.95, - }, - RESUMERECOVERY={ - file="MARSHAL-ResumeRecovery", - suffix="ogg", - loud=false, - subtitle="resuming aircraft recovery", - duration=1.75, - subduraction=5, - }, - ROGER={ - file="MARSHAL-Roger", - suffix="ogg", - loud=false, - subtitle="", - duration=0.53, - subduration=5, - }, - SAYNEEDLES={ - file="MARSHAL-SayNeedles", - suffix="ogg", - loud=false, - subtitle="Say needles", - duration=0.90, - subduration=5, - }, - STACKFULL={ - file="MARSHAL-StackFull", - suffix="ogg", - loud=false, - subtitle="Marshal Stack is currently full. Hold outside 10 NM zone and wait for further instructions", - duration=6.35, - subduration=10, - }, - STARTINGRECOVERY={ - file="MARSHAL-StartingRecovery", - suffix="ogg", - loud=false, - subtitle="", - duration=2.65, - subduration=5, - }, - CLICK={ - file="AIRBOSS-RadioClick", - suffix="ogg", - loud=false, - subtitle="", - duration=0.35, - }, - NOISE={ - file="AIRBOSS-Noise", - suffix="ogg", - loud=false, - subtitle="", - duration=3.6, - }, + self.MarshalCall = { + AFFIRMATIVE = { file = "MARSHAL-Affirmative", suffix = "ogg", loud = false, subtitle = "", duration = 0.90 }, + ALTIMETER = { file = "MARSHAL-Altimeter", suffix = "ogg", loud = false, subtitle = "", duration = 0.85 }, + BRC = { file = "MARSHAL-BRC", suffix = "ogg", loud = false, subtitle = "", duration = 0.80 }, + CARRIERTURNTOHEADING = { file = "MARSHAL-CarrierTurnToHeading", suffix = "ogg", loud = false, subtitle = "", duration = 2.48, subduration = 5 }, + CASE = { file = "MARSHAL-Case", suffix = "ogg", loud = false, subtitle = "", duration = 0.40 }, + CHARLIETIME = { file = "MARSHAL-CharlieTime", suffix = "ogg", loud = false, subtitle = "", duration = 0.90 }, + CLEAREDFORRECOVERY = { file = "MARSHAL-ClearedForRecovery", suffix = "ogg", loud = false, subtitle = "", duration = 1.25 }, + DECKCLOSED = { file = "MARSHAL-DeckClosed", suffix = "ogg", loud = false, subtitle = "", duration = 1.10, subduration = 5 }, + DEGREES = { file = "MARSHAL-Degrees", suffix = "ogg", loud = false, subtitle = "", duration = 0.60 }, + EXPECTED = { file = "MARSHAL-Expected", suffix = "ogg", loud = false, subtitle = "", duration = 0.55 }, + FLYNEEDLES = { file = "MARSHAL-FlyYourNeedles", suffix = "ogg", loud = false, subtitle = "Fly your needles", duration = 0.9, subduration = 5 }, + HOLDATANGELS = { file = "MARSHAL-HoldAtAngels", suffix = "ogg", loud = false, subtitle = "", duration = 1.10 }, + HOURS = { file = "MARSHAL-Hours", suffix = "ogg", loud = false, subtitle = "", duration = 0.60, subduration = 5 }, + MARSHALRADIAL = { file = "MARSHAL-MarshalRadial", suffix = "ogg", loud = false, subtitle = "", duration = 1.10 }, + N0 = { file = "MARSHAL-N0", suffix = "ogg", loud = false, subtitle = "", duration = 0.40 }, + N1 = { file = "MARSHAL-N1", suffix = "ogg", loud = false, subtitle = "", duration = 0.25 }, + N2 = { file = "MARSHAL-N2", suffix = "ogg", loud = false, subtitle = "", duration = 0.37 }, + N3 = { file = "MARSHAL-N3", suffix = "ogg", loud = false, subtitle = "", duration = 0.37 }, + N4 = { file = "MARSHAL-N4", suffix = "ogg", loud = false, subtitle = "", duration = 0.39 }, + N5 = { file = "MARSHAL-N5", suffix = "ogg", loud = false, subtitle = "", duration = 0.39 }, + N6 = { file = "MARSHAL-N6", suffix = "ogg", loud = false, subtitle = "", duration = 0.40 }, + N7 = { file = "MARSHAL-N7", suffix = "ogg", loud = false, subtitle = "", duration = 0.40 }, + N8 = { file = "MARSHAL-N8", suffix = "ogg", loud = false, subtitle = "", duration = 0.37 }, + N9 = { file = "MARSHAL-N9", suffix = "ogg", loud = false, subtitle = "", duration = 0.40 }, + NEGATIVE = { file = "MARSHAL-Negative", suffix = "ogg", loud = false, subtitle = "", duration = 0.80, subduration = 5 }, + NEWFB = { file = "MARSHAL-NewFB", suffix = "ogg", loud = false, subtitle = "", duration = 1.35 }, + OPS = { file = "MARSHAL-Ops", suffix = "ogg", loud = false, subtitle = "", duration = 0.48 }, + POINT = { file = "MARSHAL-Point", suffix = "ogg", loud = false, subtitle = "", duration = 0.33 }, + RADIOCHECK = { file = "MARSHAL-RadioCheck", suffix = "ogg", loud = false, subtitle = "Radio check", duration = 1.20, subduration = 5 }, + RECOVERY = { file = "MARSHAL-Recovery", suffix = "ogg", loud = false, subtitle = "", duration = 0.70, subduration = 5 }, + RECOVERYOPSSTOPPED = { file = "MARSHAL-RecoveryOpsStopped", suffix = "ogg", loud = false, subtitle = "", duration = 1.65, subduration = 5 }, + RECOVERYPAUSEDNOTICE = { file = "MARSHAL-RecoveryPausedNotice", suffix = "ogg", loud = false, subtitle = "aircraft recovery paused until further notice", duration = 2.90, subduration = 5 }, + RECOVERYPAUSEDRESUMED = { file = "MARSHAL-RecoveryPausedResumed", suffix = "ogg", loud = false, subtitle = "", duration = 3.40, subduration = 5 }, + REPORTSEEME = { file = "MARSHAL-ReportSeeMe", suffix = "ogg", loud = false, subtitle = "", duration = 0.95 }, + RESUMERECOVERY = { file = "MARSHAL-ResumeRecovery", suffix = "ogg", loud = false, subtitle = "resuming aircraft recovery", duration = 1.75, subduraction = 5 }, + ROGER = { file = "MARSHAL-Roger", suffix = "ogg", loud = false, subtitle = "", duration = 0.53, subduration = 5 }, + SAYNEEDLES = { file = "MARSHAL-SayNeedles", suffix = "ogg", loud = false, subtitle = "Say needles", duration = 0.90, subduration = 5 }, + STACKFULL = { file = "MARSHAL-StackFull", suffix = "ogg", loud = false, subtitle = "Marshal Stack is currently full. Hold outside 10 NM zone and wait for further instructions", duration = 6.35, subduration = 10 }, + STARTINGRECOVERY = { file = "MARSHAL-StartingRecovery", suffix = "ogg", loud = false, subtitle = "", duration = 2.65, subduration = 5 }, + CLICK = { file = "AIRBOSS-RadioClick", suffix = "ogg", loud = false, subtitle = "", duration = 0.35 }, + NOISE = { file = "AIRBOSS-Noise", suffix = "ogg", loud = false, subtitle = "", duration = 3.6 }, } -- Default timings by Raynor @@ -102864,77 +109352,79 @@ end -- @param #number subduration (Optional) Duration how long the subtitle is displayed. -- @param #string filename (Optional) Name of the voice over sound file. -- @param #string suffix (Optional) Extention of file. Default ".ogg". -function AIRBOSS:SetVoiceOver(radiocall, duration, subtitle, subduration, filename, suffix) - radiocall.duration=duration - radiocall.subtitle=subtitle or radiocall.subtitle - radiocall.file=filename - radiocall.suffix=suffix or ".ogg" +function AIRBOSS:SetVoiceOver( radiocall, duration, subtitle, subduration, filename, suffix ) + radiocall.duration = duration + radiocall.subtitle = subtitle or radiocall.subtitle + radiocall.file = filename + radiocall.suffix = suffix or ".ogg" end --- Get optimal aircraft AoA parameters.. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. -- @return #AIRBOSS.AircraftAoA AoA parameters for the given aircraft type. -function AIRBOSS:_GetAircraftAoA(playerData) +function AIRBOSS:_GetAircraftAoA( playerData ) -- Get AC type. - local hornet=playerData.actype==AIRBOSS.AircraftCarrier.HORNET - local goshawk=playerData.actype==AIRBOSS.AircraftCarrier.T45C - local skyhawk=playerData.actype==AIRBOSS.AircraftCarrier.A4EC - local harrier=playerData.actype==AIRBOSS.AircraftCarrier.AV8B - local tomcat=playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B + local hornet = playerData.actype == AIRBOSS.AircraftCarrier.HORNET + local goshawk = playerData.actype == AIRBOSS.AircraftCarrier.T45C + local skyhawk = playerData.actype == AIRBOSS.AircraftCarrier.A4EC + local harrier = playerData.actype == AIRBOSS.AircraftCarrier.AV8B + local tomcat = playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B -- Table with AoA values. - local aoa={} -- #AIRBOSS.AircraftAoA + local aoa = {} -- #AIRBOSS.AircraftAoA if hornet then -- F/A-18C Hornet parameters. - aoa.SLOW = 9.8 - aoa.Slow = 9.3 + aoa.SLOW = 9.8 + aoa.Slow = 9.3 aoa.OnSpeedMax = 8.8 - aoa.OnSpeed = 8.1 + aoa.OnSpeed = 8.1 aoa.OnSpeedMin = 7.4 - aoa.Fast = 6.9 - aoa.FAST = 6.3 + aoa.Fast = 6.9 + aoa.FAST = 6.3 elseif tomcat then -- F-14A/B Tomcat parameters (taken from NATOPS). Converted from units 0-30 to degrees. -- Currently assuming a linear relationship with 0=-10 degrees and 30=+40 degrees as stated in NATOPS. - aoa.SLOW = self:_AoAUnit2Deg(playerData, 17.0) --18.33 --17.0 units - aoa.Slow = self:_AoAUnit2Deg(playerData, 16.0) --16.67 --16.0 units - aoa.OnSpeedMax = self:_AoAUnit2Deg(playerData, 15.5) --15.83 --15.5 units - aoa.OnSpeed = self:_AoAUnit2Deg(playerData, 15.0) --15.0 --15.0 units - aoa.OnSpeedMin = self:_AoAUnit2Deg(playerData, 14.5) --14.17 --14.5 units - aoa.Fast = self:_AoAUnit2Deg(playerData, 14.0) --13.33 --14.0 units - aoa.FAST = self:_AoAUnit2Deg(playerData, 13.0) --11.67 --13.0 units + aoa.SLOW = self:_AoAUnit2Deg( playerData, 17.0 ) -- 18.33 --17.0 units + aoa.Slow = self:_AoAUnit2Deg( playerData, 16.0 ) -- 16.67 --16.0 units + aoa.OnSpeedMax = self:_AoAUnit2Deg( playerData, 15.5 ) -- 15.83 --15.5 units + aoa.OnSpeed = self:_AoAUnit2Deg( playerData, 15.0 ) -- 15.0 --15.0 units + aoa.OnSpeedMin = self:_AoAUnit2Deg( playerData, 14.5 ) -- 14.17 --14.5 units + aoa.Fast = self:_AoAUnit2Deg( playerData, 14.0 ) -- 13.33 --14.0 units + aoa.FAST = self:_AoAUnit2Deg( playerData, 13.0 ) -- 11.67 --13.0 units elseif goshawk then -- T-45C Goshawk parameters. - aoa.SLOW = 8.00 --19 - aoa.Slow = 7.75 --18 - aoa.OnSpeedMax = 7.25 --17.5 - aoa.OnSpeed = 7.00 --17 - aoa.OnSpeedMin = 6.75 --16.5 - aoa.Fast = 6.25 --16 - aoa.FAST = 6.00 --15 + aoa.SLOW = 8.00 -- 19 + aoa.Slow = 7.75 -- 18 + aoa.OnSpeedMax = 7.25 -- 17.5 + aoa.OnSpeed = 7.00 -- 17 + aoa.OnSpeedMin = 6.75 -- 16.5 + aoa.Fast = 6.25 -- 16 + aoa.FAST = 6.00 -- 15 elseif skyhawk then -- A-4E-C Skyhawk parameters from https://forums.eagle.ru/showpost.php?p=3703467&postcount=390 -- Note that these are arbitrary UNITS and not degrees. We need a conversion formula! -- Github repo suggests they simply use a factor of two to get from degrees to units. - aoa.SLOW = 9.50 --=19.0/2 - aoa.Slow = 9.25 --=18.5/2 - aoa.OnSpeedMax = 9.00 --=18.0/2 - aoa.OnSpeed = 8.75 --=17.5/2 8.1 - aoa.OnSpeedMin = 8.50 --=17.0/2 - aoa.Fast = 8.25 --=17.5/2 - aoa.FAST = 8.00 --=16.5/2 + aoa.SLOW = 9.50 -- =19.0/2 + aoa.Slow = 9.25 -- =18.5/2 + aoa.OnSpeedMax = 9.00 -- =18.0/2 + aoa.OnSpeed = 8.75 -- =17.5/2 8.1 + aoa.OnSpeedMin = 8.50 -- =17.0/2 + aoa.Fast = 8.25 -- =17.5/2 + aoa.FAST = 8.00 -- =16.5/2 elseif harrier then - -- AV-8B Harrier parameters. Tuning done on the Fast AoA to allow for abeam and ninety at Nozzles 60 - 73. - aoa.SLOW = 14.0 - aoa.Slow = 13.0 - aoa.OnSpeedMax = 12.0 - aoa.OnSpeed = 11.0 - aoa.OnSpeedMin = 10.0 + + -- AV-8B Harrier parameters. Tuning done on the Fast AoA to allow for abeam and ninety at Nozzles 55. Pene testing + aoa.SLOW = 16.0 + aoa.Slow = 13.5 + aoa.OnSpeedMax = 12.5 + aoa.OnSpeed = 10.0 + aoa.OnSpeedMin = 9.5 aoa.Fast = 8.0 aoa.FAST = 7.5 + end return aoa @@ -102945,13 +109435,13 @@ end -- @param #AIRBOSS.PlayerData playerData Player data table. -- @param #number aoaunits AoA in arbitrary units. -- @return #number AoA in degrees. -function AIRBOSS:_AoAUnit2Deg(playerData, aoaunits) +function AIRBOSS:_AoAUnit2Deg( playerData, aoaunits ) -- Init. - local degrees=aoaunits + local degrees = aoaunits -- Check aircraft type of player. - if playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then + if playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B then ------------- -- F-14A/B -- @@ -102963,20 +109453,20 @@ function AIRBOSS:_AoAUnit2Deg(playerData, aoaunits) -- Assuming a linear relationship between these to points of the graph. -- However: AoA=15 Units ==> 15 degrees, which is too much. - degrees=-10+50/30*aoaunits + degrees = -10 + 50 / 30 * aoaunits -- HB Facebook page https://www.facebook.com/heatblur/photos/a.683612385159716/754368278084126 -- AoA=15 Units <==> AoA=10.359 degrees. - degrees=0.918*aoaunits-3.411 + degrees = 0.918 * aoaunits - 3.411 - elseif playerData.actype==AIRBOSS.AircraftCarrier.A4EC then + elseif playerData.actype == AIRBOSS.AircraftCarrier.A4EC then ---------- -- A-4E -- ---------- -- A-4E-C source code suggests a simple factor of 1/2 for conversion. - degrees=0.5*aoaunits + degrees = 0.5 * aoaunits end @@ -102988,13 +109478,13 @@ end -- @param #AIRBOSS.PlayerData playerData Player data table. -- @param #number degrees AoA in degrees. -- @return #number AoA in arbitrary units. -function AIRBOSS:_AoADeg2Units(playerData, degrees) +function AIRBOSS:_AoADeg2Units( playerData, degrees ) -- Init. - local aoaunits=degrees + local aoaunits = degrees -- Check aircraft type of player. - if playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then + if playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B then ------------- -- F-14A/B -- @@ -103005,20 +109495,20 @@ function AIRBOSS:_AoADeg2Units(playerData, degrees) -- unit=30 ==> alpha=+40 degrees. -- Assuming a linear relationship between these to points of the graph. - aoaunits=(degrees+10)*30/50 + aoaunits = (degrees + 10) * 30 / 50 -- HB Facebook page https://www.facebook.com/heatblur/photos/a.683612385159716/754368278084126 -- AoA=15 Units <==> AoA=10.359 degrees. - aoaunits=1.089*degrees+3.715 + aoaunits = 1.089 * degrees + 3.715 - elseif playerData.actype==AIRBOSS.AircraftCarrier.A4EC then + elseif playerData.actype == AIRBOSS.AircraftCarrier.A4EC then ---------- -- A-4E -- ---------- -- A-4E source code suggests a simple factor of two as conversion. - aoaunits=2*degrees + aoaunits = 2 * degrees end @@ -103033,16 +109523,16 @@ end -- @return #number Angle of Attack or nil. -- @return #number Distance to carrier in meters or nil. -- @return #number Speed in m/s or nil. -function AIRBOSS:_GetAircraftParameters(playerData, step) +function AIRBOSS:_GetAircraftParameters( playerData, step ) -- Get parameters depended on step. - step=step or playerData.step + step = step or playerData.step -- Get AC type. - local hornet=playerData.actype==AIRBOSS.AircraftCarrier.HORNET - local skyhawk=playerData.actype==AIRBOSS.AircraftCarrier.A4EC - local tomcat=playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B - local harrier=playerData.actype==AIRBOSS.AircraftCarrier.AV8B + local hornet = playerData.actype == AIRBOSS.AircraftCarrier.HORNET + local skyhawk = playerData.actype == AIRBOSS.AircraftCarrier.A4EC + local tomcat = playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B + local harrier = playerData.actype == AIRBOSS.AircraftCarrier.AV8B -- Return values. local alt @@ -103051,153 +109541,150 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) local speed -- Aircraft specific AoA. - local aoaac=self:_GetAircraftAoA(playerData) + local aoaac = self:_GetAircraftAoA( playerData ) - if step==AIRBOSS.PatternStep.PLATFORM then + if step == AIRBOSS.PatternStep.PLATFORM then - alt=UTILS.FeetToMeters(5000) + alt = UTILS.FeetToMeters( 5000 ) - --dist=UTILS.NMToMeters(20) + -- dist=UTILS.NMToMeters(20) - speed=UTILS.KnotsToMps(250) + speed = UTILS.KnotsToMps( 250 ) - elseif step==AIRBOSS.PatternStep.ARCIN then + elseif step == AIRBOSS.PatternStep.ARCIN then - if tomcat then - speed=UTILS.KnotsToMps(150) - else - speed=UTILS.KnotsToMps(250) - end - - elseif step==AIRBOSS.PatternStep.ARCOUT then - - if tomcat then - speed=UTILS.KnotsToMps(150) - else - speed=UTILS.KnotsToMps(250) - end - - elseif step==AIRBOSS.PatternStep.DIRTYUP then - - alt=UTILS.FeetToMeters(1200) - - --speed=UTILS.KnotsToMps(250) - - elseif step==AIRBOSS.PatternStep.BULLSEYE then - - alt=UTILS.FeetToMeters(1200) - - dist=-UTILS.NMToMeters(3) - - aoa=aoaac.OnSpeed - - elseif step==AIRBOSS.PatternStep.INITIAL then - - if hornet or tomcat or harrier then - alt=UTILS.FeetToMeters(800) - speed=UTILS.KnotsToMps(350) - elseif skyhawk then - alt=UTILS.FeetToMeters(600) - speed=UTILS.KnotsToMps(250) - elseif goshawk then - alt=UTILS.FeetToMeters(800) - speed=UTILS.KnotsToMps(300) - end - - elseif step==AIRBOSS.PatternStep.BREAKENTRY then - - if hornet or tomcat or harrier then - alt=UTILS.FeetToMeters(800) - speed=UTILS.KnotsToMps(350) - elseif skyhawk then - alt=UTILS.FeetToMeters(600) - speed=UTILS.KnotsToMps(250) - elseif goshawk then - alt=UTILS.FeetToMeters(800) - speed=UTILS.KnotsToMps(300) - end - - elseif step==AIRBOSS.PatternStep.EARLYBREAK then - - if hornet or tomcat or harrier or goshawk then - alt=UTILS.FeetToMeters(800) - elseif skyhawk then - alt=UTILS.FeetToMeters(600) - end - - elseif step==AIRBOSS.PatternStep.LATEBREAK then - - if hornet or tomcat or harrier or goshawk then - alt=UTILS.FeetToMeters(800) - elseif skyhawk then - alt=UTILS.FeetToMeters(600) - end - - elseif step==AIRBOSS.PatternStep.ABEAM then - - if hornet or tomcat or harrier or goshawk then - alt=UTILS.FeetToMeters(600) - elseif skyhawk then - alt=UTILS.FeetToMeters(500) - end - - aoa=aoaac.OnSpeed - - if harrier then - -- 0.8 to 1.0 NM - dist=UTILS.NMToMeters(0.9) + if tomcat then + speed = UTILS.KnotsToMps( 150 ) else - dist=UTILS.NMToMeters(1.2) + speed = UTILS.KnotsToMps( 250 ) end - if goshawk then + elseif step == AIRBOSS.PatternStep.ARCOUT then + + if tomcat then + speed = UTILS.KnotsToMps( 150 ) + else + speed = UTILS.KnotsToMps( 250 ) + end + + elseif step == AIRBOSS.PatternStep.DIRTYUP then + + alt = UTILS.FeetToMeters( 1200 ) + + -- speed=UTILS.KnotsToMps(250) + + elseif step == AIRBOSS.PatternStep.BULLSEYE then + + alt = UTILS.FeetToMeters( 1200 ) + + dist = -UTILS.NMToMeters( 3 ) + + aoa = aoaac.OnSpeed + + elseif step == AIRBOSS.PatternStep.INITIAL then + + if hornet or tomcat or harrier then + alt = UTILS.FeetToMeters( 800 ) + speed = UTILS.KnotsToMps( 350 ) + elseif skyhawk then + + alt = UTILS.FeetToMeters( 600 ) + speed = UTILS.KnotsToMps( 250 ) + elseif goshawk then + alt = UTILS.FeetToMeters( 800 ) + speed = UTILS.KnotsToMps( 300 ) + end + + elseif step == AIRBOSS.PatternStep.BREAKENTRY then + + if hornet or tomcat or harrier then + alt = UTILS.FeetToMeters( 800 ) + speed = UTILS.KnotsToMps( 350 ) + elseif skyhawk then + alt = UTILS.FeetToMeters( 600 ) + speed = UTILS.KnotsToMps( 250 ) + elseif goshawk then + alt = UTILS.FeetToMeters( 800 ) + speed = UTILS.KnotsToMps( 300 ) + end + + elseif step == AIRBOSS.PatternStep.EARLYBREAK then + + if hornet or tomcat or harrier or goshawk then + alt = UTILS.FeetToMeters( 800 ) + elseif skyhawk then + alt = UTILS.FeetToMeters( 600 ) + end + + elseif step == AIRBOSS.PatternStep.LATEBREAK then + + if hornet or tomcat or harrier or goshawk then + alt = UTILS.FeetToMeters( 800 ) + elseif skyhawk then + alt = UTILS.FeetToMeters( 600 ) + end + + elseif step == AIRBOSS.PatternStep.ABEAM then + + if hornet or tomcat or harrier or goshawk then + alt = UTILS.FeetToMeters( 600 ) + elseif skyhawk then + alt = UTILS.FeetToMeters( 500 ) + end + + aoa = aoaac.OnSpeed + + if goshawk then -- 0.9 to 1.1 NM per natops ch.4 page 48 - dist=UTILS.NMToMeters(0.9) + dist = UTILS.NMToMeters( 0.9 ) + elseif harrier then + -- 0.8 to 1.0 NM + dist = UTILS.NMToMeters( 0.9 ) else - dist=UTILS.NMToMeters(1.1) + dist = UTILS.NMToMeters( 1.1 ) end - elseif step==AIRBOSS.PatternStep.NINETY then + elseif step == AIRBOSS.PatternStep.NINETY then if hornet or tomcat then - alt=UTILS.FeetToMeters(500) - elseif goshawk then - alt=UTILS.FeetToMeters(450) + alt = UTILS.FeetToMeters( 500 ) + elseif goshawk then + alt = UTILS.FeetToMeters( 450 ) elseif skyhawk then - alt=UTILS.FeetToMeters(500) + alt = UTILS.FeetToMeters( 500 ) elseif harrier then - alt=UTILS.FeetToMeters(425) + alt = UTILS.FeetToMeters( 425 ) end - aoa=aoaac.OnSpeed + aoa = aoaac.OnSpeed - elseif step==AIRBOSS.PatternStep.WAKE then + elseif step == AIRBOSS.PatternStep.WAKE then if hornet or goshawk then - alt=UTILS.FeetToMeters(370) + alt = UTILS.FeetToMeters( 370 ) elseif tomcat then - alt=UTILS.FeetToMeters(430) -- Tomcat should be a bit higher as it intercepts the GS a bit higher. + alt = UTILS.FeetToMeters( 430 ) -- Tomcat should be a bit higher as it intercepts the GS a bit higher. elseif skyhawk then - alt=UTILS.FeetToMeters(370) --? + alt = UTILS.FeetToMeters( 370 ) -- ? end -- Harrier wont get into wake pos. Runway is not angled and it stays port. - aoa=aoaac.OnSpeed + aoa = aoaac.OnSpeed - elseif step==AIRBOSS.PatternStep.FINAL then + elseif step == AIRBOSS.PatternStep.FINAL then if hornet or goshawk then - alt=UTILS.FeetToMeters(300) + alt = UTILS.FeetToMeters( 300 ) elseif tomcat then - alt=UTILS.FeetToMeters(360) + alt = UTILS.FeetToMeters( 360 ) elseif skyhawk then - alt=UTILS.FeetToMeters(300) --? + alt = UTILS.FeetToMeters( 300 ) -- ? elseif harrier then - -- 300-325 ft - alt=UTILS.FeetToMeters(300)-- Need to verify + alt=UTILS.FeetToMeters(312)-- 300-325 ft + end - aoa=aoaac.OnSpeed + aoa = aoaac.OnSpeed end @@ -103214,30 +109701,30 @@ end function AIRBOSS:_GetNextMarshalFight() -- Loop over all marshal flights. - for _,_flight in pairs(self.Qmarshal) do - local flight=_flight --#AIRBOSS.FlightGroup + for _, _flight in pairs( self.Qmarshal ) do + local flight = _flight -- #AIRBOSS.FlightGroup -- Current stack. - local stack=flight.flag + local stack = flight.flag -- Total marshal time in seconds. - local Tmarshal=timer.getAbsTime()-flight.time + local Tmarshal = timer.getAbsTime() - flight.time -- Min time in marshal stack. - local TmarshalMin=2*60 --Two minutes for human players. + local TmarshalMin = 2 * 60 -- Two minutes for human players. if flight.ai then - TmarshalMin=3*60 -- Three minutes for AI. + TmarshalMin = 3 * 60 -- Three minutes for AI. end -- Check if conditions are right. - if flight.holding~=nil and Tmarshal>=TmarshalMin then - if flight.case==1 and stack==1 or flight.case>1 then + if flight.holding ~= nil and Tmarshal >= TmarshalMin then + if flight.case == 1 and stack == 1 or flight.case > 1 then if flight.ai then -- Return AI flight. return flight else -- Check for human player if they are already commencing. - if flight.step~=AIRBOSS.PatternStep.COMMENCING then + if flight.step ~= AIRBOSS.PatternStep.COMMENCING then return flight end end @@ -103254,34 +109741,34 @@ function AIRBOSS:_CheckQueue() -- Print queues. if self.Debug then - self:_PrintQueue(self.flights, "All Flights") + self:_PrintQueue( self.flights, "All Flights" ) end - self:_PrintQueue(self.Qmarshal, "Marshal") - self:_PrintQueue(self.Qpattern, "Pattern") - self:_PrintQueue(self.Qwaiting, "Waiting") - self:_PrintQueue(self.Qspinning, "Spinning") + self:_PrintQueue( self.Qmarshal, "Marshal" ) + self:_PrintQueue( self.Qpattern, "Pattern" ) + self:_PrintQueue( self.Qwaiting, "Waiting" ) + self:_PrintQueue( self.Qspinning, "Spinning" ) -- If flights are waiting outside 10 NM zone and carrier switches from Case I to Case II/III, they should be added to the Marshal stack as now there is no stack limit any more. - if self.case>1 then - for _,_flight in pairs(self.Qwaiting) do - local flight=_flight --#AIRBOSS.FlightGroup + if self.case > 1 then + for _, _flight in pairs( self.Qwaiting ) do + local flight = _flight -- #AIRBOSS.FlightGroup -- Remove flight from waiting queue. - local removed=self:_RemoveFlightFromQueue(self.Qwaiting, flight) + local removed = self:_RemoveFlightFromQueue( self.Qwaiting, flight ) if removed then -- Get free stack - local stack=self:_GetFreeStack(flight.ai) + local stack = self:_GetFreeStack( flight.ai ) -- Debug info. - self:T(self.lid..string.format("Moving flight %s onboard %s from Waiting queue to Case %d Marshal stack %d", flight.groupname, flight.onboard, self.case, stack)) + self:T( self.lid .. string.format( "Moving flight %s onboard %s from Waiting queue to Case %d Marshal stack %d", flight.groupname, flight.onboard, self.case, stack ) ) -- Send flight to marshal stack. if flight.ai then - self:_MarshalAI(flight, stack) + self:_MarshalAI( flight, stack ) else - self:_MarshalPlayer(flight, stack) + self:_MarshalPlayer( flight, stack ) end -- Break the loop so that only one flight per 30 seconds is removed. @@ -103299,40 +109786,40 @@ function AIRBOSS:_CheckQueue() ----------------------------- -- Loop over all flights currently in the marshal queue. - for _,_flight in pairs(self.Qmarshal) do - local flight=_flight --#AIRBOSS.FlightGroup + for _, _flight in pairs( self.Qmarshal ) do + local flight = _flight -- #AIRBOSS.FlightGroup -- TODO: In principle this should be done/necessary only if case 1-->2/3 or 2/3-->1, right? -- When recovery switches from 2->3 or 3-->2 nothing changes in the marshal stack. -- Check if a change of stack is necessary. - if (flight.case==1 and self.case>1) or (flight.case>1 and self.case==1) then + if (flight.case == 1 and self.case > 1) or (flight.case > 1 and self.case == 1) then -- Remove flight from marshal queue. - local removed=self:_RemoveFlightFromQueue(self.Qmarshal, flight) + local removed = self:_RemoveFlightFromQueue( self.Qmarshal, flight ) if removed then -- Get free stack - local stack=self:_GetFreeStack(flight.ai) + local stack = self:_GetFreeStack( flight.ai ) -- Debug output. - self:T(self.lid..string.format("Moving flight %s onboard %s from Marshal Case %d ==> %d Marshal stack %d", flight.groupname, flight.onboard, flight.case, self.case, stack)) + self:T( self.lid .. string.format( "Moving flight %s onboard %s from Marshal Case %d ==> %d Marshal stack %d", flight.groupname, flight.onboard, flight.case, self.case, stack ) ) -- Send flight to marshal queue. if flight.ai then - self:_MarshalAI(flight, stack) + self:_MarshalAI( flight, stack ) else - self:_MarshalPlayer(flight, stack) + self:_MarshalPlayer( flight, stack ) end -- Break the loop so that only one flight per 30 seconds is removed. No spam of messages, no conflict with the loop over queue entries. break - elseif flight.case~=self.case then + elseif flight.case ~= self.case then -- This should handle 2-->3 or 3-->2 - flight.case=self.case + flight.case = self.case end @@ -103344,85 +109831,90 @@ function AIRBOSS:_CheckQueue() end -- Get number of airborne aircraft units(!) currently in pattern. - local _,npattern=self:_GetQueueInfo(self.Qpattern) + local _, npattern = self:_GetQueueInfo( self.Qpattern ) -- Get number of aircraft units spinning. - local _,nspinning=self:_GetQueueInfo(self.Qspinning) + local _, nspinning = self:_GetQueueInfo( self.Qspinning ) -- Get next marshal flight. - local marshalflight=self:_GetNextMarshalFight() + local marshalflight = self:_GetNextMarshalFight() -- Check if there are flights waiting in the Marshal stack and if the pattern is free. No one should be spinning. - if marshalflight and npattern0 then + local Tpattern = 9999 + local npunits = 1 + local pcase = 1 + if npattern > 0 then -- Last flight group send to pattern. - local patternflight=self.Qpattern[#self.Qpattern] --#AIRBOSS.FlightGroup + local patternflight = self.Qpattern[#self.Qpattern] -- #AIRBOSS.FlightGroup -- Recovery case of pattern flight. - pcase=patternflight.case + pcase = patternflight.case -- Number of airborne aircraft in this group. Count includes section members. - local npunits=self:_GetFlightUnits(patternflight, false) + local npunits = self:_GetFlightUnits( patternflight, false ) -- Get time in pattern. - Tpattern=timer.getAbsTime()-patternflight.time - self:T(self.lid..string.format("Pattern time of last group %s = %d seconds. # of units=%d.", patternflight.groupname, Tpattern, npunits)) + Tpattern = timer.getAbsTime() - patternflight.time + self:T( self.lid .. string.format( "Pattern time of last group %s = %d seconds. # of units=%d.", patternflight.groupname, Tpattern, npunits ) ) end -- Min time in pattern before next aircraft is allowed. local TpatternMin - if pcase==1 then - TpatternMin=2*60*npunits --45*npunits -- 45 seconds interval per plane! + if pcase == 1 then + TpatternMin = 2 * 60 * npunits -- 45*npunits -- 45 seconds interval per plane! else - TpatternMin=2*60*npunits --120*npunits -- 120 seconds interval per plane! + TpatternMin = 2 * 60 * npunits -- 120*npunits -- 120 seconds interval per plane! end -- Check interval to last pattern flight. - if Tpattern>TpatternMin then - self:T(self.lid..string.format("Sending marshal flight %s to pattern.", marshalflight.groupname)) - self:_ClearForLanding(marshalflight) + if Tpattern > TpatternMin then + self:T( self.lid .. string.format( "Sending marshal flight %s to pattern.", marshalflight.groupname ) ) + self:_ClearForLanding( marshalflight ) end end end - --- Clear flight for landing. AI are removed from Marshal queue and the Marshal stack is collapsed. -- If next in line is an AI flight, this is done. If human player is next, we wait for "Commence" via F10 radio menu command. -- @param #AIRBOSS self -- @param #AIRBOSS.FlightGroup flight Flight to go to pattern. -function AIRBOSS:_ClearForLanding(flight) +function AIRBOSS:_ClearForLanding( flight ) -- Check if flight is AI or human. If AI, we collapse the stack and commence. If human, we suggest to commence. if flight.ai then -- Collapse stack and send AI to pattern. - self:_RemoveFlightFromMarshalQueue(flight, false) - self:_LandAI(flight) + self:_RemoveFlightFromMarshalQueue( flight, false ) + self:_LandAI( flight ) -- Cleared for Case X recovery. - self:_MarshalCallClearedForRecovery(flight.onboard, flight.case) + self:_MarshalCallClearedForRecovery( flight.onboard, flight.case ) + + -- Voice over of the commencing simulated call from AI + if self.xtVoiceOversAI then + local leader = flight.group:GetUnits()[1] + self:_CommencingCall(leader, flight.onboard) + end else -- Cleared for Case X recovery. - if flight.step~=AIRBOSS.PatternStep.COMMENCING then - self:_MarshalCallClearedForRecovery(flight.onboard, flight.case) - flight.time=timer.getAbsTime() + if flight.step ~= AIRBOSS.PatternStep.COMMENCING then + self:_MarshalCallClearedForRecovery( flight.onboard, flight.case ) + flight.time = timer.getAbsTime() end -- Set step to commencing. This will trigger the zone check until the player is in the right place. - self:_SetPlayerStep(flight, AIRBOSS.PatternStep.COMMENCING, 3) + self:_SetPlayerStep( flight, AIRBOSS.PatternStep.COMMENCING, 3 ) end @@ -103433,25 +109925,25 @@ end -- @param #AIRBOSS.PlayerData playerData Player data. -- @param #string step Next step. -- @param #number delay (Optional) Set set after a delay in seconds. -function AIRBOSS:_SetPlayerStep(playerData, step, delay) +function AIRBOSS:_SetPlayerStep( playerData, step, delay ) - if delay and delay>0 then + if delay and delay > 0 then -- Delayed call. - --SCHEDULER:New(nil, self._SetPlayerStep, {self, playerData, step}, delay) - self:ScheduleOnce(delay, self._SetPlayerStep, self, playerData, step) + -- SCHEDULER:New(nil, self._SetPlayerStep, {self, playerData, step}, delay) + self:ScheduleOnce( delay, self._SetPlayerStep, self, playerData, step ) else -- Check if player still exists after possible delay. if playerData then -- Set player step. - playerData.step=step + playerData.step = step -- Erase warning. - playerData.warning=nil + playerData.warning = nil -- Next step hint. - self:_StepHint(playerData) + self:_StepHint( playerData ) end end @@ -103463,92 +109955,89 @@ end function AIRBOSS:_ScanCarrierZone() -- Carrier position. - local coord=self:GetCoordinate() + local coord = self:GetCoordinate() -- Scan radius = radius of the CCA. - local RCCZ=self.zoneCCA:GetRadius() + local RCCZ = self.zoneCCA:GetRadius() -- Debug info. - self:T(self.lid..string.format("Scanning Carrier Controlled Area. Radius=%.1f NM.", UTILS.MetersToNM(RCCZ))) + self:T( self.lid .. string.format( "Scanning Carrier Controlled Area. Radius=%.1f NM.", UTILS.MetersToNM( RCCZ ) ) ) -- Scan units in carrier zone. - local _,_,_,unitscan=coord:ScanObjects(RCCZ, true, false, false) - + local _, _, _, unitscan = coord:ScanObjects( RCCZ, true, false, false ) -- Make a table with all groups currently in the CCA zone. - local insideCCA={} - for _,_unit in pairs(unitscan) do - local unit=_unit --Wrapper.Unit#UNIT + local insideCCA = {} + for _, _unit in pairs( unitscan ) do + local unit = _unit -- Wrapper.Unit#UNIT -- Necessary conditions to be met: - local airborne=unit:IsAir() --and unit:InAir() - local inzone=unit:IsInZone(self.zoneCCA) - local friendly=self:GetCoalition()==unit:GetCoalition() - local carrierac=self:_IsCarrierAircraft(unit) + local airborne = unit:IsAir() -- and unit:InAir() + local inzone = unit:IsInZone( self.zoneCCA ) + local friendly = self:GetCoalition() == unit:GetCoalition() + local carrierac = self:_IsCarrierAircraft( unit ) -- Check if this an aircraft and that it is airborne and closing in. if airborne and inzone and friendly and carrierac then - local group=unit:GetGroup() - local groupname=group:GetName() + local group = unit:GetGroup() + local groupname = group:GetName() - if insideCCA[groupname]==nil then - insideCCA[groupname]=group + if insideCCA[groupname] == nil then + insideCCA[groupname] = group end end end -- Find new flights that are inside CCA. - for groupname,_group in pairs(insideCCA) do - local group=_group --Wrapper.Group#GROUP + for groupname, _group in pairs( insideCCA ) do + local group = _group -- Wrapper.Group#GROUP -- Get flight group if possible. - local knownflight=self:_GetFlightFromGroupInQueue(group, self.flights) + local knownflight = self:_GetFlightFromGroupInQueue( group, self.flights ) -- Get aircraft type name. - local actype=group:GetTypeName() + local actype = group:GetTypeName() -- Create a new flight group if knownflight then -- Check if flight is AI and if we want to handle it at all. - if knownflight.ai and knownflight.flag==-100 and self.handleai then + if knownflight.ai and knownflight.flag == -100 and self.handleai then - local putintomarshal=false + local putintomarshal = false -- Get flight group. - local flight=_DATABASE:GetFlightGroup(groupname) + local flight = _DATABASE:GetOpsGroup( groupname ) - if flight and flight:IsInbound() and flight.destbase:GetName()==self.carrier:GetName() then + if flight and flight:IsInbound() and flight.destbase:GetName() == self.carrier:GetName() then if flight.ishelo then else - putintomarshal=true + putintomarshal = true end - flight.airboss=self + flight.airboss = self end - - -- Send AI flight to marshal stack. if putintomarshal then -- Get the next free stack for current recovery case. - local stack=self:_GetFreeStack(knownflight.ai) + local stack = self:_GetFreeStack( knownflight.ai ) -- Repawn. - local respawn=self.respawnAI + local respawn = self.respawnAI if stack then -- Send AI to marshal stack. We respawn the group to clean possible departure and destination airbases. - self:_MarshalAI(knownflight, stack, respawn) + self:_MarshalAI( knownflight, stack, respawn ) else -- Send AI to orbit outside 10 NM zone and wait until the next Marshal stack is available. - if not self:_InQueue(self.Qwaiting, knownflight.group) then - self:_WaitAI(knownflight, respawn) -- Group is respawned to clear any attached airfields. + if not self:_InQueue( self.Qwaiting, knownflight.group ) then + self:_WaitAI( knownflight, respawn ) -- Group is respawned to clear any attached airfields. end end @@ -103556,36 +110045,35 @@ function AIRBOSS:_ScanCarrierZone() -- Break the loop to not have all flights at once! Spams the message screen. break - end -- Closed in or tanker/AWACS + end -- Closed in or tanker/AWACS end else -- Unknown new AI flight. Create a new flight group. - if not self:_IsHuman(group) then - self:_CreateFlightGroup(group) + if not self:_IsHuman( group ) then + self:_CreateFlightGroup( group ) end - end end -- Find flights that are not in CCA. - local remove={} - for _,_flight in pairs(self.flights) do - local flight=_flight --#AIRBOSS.FlightGroup - if insideCCA[flight.groupname]==nil then + local remove = {} + for _, _flight in pairs( self.flights ) do + local flight = _flight -- #AIRBOSS.FlightGroup + if insideCCA[flight.groupname] == nil then -- Do not remove flights in marshal pattern. At least for case 2 & 3. If zone is set small, they might be outside in the holding pattern. - if flight.ai and not (self:_InQueue(self.Qmarshal, flight.group) or self:_InQueue(self.Qpattern, flight.group)) then - table.insert(remove, flight) + if flight.ai and not (self:_InQueue( self.Qmarshal, flight.group ) or self:_InQueue( self.Qpattern, flight.group )) then + table.insert( remove, flight ) end end end -- Remove flight groups outside CCA. - for _,flight in pairs(remove) do - self:_RemoveFlightFromQueue(self.flights, flight) + for _, flight in pairs( remove ) do + self:_RemoveFlightFromQueue( self.flights, flight ) end end @@ -103593,82 +110081,81 @@ end --- Tell player to wait outside the 10 NM zone until a Marshal stack is available. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. -function AIRBOSS:_WaitPlayer(playerData) +function AIRBOSS:_WaitPlayer( playerData ) -- Check if flight is known to the airboss already. if playerData then -- Number of waiting flights - local nwaiting=#self.Qwaiting + local nwaiting = #self.Qwaiting -- Radio message: Stack is full. - self:_MarshalCallStackFull(playerData.onboard, nwaiting) + self:_MarshalCallStackFull( playerData.onboard, nwaiting ) -- Add player flight to waiting queue. - table.insert(self.Qwaiting, playerData) + table.insert( self.Qwaiting, playerData ) -- Set time stamp. - playerData.time=timer.getAbsTime() + playerData.time = timer.getAbsTime() -- Set step to waiting. - playerData.step=AIRBOSS.PatternStep.WAITING - playerData.warning=nil + playerData.step = AIRBOSS.PatternStep.WAITING + playerData.warning = nil -- Set all flights in section to waiting. - for _,_flight in pairs(playerData.section) do - local flight=_flight --#AIRBOSS.PlayerData - flight.step=AIRBOSS.PatternStep.WAITING - flight.time=timer.getAbsTime() - flight.warning=nil + for _, _flight in pairs( playerData.section ) do + local flight = _flight -- #AIRBOSS.PlayerData + flight.step = AIRBOSS.PatternStep.WAITING + flight.time = timer.getAbsTime() + flight.warning = nil end end end - --- Orbit at a specified position at a specified altitude with a specified speed. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. -- @param #number stack The Marshal stack the player gets. -function AIRBOSS:_MarshalPlayer(playerData, stack) +function AIRBOSS:_MarshalPlayer( playerData, stack ) -- Check if flight is known to the airboss already. if playerData then -- Add group to marshal stack. - self:_AddMarshalGroup(playerData, stack) + self:_AddMarshalGroup( playerData, stack ) -- Set step to holding. - self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.HOLDING) + self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.HOLDING ) -- Holding switch to nil until player arrives in the holding zone. - playerData.holding=nil + playerData.holding = nil -- Set same stack for all flights in section. - for _,_flight in pairs(playerData.section) do - local flight=_flight --#AIRBOSS.PlayerData + for _, _flight in pairs( playerData.section ) do + local flight = _flight -- #AIRBOSS.PlayerData -- XXX: Inform player? Should be done by lead via radio? -- Set step. - self:_SetPlayerStep(flight, AIRBOSS.PatternStep.HOLDING) + self:_SetPlayerStep( flight, AIRBOSS.PatternStep.HOLDING ) -- Holding to nil, until arrived. - flight.holding=nil + flight.holding = nil -- Set case to that of lead. - flight.case=playerData.case + flight.case = playerData.case -- Set stack flag. - flight.flag=stack + flight.flag = stack - -- Trigger Marshal event. - self:Marshal(flight) + -- Trigger Marshal event. + self:Marshal( flight ) end else - self:E(self.lid.."ERROR: Could not add player to Marshal stack! playerData=nil") + self:E( self.lid .. "ERROR: Could not add player to Marshal stack! playerData=nil" ) end end @@ -103678,61 +110165,61 @@ end -- @param #AIRBOSS self -- @param #AIRBOSS.FlightGroup flight Flight group. -- @param #boolean respawn If true respawn the group. Otherwise reset the mission task with new waypoints. -function AIRBOSS:_WaitAI(flight, respawn) +function AIRBOSS:_WaitAI( flight, respawn ) -- Set flag to something other than -100 and <0 - flight.flag=-99 + flight.flag = -99 -- Add AI flight to waiting queue. - table.insert(self.Qwaiting, flight) + table.insert( self.Qwaiting, flight ) -- Flight group name. - local group=flight.group - local groupname=flight.groupname + local group = flight.group + local groupname = flight.groupname -- Aircraft speed 274 knots TAS ~= 250 KIAS when orbiting the pattern. (Orbit expects m/s.) - local speedOrbitMps=UTILS.KnotsToMps(274) + local speedOrbitMps = UTILS.KnotsToMps( 274 ) -- Orbit speed in km/h for waypoints. - local speedOrbitKmh=UTILS.KnotsToKmph(274) + local speedOrbitKmh = UTILS.KnotsToKmph( 274 ) -- Aircraft speed 400 knots when transiting to holding zone. (Waypoint expects km/h.) - local speedTransit=UTILS.KnotsToKmph(370) + local speedTransit = UTILS.KnotsToKmph( 370 ) -- Carrier coordinate - local cv=self:GetCoordinate() + local cv = self:GetCoordinate() -- Coordinate of flight group - local fc=group:GetCoordinate() + local fc = group:GetCoordinate() -- Carrier heading - local hdg=self:GetHeading(false) + local hdg = self:GetHeading( false ) -- Heading from carrier to flight group - local hdgto=cv:HeadingTo(fc) + local hdgto = cv:HeadingTo( fc ) -- Holding alitude between angels 6 and 10 (random). - local angels=math.random(6,10) - local altitude=UTILS.FeetToMeters(angels*1000) + local angels = math.random( 6, 10 ) + local altitude = UTILS.FeetToMeters( angels * 1000 ) -- Point outsize 10 NM zone of the carrier. - local p0=cv:Translate(UTILS.NMToMeters(11), hdgto):Translate(UTILS.NMToMeters(5), hdg):SetAltitude(altitude) + local p0 = cv:Translate( UTILS.NMToMeters( 11 ), hdgto ):Translate( UTILS.NMToMeters( 5 ), hdg ):SetAltitude( altitude ) -- Waypoints array to be filled depending on case etc. - local wp={} + local wp = {} -- Current position. Always good for as the first waypoint. - wp[1]=group:GetCoordinate():WaypointAirTurningPoint(nil, speedTransit, {}, "Current Position") + wp[1] = group:GetCoordinate():WaypointAirTurningPoint( nil, speedTransit, {}, "Current Position" ) -- Set orbit task. - local taskorbit=group:TaskOrbit(p0, altitude, speedOrbitMps) + local taskorbit = group:TaskOrbit( p0, altitude, speedOrbitMps ) -- Orbit at waypoint. - wp[#wp+1]=p0:WaypointAirTurningPoint(nil, speedOrbitKmh, {taskorbit}, string.format("Waiting Orbit at Angels %d", angels)) + wp[#wp + 1] = p0:WaypointAirTurningPoint( nil, speedOrbitKmh, { taskorbit }, string.format( "Waiting Orbit at Angels %d", angels ) ) -- Debug markers. if self.Debug then - p0:MarkToAll(string.format("Waiting Orbit of flight %s at Angels %s", groupname, angels)) + p0:MarkToAll( string.format( "Waiting Orbit of flight %s at Angels %s", groupname, angels ) ) end if respawn then @@ -103741,21 +110228,21 @@ function AIRBOSS:_WaitAI(flight, respawn) -- Note: This resets the weapons and the fuel state. But not the units fortunately. -- Get group template. - local Template=group:GetTemplate() + local Template = group:GetTemplate() -- Set route points. - Template.route.points=wp + Template.route.points = wp -- Respawn the group. - group=group:Respawn(Template, true) + group = group:Respawn( Template, true ) end -- Reinit waypoints. - group:WayPointInitialize(wp) + group:WayPointInitialize( wp ) -- Route group. - group:Route(wp, 1) + group:Route( wp, 1 ) end @@ -103765,69 +110252,74 @@ end -- @param #AIRBOSS.FlightGroup flight Flight group. -- @param #number nstack Stack number of group. Can also be the current stack if AI position needs to be updated wrt to changed carrier position. -- @param #boolean respawn If true, respawn the flight otherwise update mission task with new waypoints. -function AIRBOSS:_MarshalAI(flight, nstack, respawn) - self:F2({flight=flight, nstack=nstack, respawn=respawn}) +function AIRBOSS:_MarshalAI( flight, nstack, respawn ) + self:F2( { flight = flight, nstack = nstack, respawn = respawn } ) -- Nil check. - if flight==nil or flight.group==nil then - self:E(self.lid.."ERROR: flight or flight.group is nil.") + if flight == nil or flight.group == nil then + self:E( self.lid .. "ERROR: flight or flight.group is nil." ) return end -- Nil check. - if flight.group:GetCoordinate()==nil then - self:E(self.lid.."ERROR: cannot get coordinate of flight group.") + if flight.group:GetCoordinate() == nil then + self:E( self.lid .. "ERROR: cannot get coordinate of flight group." ) return end -- Check if flight is already in Marshal queue. if not self:_InQueue(self.Qmarshal,flight.group) then + -- Simulate inbound call + if self.xtVoiceOversAI then + local leader = flight.group:GetUnits()[1] + self:_MarshallInboundCall(leader, flight.onboard) + end -- Add group to marshal stack queue. - self:_AddMarshalGroup(flight, nstack) + self:_AddMarshalGroup( flight, nstack ) end -- Explode unit for testing. Worked! - --local u1=flight.group:GetUnit(1) --Wrapper.Unit#UNIT - --u1:Explode(500, 10) + -- local u1=flight.group:GetUnit(1) --Wrapper.Unit#UNIT + -- u1:Explode(500, 10) -- Recovery case. - local case=flight.case + local case = flight.case -- Get old/current stack. - local ostack=flight.flag + local ostack = flight.flag -- Flight group name. - local group=flight.group - local groupname=flight.groupname + local group = flight.group + local groupname = flight.groupname -- Set new stack. - flight.flag=nstack + flight.flag = nstack -- Current carrier position. - local Carrier=self:GetCoordinate() + local Carrier = self:GetCoordinate() -- Carrier heading. - local hdg=self:GetHeading() + local hdg = self:GetHeading() -- Aircraft speed 274 knots TAS ~= 250 KIAS when orbiting the pattern. (Orbit expects m/s.) - local speedOrbitMps=UTILS.KnotsToMps(274) + local speedOrbitMps = UTILS.KnotsToMps( 274 ) -- Orbit speed in km/h for waypoints. - local speedOrbitKmh=UTILS.KnotsToKmph(274) + local speedOrbitKmh = UTILS.KnotsToKmph( 274 ) -- Aircraft speed 400 knots when transiting to holding zone. (Waypoint expects km/h.) - local speedTransit=UTILS.KnotsToKmph(370) + local speedTransit = UTILS.KnotsToKmph( 370 ) local altitude - local p0 --Core.Point#COORDINATE - local p1 --Core.Point#COORDINATE - local p2 --Core.Point#COORDINATE + local p0 -- Core.Point#COORDINATE + local p1 -- Core.Point#COORDINATE + local p2 -- Core.Point#COORDINATE -- Get altitude and positions. - altitude, p1, p2=self:_GetMarshalAltitude(nstack, case) + altitude, p1, p2 = self:_GetMarshalAltitude( nstack, case ) -- Waypoints array to be filled depending on case etc. - local wp={} + local wp = {} -- If flight has not arrived in the holding zone, we guide it there. if not flight.holding then @@ -103837,36 +110329,36 @@ function AIRBOSS:_MarshalAI(flight, nstack, respawn) ---------------------- -- Debug info. - self:T(self.lid..string.format("Guiding AI flight %s to marshal stack %d-->%d.", groupname, ostack, nstack)) + self:T( self.lid .. string.format( "Guiding AI flight %s to marshal stack %d-->%d.", groupname, ostack, nstack ) ) -- Current position. Always good for as the first waypoint. - wp[1]=group:GetCoordinate():WaypointAirTurningPoint(nil, speedTransit, {}, "Current Position") + wp[1] = group:GetCoordinate():WaypointAirTurningPoint( nil, speedTransit, {}, "Current Position" ) -- Task function when arriving at the holding zone. This will set flight.holding=true. - local TaskArrivedHolding=flight.group:TaskFunction("AIRBOSS._ReachedHoldingZone", self, flight) + local TaskArrivedHolding = flight.group:TaskFunction( "AIRBOSS._ReachedHoldingZone", self, flight ) -- Select case. - if case==1 then + if case == 1 then -- Initial point 7 NM and a bit port of carrier. - local pE=Carrier:Translate(UTILS.NMToMeters(7), hdg-30):SetAltitude(altitude) + local pE = Carrier:Translate( UTILS.NMToMeters( 7 ), hdg - 30 ):SetAltitude( altitude ) -- Entry point 5 NM port and slightly astern the boat. - p0=Carrier:Translate(UTILS.NMToMeters(5), hdg-135):SetAltitude(altitude) + p0 = Carrier:Translate( UTILS.NMToMeters( 5 ), hdg - 135 ):SetAltitude( altitude ) -- Waypoint ahead of carrier's holding zone. - wp[#wp+1]=pE:WaypointAirTurningPoint(nil, speedTransit, {TaskArrivedHolding}, "Entering Case I Marshal Pattern") + wp[#wp + 1] = pE:WaypointAirTurningPoint( nil, speedTransit, { TaskArrivedHolding }, "Entering Case I Marshal Pattern" ) else -- Get correct radial depending on recovery case including offset. - local radial=self:GetRadial(case, false, true) + 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") + wp[#wp + 1] = p0:WaypointAirTurningPoint( nil, speedTransit, { TaskArrivedHolding }, "Entering Case II/III Marshal Pattern" ) end @@ -103877,27 +110369,27 @@ function AIRBOSS:_MarshalAI(flight, nstack, respawn) ------------------------ -- Debug info. - self:T(self.lid..string.format("Updating AI flight %s at marshal stack %d-->%d.", groupname, ostack, nstack)) + self:T( self.lid .. string.format( "Updating AI flight %s at marshal stack %d-->%d.", groupname, ostack, nstack ) ) -- Current position. Speed expected in km/h. - wp[1]=group:GetCoordinate():WaypointAirTurningPoint(nil, speedOrbitKmh, {}, "Current Position") + wp[1] = group:GetCoordinate():WaypointAirTurningPoint( nil, speedOrbitKmh, {}, "Current Position" ) -- Create new waypoint 0.2 Nm ahead of current positon. - p0=group:GetCoordinate():Translate(UTILS.NMToMeters(0.2), group:GetHeading(), true) + p0 = group:GetCoordinate():Translate( UTILS.NMToMeters( 0.2 ), group:GetHeading(), true ) end -- Set orbit task. - local taskorbit=group:TaskOrbit(p1, altitude, speedOrbitMps, p2) + local taskorbit = group:TaskOrbit( p1, altitude, speedOrbitMps, p2 ) -- Orbit at waypoint. - wp[#wp+1]=p0:WaypointAirTurningPoint(nil, speedOrbitKmh, {taskorbit}, string.format("Marshal Orbit Stack %d", nstack)) + wp[#wp + 1] = p0:WaypointAirTurningPoint( nil, speedOrbitKmh, { taskorbit }, string.format( "Marshal Orbit Stack %d", nstack ) ) -- Debug markers. if self.Debug then - p0:MarkToAll("WP P0 "..groupname) - p1:MarkToAll("RT P1 "..groupname) - p2:MarkToAll("RT P2 "..groupname) + p0:MarkToAll( "WP P0 " .. groupname ) + p1:MarkToAll( "RT P1 " .. groupname ) + p2:MarkToAll( "RT P2 " .. groupname ) end if respawn then @@ -103906,40 +110398,40 @@ function AIRBOSS:_MarshalAI(flight, nstack, respawn) -- Note: This resets the weapons and the fuel state. But not the units fortunately. -- Get group template. - local Template=group:GetTemplate() + local Template = group:GetTemplate() -- Set route points. - Template.route.points=wp + Template.route.points = wp -- Respawn the group. - flight.group=group:Respawn(Template, true) + flight.group = group:Respawn( Template, true ) end -- Reinit waypoints. - flight.group:WayPointInitialize(wp) + flight.group:WayPointInitialize( wp ) -- Route group. - flight.group:Route(wp, 1) + flight.group:Route( wp, 1 ) -- Trigger Marshal event. - self:Marshal(flight) + self:Marshal( flight ) end --- Tell AI to refuel. Either at the recovery tanker or at the nearest divert airfield. -- @param #AIRBOSS self -- @param #AIRBOSS.FlightGroup flight Flight group. -function AIRBOSS:_RefuelAI(flight) +function AIRBOSS:_RefuelAI( flight ) -- Waypoints array. - local wp={} + local wp = {} -- Current speed. - local CurrentSpeed=flight.group:GetVelocityKMH() + local CurrentSpeed = flight.group:GetVelocityKMH() -- Current positon. - wp[#wp+1]=flight.group:GetCoordinate():WaypointAirTurningPoint(nil, CurrentSpeed, {}, "Current position") + wp[#wp + 1] = flight.group:GetCoordinate():WaypointAirTurningPoint( nil, CurrentSpeed, {}, "Current position" ) -- Check if aircraft can be refueled. -- TODO: This should also depend on the tanker type AC. @@ -103957,25 +110449,25 @@ function AIRBOSS:_RefuelAI(flight) end -- Message. - local text="" + local text = "" -- Refuel or divert? if self.tanker and refuelac then -- Current Tanker position. - local tankerpos=self.tanker.tanker:GetCoordinate() + local tankerpos = self.tanker.tanker:GetCoordinate() -- Task refueling. - local TaskRefuel=flight.group:TaskRefueling() + local TaskRefuel = flight.group:TaskRefueling() -- Task to go back to Marshal. - local TaskMarshal=flight.group:TaskFunction("AIRBOSS._TaskFunctionMarshalAI", self, flight) + local TaskMarshal = flight.group:TaskFunction( "AIRBOSS._TaskFunctionMarshalAI", self, flight ) -- Waypoint with tasks. - wp[#wp+1]=tankerpos:WaypointAirTurningPoint(nil, CurrentSpeed, {TaskRefuel, TaskMarshal}, "Refueling") + wp[#wp + 1] = tankerpos:WaypointAirTurningPoint( nil, CurrentSpeed, { TaskRefuel, TaskMarshal }, "Refueling" ) -- Marshal Message. - self:_MarshalCallGasAtTanker(flight.onboard) + self:_MarshalCallGasAtTanker( flight.onboard ) else @@ -103983,109 +110475,109 @@ function AIRBOSS:_RefuelAI(flight) -- Guide AI to divert field -- ------------------------------ - -- Closest Airfield of the coaliton. - local divertfield=self:GetCoordinate():GetClosestAirbase(Airbase.Category.AIRDROME, self:GetCoalition()) + -- Closest Airfield of the coalition. + local divertfield = self:GetCoordinate():GetClosestAirbase( Airbase.Category.AIRDROME, self:GetCoalition() ) -- Handle case where there is no divert field of the own coalition and try neutral instead. - if divertfield==nil then - divertfield=self:GetCoordinate():GetClosestAirbase(Airbase.Category.AIRDROME, 0) + if divertfield == nil then + divertfield = self:GetCoordinate():GetClosestAirbase( Airbase.Category.AIRDROME, 0 ) end if divertfield then -- Coordinate. - local divertcoord=divertfield:GetCoordinate() + local divertcoord = divertfield:GetCoordinate() -- Landing waypoint. - wp[#wp+1]=divertcoord:WaypointAirLanding(UTILS.KnotsToKmph(300), divertfield, {}, "Divert Field") + wp[#wp + 1] = divertcoord:WaypointAirLanding( UTILS.KnotsToKmph( 300 ), divertfield, {}, "Divert Field" ) -- Marshal Message. - self:_MarshalCallGasAtDivert(flight.onboard, divertfield:GetName()) + self:_MarshalCallGasAtDivert( flight.onboard, divertfield:GetName() ) -- Respawn! -- Get group template. - local Template=flight.group:GetTemplate() + local Template = flight.group:GetTemplate() -- Set route points. - Template.route.points=wp + Template.route.points = wp -- Respawn the group. - flight.group=flight.group:Respawn(Template, true) + flight.group = flight.group:Respawn( Template, true ) else -- Set flight to refueling so this is not called again. - self:E(self.lid..string.format("WARNING: No recovery tanker or divert field available for group %s.", flight.groupname)) - flight.refueling=true + self:E( self.lid .. string.format( "WARNING: No recovery tanker or divert field available for group %s.", flight.groupname ) ) + flight.refueling = true return end end -- Reinit waypoints. - flight.group:WayPointInitialize(wp) + flight.group:WayPointInitialize( wp ) -- Route group. - flight.group:Route(wp, 1) + flight.group:Route( wp, 1 ) -- Set refueling switch. - flight.refueling=true + flight.refueling = true end --- Tell AI to land on the carrier. -- @param #AIRBOSS self -- @param #AIRBOSS.FlightGroup flight Flight group. -function AIRBOSS:_LandAI(flight) +function AIRBOSS:_LandAI( flight ) - -- Debug info. - self:T(self.lid..string.format("Landing AI flight %s.", flight.groupname)) + -- Debug info. + self:T( self.lid .. string.format( "Landing AI flight %s.", flight.groupname ) ) -- NOTE: Looks like the AI needs to approach at the "correct" speed. If they are too fast, they fly an unnecessary circle to bleed of speed first. -- Unfortunately, the correct speed depends on the aircraft type! -- Aircraft speed when flying the pattern. - local Speed=UTILS.KnotsToKmph(200) + local Speed = UTILS.KnotsToKmph( 200 ) - if flight.actype==AIRBOSS.AircraftCarrier.HORNET or flight.actype==AIRBOSS.AircraftCarrier.FA18C then - Speed=UTILS.KnotsToKmph(200) - elseif flight.actype==AIRBOSS.AircraftCarrier.E2D then - Speed=UTILS.KnotsToKmph(150) - elseif flight.actype==AIRBOSS.AircraftCarrier.F14A_AI or flight.actype==AIRBOSS.AircraftCarrier.F14A or flight.actype==AIRBOSS.AircraftCarrier.F14B then - Speed=UTILS.KnotsToKmph(175) - elseif flight.actype==AIRBOSS.AircraftCarrier.S3B or flight.actype==AIRBOSS.AircraftCarrier.S3BTANKER then - Speed=UTILS.KnotsToKmph(140) + if flight.actype == AIRBOSS.AircraftCarrier.HORNET or flight.actype == AIRBOSS.AircraftCarrier.FA18C then + Speed = UTILS.KnotsToKmph( 200 ) + elseif flight.actype == AIRBOSS.AircraftCarrier.E2D then + Speed = UTILS.KnotsToKmph( 150 ) + elseif flight.actype == AIRBOSS.AircraftCarrier.F14A_AI or flight.actype == AIRBOSS.AircraftCarrier.F14A or flight.actype == AIRBOSS.AircraftCarrier.F14B then + Speed = UTILS.KnotsToKmph( 175 ) + elseif flight.actype == AIRBOSS.AircraftCarrier.S3B or flight.actype == AIRBOSS.AircraftCarrier.S3BTANKER then + Speed = UTILS.KnotsToKmph( 140 ) end -- Carrier position. - local Carrier=self:GetCoordinate() + local Carrier = self:GetCoordinate() -- Carrier heading. - local hdg=self:GetHeading() + local hdg = self:GetHeading() -- Waypoints array. - local wp={} + local wp = {} - local CurrentSpeed=flight.group:GetVelocityKMH() + local CurrentSpeed = flight.group:GetVelocityKMH() -- Current positon. - wp[#wp+1]=flight.group:GetCoordinate():WaypointAirTurningPoint(nil, CurrentSpeed, {}, "Current position") + wp[#wp + 1] = flight.group:GetCoordinate():WaypointAirTurningPoint( nil, CurrentSpeed, {}, "Current position" ) -- Altitude 800 ft. Looks like this works best. - local alt=UTILS.FeetToMeters(800) + local alt = UTILS.FeetToMeters( 800 ) -- Landing waypoint 5 NM behind carrier at 2000 ft = 610 meters ASL. - wp[#wp+1]=Carrier:Translate(UTILS.NMToMeters(4), hdg-160):SetAltitude(alt):WaypointAirLanding(Speed, self.airbase, nil, "Landing") - --wp[#wp+1]=Carrier:Translate(UTILS.NMToMeters(4), hdg-160):SetAltitude(alt):WaypointAirLandingReFu(Speed, self.airbase, nil, "Landing") + wp[#wp + 1] = Carrier:Translate( UTILS.NMToMeters( 4 ), hdg - 160 ):SetAltitude( alt ):WaypointAirLanding( Speed, self.airbase, nil, "Landing" ) + -- wp[#wp+1]=Carrier:Translate(UTILS.NMToMeters(4), hdg-160):SetAltitude(alt):WaypointAirLandingReFu(Speed, self.airbase, nil, "Landing") - --wp[#wp+1]=self:GetCoordinate():Translate(UTILS.NMToMeters(3), hdg-160):SetAltitude(alt):WaypointAirTurningPoint(nil,Speed, {}, "Before Initial") ---WaypointAirLanding(Speed, self.airbase, nil, "Landing") - --wp[#wp+1]=self:GetCoordinate():WaypointAirLanding(Speed, self.airbase, nil, "Landing") + -- wp[#wp+1]=self:GetCoordinate():Translate(UTILS.NMToMeters(3), hdg-160):SetAltitude(alt):WaypointAirTurningPoint(nil,Speed, {}, "Before Initial") ---WaypointAirLanding(Speed, self.airbase, nil, "Landing") + -- wp[#wp+1]=self:GetCoordinate():WaypointAirLanding(Speed, self.airbase, nil, "Landing") -- Reinit waypoints. - flight.group:WayPointInitialize(wp) + flight.group:WayPointInitialize( wp ) -- Route group. - flight.group:Route(wp, 0) + flight.group:Route( wp, 0 ) end --- Get marshal altitude and two positions of a counter-clockwise race track pattern. @@ -104095,83 +110587,83 @@ end -- @return #number Holding altitude in meters. -- @return Core.Point#COORDINATE First race track coordinate. -- @return Core.Point#COORDINATE Second race track coordinate. -function AIRBOSS:_GetMarshalAltitude(stack, case) +function AIRBOSS:_GetMarshalAltitude( stack, case ) -- Stack <= 0. - if stack<=0 then - return 0,nil,nil + if stack <= 0 then + return 0, nil, nil end -- Recovery case. - case=case or self.case + case = case or self.case -- Carrier position. - local Carrier=self:GetCoordinate() + local Carrier = self:GetCoordinate() -- Altitude of first stack. Depends on recovery case. local angels0 local Dist - local p1=nil --Core.Point#COORDINATE - local p2=nil --Core.Point#COORDINATE + local p1 = nil -- Core.Point#COORDINATE + local p2 = nil -- Core.Point#COORDINATE -- Stack number. - local nstack=stack-1 + local nstack = stack - 1 - if case==1 then + if case == 1 then -- CASE I: Holding at 2000 ft on a circular pattern port of the carrier. Interval +1000 ft for next stack. - angels0=2 + angels0 = 2 -- Get true heading of carrier. - local hdg=self.carrier:GetHeading() + local hdg = self.carrier:GetHeading() -- For CCW pattern: First point astern, second ahead of the carrier. -- First point over carrier. - p1=Carrier + p1 = Carrier -- Second point 1.5 NM ahead. - p2=Carrier:Translate(UTILS.NMToMeters(1.5), hdg) + p2 = Carrier:Translate( UTILS.NMToMeters( 1.5 ), hdg ) -- Tarawa,LHA,LHD Delta patterns. - if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then + if self.carriertype == AIRBOSS.CarrierType.HERMES or self.carriertype == AIRBOSS.CarrierType.TARAWA or self.carriertype == AIRBOSS.CarrierType.AMERICA or self.carriertype == AIRBOSS.CarrierType.JCARLOS or self.carriertype == AIRBOSS.CarrierType.CANBERRA then -- Pattern is directly overhead the carrier. - p1=Carrier:Translate(UTILS.NMToMeters(1.0), hdg+90) - p2=p1:Translate(2.5, hdg) + p1 = Carrier:Translate( UTILS.NMToMeters( 1.0 ), hdg + 90 ) + p2 = p1:Translate( 2.5, hdg ) end else -- CASE II/III: Holding at 6000 ft on a racetrack pattern astern the carrier. - angels0=6 + angels0 = 6 -- Distance: d=n*angels0+15 NM, so first stack is at 15+6=21 NM - Dist=UTILS.NMToMeters(nstack+angels0+15) + Dist = UTILS.NMToMeters( nstack + angels0 + 15 ) -- Get correct radial depending on recovery case including offset. - local radial=self:GetRadial(case, false, true) + local radial = self:GetRadial( case, false, true ) -- For CCW pattern: p1 further astern than p2. -- Length of the race track pattern. - local l=UTILS.NMToMeters(10) + local l = UTILS.NMToMeters( 10 ) -- First point of race track pattern. - p1=Carrier:Translate(Dist+l, radial) + p1 = Carrier:Translate( Dist + l, radial ) -- Second point. - p2=Carrier:Translate(Dist, radial) + p2 = Carrier:Translate( Dist, radial ) end -- Pattern altitude. - local altitude=UTILS.FeetToMeters((nstack+angels0)*1000) + local altitude = UTILS.FeetToMeters( (nstack + angels0) * 1000 ) -- Set altitude of coordinate. - p1:SetAltitude(altitude, true) - p2:SetAltitude(altitude, true) + p1:SetAltitude( altitude, true ) + p2:SetAltitude( altitude, true ) return altitude, p1, p2 end @@ -104180,95 +110672,95 @@ end -- @param #AIRBOSS self -- @param #AIRBOSS.FlightGroup flightgroup Flight data. -- @return #number Charlie (abs) time in seconds. Or nil, if stack<0 or no recovery window will open. -function AIRBOSS:_GetCharlieTime(flightgroup) +function AIRBOSS:_GetCharlieTime( flightgroup ) -- Get current stack of player. - local stack=flightgroup.flag + local stack = flightgroup.flag -- Flight is not in marshal stack. - if stack<=0 then + if stack <= 0 then return nil end -- Current abs time. - local Tnow=timer.getAbsTime() + local Tnow = timer.getAbsTime() -- Time the player has to spend in marshal stack until all lower stacks are emptied. - local Tcharlie=0 + local Tcharlie = 0 - local Trecovery=0 + local Trecovery = 0 if self.recoverywindow then -- Time in seconds until the next recovery starts or 0 if window is already open. - Trecovery=math.max(self.recoverywindow.START-Tnow, 0) + Trecovery = math.max( self.recoverywindow.START - Tnow, 0 ) else -- Set ~7 min if no future recovery window is defined. Otherwise radio call function crashes. - Trecovery=7*60 + Trecovery = 7 * 60 end -- Loop over flights currently in the marshal queue. - for _,_flight in pairs(self.Qmarshal) do - local flight=_flight --#AIRBOSS.FlightGroup + for _, _flight in pairs( self.Qmarshal ) do + local flight = _flight -- #AIRBOSS.FlightGroup -- Stack of marshal flight. - local mstack=flight.flag + local mstack = flight.flag -- Time to get to the marshal stack if not holding already. - local Tarrive=0 + local Tarrive = 0 -- Minimum holding time per stack. - local Tholding=3*60 + local Tholding = 3 * 60 - if stack>0 and mstack>0 and mstack<=stack then + if stack > 0 and mstack > 0 and mstack <= stack then -- Check if flight is already holding or just on its way. - if flight.holding==nil then + if flight.holding == nil then -- Flight is on its way to the marshal stack. -- Coordinate of the holding zone. - local holdingzone=self:_GetZoneHolding(flight.case, 1):GetCoordinate() + local holdingzone = self:_GetZoneHolding( flight.case, 1 ):GetCoordinate() -- Distance to holding zone. - local d0=holdingzone:Get2DDistance(flight.group:GetCoordinate()) + local d0 = holdingzone:Get2DDistance( flight.group:GetCoordinate() ) -- Current velocity. - local v0=flight.group:GetVelocityMPS() + local v0 = flight.group:GetVelocityMPS() -- Time to get to the carrier. - Tarrive=d0/v0 + Tarrive = d0 / v0 - self:T3(self.lid..string.format("Tarrive=%.1f seconds, Clock %s", Tarrive, UTILS.SecondsToClock(Tnow+Tarrive))) + self:T3( self.lid .. string.format( "Tarrive=%.1f seconds, Clock %s", Tarrive, UTILS.SecondsToClock( Tnow + Tarrive ) ) ) else -- Flight is already holding. -- Next in line. - if mstack==1 then + if mstack == 1 then -- Current holding time. flight.time stamp should be when entering holding or last time the stack collapsed. - local tholding=timer.getAbsTime()-flight.time + local tholding = timer.getAbsTime() - flight.time -- Deduce current holding time. Ensure that is >=0. - Tholding=math.max(3*60-tholding, 0) + Tholding = math.max( 3 * 60 - tholding, 0 ) end end -- This is the approx time needed to get to the pattern. If we are already there, it is the time until the recovery window opens or 0 if it is already open. - local Tmin=math.max(Tarrive, Trecovery) + local Tmin = math.max( Tarrive, Trecovery ) -- Charlie time + 2 min holding in stack 1. - Tcharlie=math.max(Tmin, Tcharlie)+Tholding + Tcharlie = math.max( Tmin, Tcharlie ) + Tholding end end -- Convert to abs time. - Tcharlie=Tcharlie+Tnow + Tcharlie = Tcharlie + Tnow -- Debug info. - local text=string.format("Charlie time for flight %s (%s) %s", flightgroup.onboard, flightgroup.groupname, UTILS.SecondsToClock(Tcharlie)) - MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) - self:T(self.lid..text) + local text = string.format( "Charlie time for flight %s (%s) %s", flightgroup.onboard, flightgroup.groupname, UTILS.SecondsToClock( Tcharlie ) ) + MESSAGE:New( text, 10, "DEBUG" ):ToAllIf( self.Debug ) + self:T( self.lid .. text ) return Tcharlie end @@ -104277,51 +110769,51 @@ end -- @param #AIRBOSS self -- @param #AIRBOSS.FlightGroup flight Flight group. -- @param #number stack Marshal stack. This (re-)sets the flag value. -function AIRBOSS:_AddMarshalGroup(flight, stack) +function AIRBOSS:_AddMarshalGroup( flight, stack ) -- Set flag value. This corresponds to the stack number which starts at 1. - flight.flag=stack + flight.flag = stack -- Set recovery case. - flight.case=self.case + flight.case = self.case -- Add to marshal queue. - table.insert(self.Qmarshal, flight) + table.insert( self.Qmarshal, flight ) -- Pressure. - local P=UTILS.hPa2inHg(self:GetCoordinate():GetPressure()) + local P = UTILS.hPa2inHg( self:GetCoordinate():GetPressure() ) -- Stack altitude. - --local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack, flight.case)) - local alt=self:_GetMarshalAltitude(stack, flight.case) + -- local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack, flight.case)) + local alt = self:_GetMarshalAltitude( stack, flight.case ) -- Current BRC. - local brc=self:GetBRC() + local brc = self:GetBRC() -- If the carrier is supposed to turn into the wind, we take the wind coordinate. if self.recoverywindow and self.recoverywindow.WIND then - brc=self:GetBRCintoWind() + brc = self:GetBRCintoWind() end -- Get charlie time estimate. - flight.Tcharlie=self:_GetCharlieTime(flight) + flight.Tcharlie = self:_GetCharlieTime( flight ) -- Convert to clock string. - local Ccharlie=UTILS.SecondsToClock(flight.Tcharlie) + local Ccharlie = UTILS.SecondsToClock( flight.Tcharlie ) -- Combined marshal call. - self:_MarshalCallArrived(flight.onboard, flight.case, brc, alt, Ccharlie, P) + self:_MarshalCallArrived( flight.onboard, flight.case, brc, alt, Ccharlie, P ) -- Hint about TACAN bearing. - if self.TACANon and (not flight.ai) and flight.difficulty==AIRBOSS.Difficulty.EASY then + if self.TACANon and (not flight.ai) and flight.difficulty == AIRBOSS.Difficulty.EASY then -- Get inverse magnetic radial potential offset. - local radial=self:GetRadial(flight.case, true, true, true) - if flight.case==1 then + local radial = self:GetRadial( flight.case, true, true, true ) + if flight.case == 1 then -- For case 1 we want the BRC but above routine return FB. - radial=self:GetBRC() + radial = self:GetBRC() end - local text=string.format("Select TACAN %03d°, channel %d%s (%s)", radial, self.TACANchannel,self.TACANmode, self.TACANmorse) - self:MessageToPlayer(flight, text, nil, "") + local text = string.format( "Select TACAN %03d°, channel %d%s (%s)", radial, self.TACANchannel, self.TACANmode, self.TACANmorse ) + self:MessageToPlayer( flight, text, nil, "" ) end end @@ -104330,87 +110822,87 @@ end -- @param #AIRBOSS self -- @param #AIRBOSS.FlightGroup flight Flight that left the marshal stack. -- @param #boolean nopattern If true, flight does not go to pattern. -function AIRBOSS:_CollapseMarshalStack(flight, nopattern) - self:F2({flight=flight, nopattern=nopattern}) +function AIRBOSS:_CollapseMarshalStack( flight, nopattern ) + self:F2( { flight = flight, nopattern = nopattern } ) -- Recovery case of flight. - local case=flight.case + local case = flight.case -- Stack of flight. - local stack=flight.flag + local stack = flight.flag -- Check that stack > 0. - if stack<=0 then - self:E(self.lid..string.format("ERROR: Flight %s is has stack value %d<0. Cannot collapse stack!", flight.groupname, stack)) + if stack <= 0 then + self:E( self.lid .. string.format( "ERROR: Flight %s is has stack value %d<0. Cannot collapse stack!", flight.groupname, stack ) ) return end -- Memorize time when stack collapsed. Should better depend on case but for now we assume there are no two different stacks Case I or II/III. - self.Tcollapse=timer.getTime() + self.Tcollapse = timer.getTime() -- Decrease flag values of all flight groups in marshal stack. - for _,_flight in pairs(self.Qmarshal) do - local mflight=_flight --#AIRBOSS.PlayerData + for _, _flight in pairs( self.Qmarshal ) do + local mflight = _flight -- #AIRBOSS.PlayerData -- Only collapse stack of which the flight left. CASE II/III stacks are not collapsed. - if (case==1 and mflight.case==1) then --or (case>1 and mflight.case>1) then + if (case == 1 and mflight.case == 1) then -- or (case>1 and mflight.case>1) then -- Get current flag/stack value. - local mstack=mflight.flag + local mstack = mflight.flag -- Only collapse stacks above the new pattern flight. - if mstack>stack then + if mstack > stack then -- TODO: Is this now right as we allow more flights per stack? -- Question is, does the stack collapse if the lower stack is completely empty or do aircraft descent if just one flight leaves. -- For now, assuming that the stack must be completely empty before the next higher AC are allowed to descent. - local newstack=self:_GetFreeStack(mflight.ai, mflight.case, true) + local newstack = self:_GetFreeStack( mflight.ai, mflight.case, true ) -- Free stack has to be below. - if newstack and newstack %d.", mflight.groupname, mflight.case, mstack, newstack)) + self:T( self.lid .. string.format( "Collapse Marshal: Flight %s (case %d) is changing marshal stack %d --> %d.", mflight.groupname, mflight.case, mstack, newstack ) ) if mflight.ai then -- Command AI to decrease stack. Flag is set in the routine. - self:_MarshalAI(mflight, newstack) + self:_MarshalAI( mflight, newstack ) else -- Decrease stack/flag. Human player needs to take care himself. - mflight.flag=newstack + mflight.flag = newstack -- Angels of new stack. - local angels=self:_GetAngels(self:_GetMarshalAltitude(newstack, case)) + local angels = self:_GetAngels( self:_GetMarshalAltitude( newstack, case ) ) -- Inform players. - if mflight.difficulty~=AIRBOSS.Difficulty.HARD then + if mflight.difficulty ~= AIRBOSS.Difficulty.HARD then -- Send message to all non-pros that they can descent. - local text=string.format("descent to stack at Angels %d.", angels) - self:MessageToPlayer(mflight, text, "MARSHAL") + local text = string.format( "descent to stack at Angels %d.", angels ) + self:MessageToPlayer( mflight, text, "MARSHAL" ) end -- Set time stamp. - mflight.time=timer.getAbsTime() + mflight.time = timer.getAbsTime() -- Loop over section members. - for _,_sec in pairs(mflight.section) do - local sec=_sec --#AIRBOSS.PlayerData + for _, _sec in pairs( mflight.section ) do + local sec = _sec -- #AIRBOSS.PlayerData -- Also decrease flag for section members of flight. - sec.flag=newstack + sec.flag = newstack -- Set new time stamp. - sec.time=timer.getAbsTime() + sec.time = timer.getAbsTime() -- Inform section member. - if sec.difficulty~=AIRBOSS.Difficulty.HARD then - local text=string.format("descent to stack at Angels %d.", angels) - self:MessageToPlayer(sec, text, "MARSHAL") + if sec.difficulty ~= AIRBOSS.Difficulty.HARD then + local text = string.format( "descent to stack at Angels %d.", angels ) + self:MessageToPlayer( sec, text, "MARSHAL" ) end end @@ -104422,28 +110914,27 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) end end - if nopattern then -- Debug message. - self:T(self.lid..string.format("Flight %s is leaving stack but not going to pattern.", flight.groupname)) + self:T( self.lid .. string.format( "Flight %s is leaving stack but not going to pattern.", flight.groupname ) ) else -- Debug message. - local Tmarshal=UTILS.SecondsToClock(timer.getAbsTime()-flight.time) - self:T(self.lid..string.format("Flight %s is leaving marshal after %s and going pattern.", flight.groupname, Tmarshal)) + local Tmarshal = UTILS.SecondsToClock( timer.getAbsTime() - flight.time ) + self:T( self.lid .. string.format( "Flight %s is leaving marshal after %s and going pattern.", flight.groupname, Tmarshal ) ) -- Add flight to pattern queue. - self:_AddFlightToPatternQueue(flight) + self:_AddFlightToPatternQueue( flight ) end -- Set flag to -1 (-1 is rather arbitrary but it should not be positive or -100 or -42). - flight.flag=-1 + flight.flag = -1 -- New time stamp for time in pattern. - flight.time=timer.getAbsTime() + flight.time = timer.getAbsTime() end @@ -104453,87 +110944,87 @@ end -- @param #number case Recovery case. Default current (self) case in progress. -- @param #boolean empty Return lowest stack that is completely empty. -- @return #number Lowest free stack available for the given case or nil if all Case I stacks are taken. -function AIRBOSS:_GetFreeStack(ai, case, empty) +function AIRBOSS:_GetFreeStack( ai, case, empty ) -- Recovery case. - case=case or self.case + case = case or self.case - if case==1 then - return self:_GetFreeStack_Old(ai, case, empty) + if case == 1 then + return self:_GetFreeStack_Old( ai, case, empty ) end -- Max number of stacks available. - local nmaxstacks=100 - if case==1 then - nmaxstacks=self.Nmaxmarshal + local nmaxstacks = 100 + if case == 1 then + nmaxstacks = self.Nmaxmarshal end -- Assume up to two (human) flights per stack. All are free. - local stack={} - for i=1,nmaxstacks do - stack[i]=self.NmaxStack -- Number of human flights per stack. + local stack = {} + for i = 1, nmaxstacks do + stack[i] = self.NmaxStack -- Number of human flights per stack. end - local nmax=1 + local nmax = 1 -- Loop over all flights in marshal stack. - for _,_flight in pairs(self.Qmarshal) do - local flight=_flight --#AIRBOSS.FlightGroup + for _, _flight in pairs( self.Qmarshal ) do + local flight = _flight -- #AIRBOSS.FlightGroup -- Check that the case is right. - if flight.case==case then + if flight.case == case then -- Get stack of flight. - local n=flight.flag + local n = flight.flag - if n>nmax then - nmax=n + if n > nmax then + nmax = n end - if n>0 then - if flight.ai or flight.case>1 then - stack[n]=0 -- AI get one stack on their own. Also CASE II/III get one stack each. + if n > 0 then + if flight.ai or flight.case > 1 then + stack[n] = 0 -- AI get one stack on their own. Also CASE II/III get one stack each. else - stack[n]=stack[n]-1 + stack[n] = stack[n] - 1 end else - self:E(string.format("ERROR: Flight %s in marshal stack has stack value <= 0. Stack value is %d.", flight.groupname, n)) + self:E( string.format( "ERROR: Flight %s in marshal stack has stack value <= 0. Stack value is %d.", flight.groupname, n ) ) end end end - local nfree=nil - if stack[nmax]==0 then + local nfree = nil + if stack[nmax] == 0 then -- Max occupied stack is completely full! - if case==1 then - if nmax>=nmaxstacks then + if case == 1 then + if nmax >= nmaxstacks then -- Already all Case I stacks are occupied ==> wait outside 10 NM zone. - nfree=nil + nfree = nil else -- Return next free stack. - nfree=nmax+1 + nfree = nmax + 1 end else -- Case II/III return next stack - nfree=nmax+1 + nfree = nmax + 1 end - elseif stack[nmax]==self.NmaxStack then + elseif stack[nmax] == self.NmaxStack then -- Max occupied stack is completely empty! This should happen only when there is no other flight in the marshal queue. - self:E(self.lid..string.format("ERROR: Max occupied stack is empty. Should not happen! Nmax=%d, stack[nmax]=%d", nmax, stack[nmax])) - nfree=nmax + self:E( self.lid .. string.format( "ERROR: Max occupied stack is empty. Should not happen! Nmax=%d, stack[nmax]=%d", nmax, stack[nmax] ) ) + nfree = nmax else -- Max occupied stack is partly full. - if ai or empty or case>1 then - nfree=nmax+1 + if ai or empty or case > 1 then + nfree = nmax + 1 else - nfree=nmax + nfree = nmax end end - self:I(self.lid..string.format("Returning free stack %s", tostring(nfree))) + self:I( self.lid .. string.format( "Returning free stack %s", tostring( nfree ) ) ) return nfree end @@ -104543,60 +111034,60 @@ end -- @param #number case Recovery case. Default current (self) case in progress. -- @param #boolean empty Return lowest stack that is completely empty. -- @return #number Lowest free stack available for the given case or nil if all Case I stacks are taken. -function AIRBOSS:_GetFreeStack_Old(ai, case, empty) +function AIRBOSS:_GetFreeStack_Old( ai, case, empty ) -- Recovery case. - case=case or self.case + case = case or self.case -- Max number of stacks available. - local nmaxstacks=100 - if case==1 then - nmaxstacks=self.Nmaxmarshal + local nmaxstacks = 100 + if case == 1 then + nmaxstacks = self.Nmaxmarshal end -- Assume up to two (human) flights per stack. All are free. - local stack={} - for i=1,nmaxstacks do - stack[i]=self.NmaxStack -- Number of human flights per stack. + local stack = {} + for i = 1, nmaxstacks do + stack[i] = self.NmaxStack -- Number of human flights per stack. end -- Loop over all flights in marshal stack. - for _,_flight in pairs(self.Qmarshal) do - local flight=_flight --#AIRBOSS.FlightGroup + for _, _flight in pairs( self.Qmarshal ) do + local flight = _flight -- #AIRBOSS.FlightGroup -- Check that the case is right. - if flight.case==case then + if flight.case == case then -- Get stack of flight. - local n=flight.flag + local n = flight.flag - if n>0 then - if flight.ai or flight.case>1 then - stack[n]=0 -- AI get one stack on their own. Also CASE II/III get one stack each. + if n > 0 then + if flight.ai or flight.case > 1 then + stack[n] = 0 -- AI get one stack on their own. Also CASE II/III get one stack each. else - stack[n]=stack[n]-1 + stack[n] = stack[n] - 1 end else - self:E(string.format("ERROR: Flight %s in marshal stack has stack value <= 0. Stack value is %d.", flight.groupname, n)) + self:E( string.format( "ERROR: Flight %s in marshal stack has stack value <= 0. Stack value is %d.", flight.groupname, n ) ) end end end -- Loop over stacks and check which one has a place left. - local nfree=nil - for i=1,nmaxstacks do - self:T2(self.lid..string.format("FF Stack[%d]=%d", i, stack[i])) - if ai or empty or case>1 then + local nfree = nil + for i = 1, nmaxstacks do + self:T2( self.lid .. string.format( "FF Stack[%d]=%d", i, stack[i] ) ) + if ai or empty or case > 1 then -- AI need the whole stack. - if stack[i]==self.NmaxStack then - nfree=i + if stack[i] == self.NmaxStack then + nfree = i return i end else -- Human players only need one free spot. - if stack[i]>0 then - nfree=i + if stack[i] > 0 then + nfree = i return i end end @@ -104612,32 +111103,32 @@ end -- @return #number Number of units in flight including section members. -- @return #number Number of units in flight excluding section members. -- @return #number Number of section members. -function AIRBOSS:_GetFlightUnits(flight, onground) +function AIRBOSS:_GetFlightUnits( flight, onground ) -- Default is only airborne. - local inair=true - if onground==true then - inair=false + local inair = true + if onground == true then + inair = false end --- Count units of a group which are alive and in the air. - local function countunits(_group, inair) - local group=_group --Wrapper.Group#GROUP - local units=group:GetUnits() - local n=0 + local function countunits( _group, inair ) + local group = _group -- Wrapper.Group#GROUP + local units = group:GetUnits() + local n = 0 if units then - for _,_unit in pairs(units) do - local unit=_unit --Wrapper.Unit#UNIT - if unit and unit:IsAlive() then + for _, _unit in pairs( units ) do + local unit = _unit -- Wrapper.Unit#UNIT + if unit and unit:IsAlive() then if inair then -- Only count units in air. if unit:InAir() then - self:T2(self.lid..string.format("Unit %s is in AIR", unit:GetName())) - n=n+1 + self:T2( self.lid .. string.format( "Unit %s is in AIR", unit:GetName() ) ) + n = n + 1 end else -- Count units in air or on the ground. - n=n+1 + n = n + 1 end end end @@ -104645,19 +111136,18 @@ function AIRBOSS:_GetFlightUnits(flight, onground) return n end - -- Count units of the group itself (alive units in air). - local nunits=countunits(flight.group, inair) + local nunits = countunits( flight.group, inair ) -- Count section members. - local nsection=0 - for _,sec in pairs(flight.section) do - local secflight=sec --#AIRBOSS.PlayerData + local nsection = 0 + for _, sec in pairs( flight.section ) do + local secflight = sec -- #AIRBOSS.PlayerData -- Count alive units in air. - nsection=nsection+countunits(secflight.group, inair) + nsection = nsection + countunits( secflight.group, inair ) end - return nunits+nsection, nunits, nsection + return nunits + nsection, nunits, nsection end --- Get number of groups and units in queue, which are alive and airborne. In units we count the section members as well. @@ -104666,14 +111156,14 @@ end -- @param #number case (Optional) Only count flights, which are in a specific recovery case. Note that you can use case=23 for flights that are either in Case II or III. By default all groups/units regardless of case are counted. -- @return #number Total number of flight groups in queue. -- @return #number Total number of aircraft in queue since each flight group can contain multiple aircraft. -function AIRBOSS:_GetQueueInfo(queue, case) +function AIRBOSS:_GetQueueInfo( queue, case ) - local ngroup=0 - local Nunits=0 + local ngroup = 0 + local Nunits = 0 -- Loop over flight groups. - for _,_flight in pairs(queue) do - local flight=_flight --#AIRBOSS.FlightGroup + for _, _flight in pairs( queue ) do + local flight = _flight -- #AIRBOSS.FlightGroup -- Check if a specific case was requested. if case then @@ -104682,17 +111172,17 @@ function AIRBOSS:_GetQueueInfo(queue, case) -- Only count specific case with special 23 = CASE II and III combined. ------------------------------------------------------------------------ - if (flight.case==case) or (case==23 and (flight.case==2 or flight.case==3)) then + if (flight.case == case) or (case == 23 and (flight.case == 2 or flight.case == 3)) then -- Number of total units, units in flight and section members ALIVE and AIRBORNE. - local ntot,nunits,nsection=self:_GetFlightUnits(flight) + local ntot, nunits, nsection = self:_GetFlightUnits( flight ) -- Add up total unit number. - Nunits=Nunits+ntot + Nunits = Nunits + ntot -- Increase group count. - if ntot>0 then - ngroup=ngroup+1 + if ntot > 0 then + ngroup = ngroup + 1 end end @@ -104704,14 +111194,14 @@ function AIRBOSS:_GetQueueInfo(queue, case) --------------------------------------------------------------------------- -- Number of total units, units in flight and section members ALIVE and AIRBORNE. - local ntot,nunits,nsection=self:_GetFlightUnits(flight) + local ntot, nunits, nsection = self:_GetFlightUnits( flight ) -- Add up total unit number. - Nunits=Nunits+ntot + Nunits = Nunits + ntot -- Increase group count. - if ntot>0 then - ngroup=ngroup+1 + if ntot > 0 then + ngroup = ngroup + 1 end end @@ -104725,47 +111215,45 @@ end -- @param #AIRBOSS self -- @param #table queue Queue to print. -- @param #string name Queue name. -function AIRBOSS:_PrintQueue(queue, name) +function AIRBOSS:_PrintQueue( queue, name ) - --local nqueue=#queue - local Nqueue, nqueue=self:_GetQueueInfo(queue) + -- local nqueue=#queue + local Nqueue, nqueue = self:_GetQueueInfo( queue ) - local text=string.format("%s Queue N=%d (#%d), n=%d:", name, Nqueue, #queue, nqueue) - if #queue==0 then - text=text.." empty." + local text = string.format( "%s Queue N=%d (#%d), n=%d:", name, Nqueue, #queue, nqueue ) + if #queue == 0 then + text = text .. " empty." else - for i,_flight in pairs(queue) do - local flight=_flight --#AIRBOSS.FlightGroup + for i, _flight in pairs( queue ) do + local flight = _flight -- #AIRBOSS.FlightGroup - local clock=UTILS.SecondsToClock(timer.getAbsTime()-flight.time) - local case=flight.case - local stack=flight.flag - local fuel=flight.group:GetFuelMin()*100 - local ai=tostring(flight.ai) - local lead=flight.seclead - local Nsec=#flight.section - local actype=self:_GetACNickname(flight.actype) - local onboard=flight.onboard - local holding=tostring(flight.holding) + local clock = UTILS.SecondsToClock( timer.getAbsTime() - flight.time ) + local case = flight.case + local stack = flight.flag + local fuel = flight.group:GetFuelMin() * 100 + local ai = tostring( flight.ai ) + local lead = flight.seclead + local Nsec = #flight.section + local actype = self:_GetACNickname( flight.actype ) + local onboard = flight.onboard + local holding = tostring( flight.holding ) -- Airborne units. - local _, nunits, nsec=self:_GetFlightUnits(flight, false) + local _, nunits, nsec = self:_GetFlightUnits( flight, false ) -- Text. - text=text..string.format("\n[%d] %s*%d (%s): lead=%s (%d/%d), onboard=%s, flag=%d, case=%d, time=%s, fuel=%d, ai=%s, holding=%s", - i, flight.groupname, nunits, actype, lead, nsec, Nsec, onboard, stack, case, clock, fuel, ai, holding) - if stack>0 then - local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack, case)) - text=text..string.format(" stackalt=%d ft", alt) + text = text .. string.format( "\n[%d] %s*%d (%s): lead=%s (%d/%d), onboard=%s, flag=%d, case=%d, time=%s, fuel=%d, ai=%s, holding=%s", i, flight.groupname, nunits, actype, lead, nsec, Nsec, onboard, stack, case, clock, fuel, ai, holding ) + if stack > 0 then + local alt = UTILS.MetersToFeet( self:_GetMarshalAltitude( stack, case ) ) + text = text .. string.format( " stackalt=%d ft", alt ) end - for j,_element in pairs(flight.elements) do - local element=_element --#AIRBOSS.FlightElement - text=text..string.format("\n (%d) %s (%s): ai=%s, ballcall=%s, recovered=%s", - j, element.onboard, element.unitname, tostring(element.ai), tostring(element.ballcall), tostring(element.recovered)) + for j, _element in pairs( flight.elements ) do + local element = _element -- #AIRBOSS.FlightElement + text = text .. string.format( "\n (%d) %s (%s): ai=%s, ballcall=%s, recovered=%s", j, element.onboard, element.unitname, tostring( element.ai ), tostring( element.ballcall ), tostring( element.recovered ) ) end end end - self:T(self.lid..text) + self:T( self.lid .. text ) end ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -104776,159 +111264,158 @@ end -- @param #AIRBOSS self -- @param Wrapper.Group#GROUP group Aircraft group. -- @return #AIRBOSS.FlightGroup Flight group. -function AIRBOSS:_CreateFlightGroup(group) +function AIRBOSS:_CreateFlightGroup( group ) -- Debug info. - self:T(self.lid..string.format("Creating new flight for group %s of aircraft type %s.", group:GetName(), group:GetTypeName())) + self:T( self.lid .. string.format( "Creating new flight for group %s of aircraft type %s.", group:GetName(), group:GetTypeName() ) ) -- New flight. - local flight={} --#AIRBOSS.FlightGroup + local flight = {} -- #AIRBOSS.FlightGroup -- Check if not already in flights - if not self:_InQueue(self.flights, group) then + if not self:_InQueue( self.flights, group ) then -- Flight group name - local groupname=group:GetName() - local human, playername=self:_IsHuman(group) + local groupname = group:GetName() + local human, playername = self:_IsHuman( group ) -- Queue table item. - flight.group=group - flight.groupname=group:GetName() - flight.nunits=#group:GetUnits() - flight.time=timer.getAbsTime() - flight.dist0=group:GetCoordinate():Get2DDistance(self:GetCoordinate()) - flight.flag=-100 - flight.ai=not human - flight.actype=group:GetTypeName() - flight.onboardnumbers=self:_GetOnboardNumbers(group) - flight.seclead=flight.group:GetUnit(1):GetName() -- Sec lead is first unitname of group but player name for players. - flight.section={} - flight.ballcall=false - flight.refueling=false - flight.holding=nil - flight.name=flight.group:GetUnit(1):GetName() --Will be overwritten in _Newplayer with player name if human player in the group. + flight.group = group + flight.groupname = group:GetName() + flight.nunits = #group:GetUnits() + flight.time = timer.getAbsTime() + flight.dist0 = group:GetCoordinate():Get2DDistance( self:GetCoordinate() ) + flight.flag = -100 + flight.ai = not human + flight.actype = group:GetTypeName() + flight.onboardnumbers = self:_GetOnboardNumbers( group ) + flight.seclead = flight.group:GetUnit( 1 ):GetName() -- Sec lead is first unitname of group but player name for players. + flight.section = {} + flight.ballcall = false + flight.refueling = false + flight.holding = nil + flight.name = flight.group:GetUnit( 1 ):GetName() -- Will be overwritten in _Newplayer with player name if human player in the group. -- Note, this should be re-set elsewhere! - flight.case=self.case + flight.case = self.case -- Flight elements. - local text=string.format("Flight elements of group %s:", flight.groupname) - flight.elements={} - local units=group:GetUnits() - for i,_unit in pairs(units) do - local unit=_unit --Wrapper.Unit#UNIT - local element={} --#AIRBOSS.FlightElement - element.unit=unit - element.unitname=unit:GetName() - element.onboard=flight.onboardnumbers[element.unitname] - element.ballcall=false - element.ai=not self:_IsHumanUnit(unit) - element.recovered=nil - text=text..string.format("\n[%d] %s onboard #%s, AI=%s", i, element.unitname, tostring(element.onboard), tostring(element.ai)) - table.insert(flight.elements, element) + local text = string.format( "Flight elements of group %s:", flight.groupname ) + flight.elements = {} + local units = group:GetUnits() + for i, _unit in pairs( units ) do + local unit = _unit -- Wrapper.Unit#UNIT + local element = {} -- #AIRBOSS.FlightElement + element.unit = unit + element.unitname = unit:GetName() + element.onboard = flight.onboardnumbers[element.unitname] + element.ballcall = false + element.ai = not self:_IsHumanUnit( unit ) + element.recovered = nil + text = text .. string.format( "\n[%d] %s onboard #%s, AI=%s", i, element.unitname, tostring( element.onboard ), tostring( element.ai ) ) + table.insert( flight.elements, element ) end - self:T(self.lid..text) + self:T( self.lid .. text ) -- Onboard if flight.ai then - local onboard=flight.onboardnumbers[flight.seclead] - flight.onboard=onboard + local onboard = flight.onboardnumbers[flight.seclead] + flight.onboard = onboard else - flight.onboard=self:_GetOnboardNumberPlayer(group) + flight.onboard = self:_GetOnboardNumberPlayer( group ) end -- Add to known flights. - table.insert(self.flights, flight) + table.insert( self.flights, flight ) else - self:E(self.lid..string.format("ERROR: Flight group %s already exists in self.flights!", group:GetName())) + self:E( self.lid .. string.format( "ERROR: Flight group %s already exists in self.flights!", group:GetName() ) ) return nil end return flight end - --- Initialize player data after birth event of player unit. -- @param #AIRBOSS self -- @param #string unitname Name of the player unit. -- @return #AIRBOSS.PlayerData Player data. -function AIRBOSS:_NewPlayer(unitname) +function AIRBOSS:_NewPlayer( unitname ) -- Get player unit and name. - local playerunit, playername=self:_GetPlayerUnitAndName(unitname) + local playerunit, playername = self:_GetPlayerUnitAndName( unitname ) if playerunit and playername then -- Get group. - local group=playerunit:GetGroup() + local group = playerunit:GetGroup() -- Player data. - local playerData --#AIRBOSS.PlayerData + local playerData -- #AIRBOSS.PlayerData -- Create a flight group for the player. - playerData=self:_CreateFlightGroup(group) + playerData = self:_CreateFlightGroup( group ) -- Nil check. if playerData then -- Player unit, client and callsign. - playerData.unit = playerunit + playerData.unit = playerunit playerData.unitname = unitname - playerData.name = playername + playerData.name = playername playerData.callsign = playerData.unit:GetCallsign() - playerData.client = CLIENT:FindByName(unitname, nil, true) - playerData.seclead = playername + playerData.client = CLIENT:FindByName( unitname, nil, true ) + playerData.seclead = playername -- Number of passes done by player in this slot. - playerData.passes=0 --playerData.passes or 0 + playerData.passes = 0 -- playerData.passes or 0 -- Messages for player. - playerData.messages={} + playerData.messages = {} -- Debriefing tables. - playerData.lastdebrief=playerData.lastdebrief or {} + playerData.lastdebrief = playerData.lastdebrief or {} -- Attitude monitor. - playerData.attitudemonitor=false + playerData.attitudemonitor = false -- Trap sheet save. - if playerData.trapon==nil then - playerData.trapon=self.trapsheet + if playerData.trapon == nil then + playerData.trapon = self.trapsheet end -- Set difficulty level. - playerData.difficulty=playerData.difficulty or self.defaultskill + playerData.difficulty = playerData.difficulty or self.defaultskill -- Subtitles of player. - if playerData.subtitles==nil then - playerData.subtitles=true + if playerData.subtitles == nil then + playerData.subtitles = true end -- Show step hints. - if playerData.showhints==nil then - if playerData.difficulty==AIRBOSS.Difficulty.HARD then - playerData.showhints=false + if playerData.showhints == nil then + if playerData.difficulty == AIRBOSS.Difficulty.HARD then + playerData.showhints = false else - playerData.showhints=true + playerData.showhints = true end end -- Points rewarded. - playerData.points={} + playerData.points = {} -- Init stuff for this round. - playerData=self:_InitPlayer(playerData) + playerData = self:_InitPlayer( playerData ) -- Init player data. - self.players[playername]=playerData + self.players[playername] = playerData -- Init player grades table if necessary. - self.playerscores[playername]=self.playerscores[playername] or {} + self.playerscores[playername] = self.playerscores[playername] or {} -- Welcome player message. if self.welcome then - self:MessageToPlayer(playerData, string.format("Welcome, %s %s!", playerData.difficulty, playerData.name), string.format("AIRBOSS %s", self.alias), "", 5) + self:MessageToPlayer( playerData, string.format( "Welcome, %s %s!", playerData.difficulty, playerData.name ), string.format( "AIRBOSS %s", self.alias ), "", 5 ) end end @@ -104945,70 +111432,71 @@ end -- @param #AIRBOSS.PlayerData playerData Player data. -- @param #string step (Optional) New player step. Default UNDEFINED. -- @return #AIRBOSS.PlayerData Initialized player data. -function AIRBOSS:_InitPlayer(playerData, step) - self:T(self.lid..string.format("Initializing player data for %s callsign %s.", playerData.name, playerData.callsign)) +function AIRBOSS:_InitPlayer( playerData, step ) + self:T( self.lid .. string.format( "Initializing player data for %s callsign %s.", playerData.name, playerData.callsign ) ) - playerData.step=step or AIRBOSS.PatternStep.UNDEFINED - playerData.groove={} - playerData.debrief={} - playerData.trapsheet={} - playerData.warning=nil - playerData.holding=nil - playerData.refueling=false - playerData.valid=false - playerData.lig=false - playerData.wop=false - playerData.waveoff=false - playerData.wofd=false - playerData.owo=false - playerData.boltered=false - playerData.landed=false - playerData.Tlso=timer.getTime() - playerData.Tgroove=nil - playerData.TIG0=nil - playerData.wire=nil - playerData.flag=-100 - playerData.debriefschedulerID=nil + playerData.step = step or AIRBOSS.PatternStep.UNDEFINED + playerData.groove = {} + playerData.debrief = {} + playerData.trapsheet = {} + playerData.warning = nil + playerData.holding = nil + playerData.refueling = false + playerData.valid = false + playerData.lig = false + playerData.wop = false + playerData.waveoff = false + playerData.wofd = false + playerData.owo = false + playerData.boltered = false + playerData.hover = false + playerData.stable = false + playerData.landed = false + playerData.Tlso = timer.getTime() + playerData.Tgroove = nil + playerData.TIG0 = nil + playerData.wire = nil + playerData.flag = -100 + playerData.debriefschedulerID = nil -- Set us up on final if group name contains "Groove". But only for the first pass. - if playerData.group:GetName():match("Groove") and playerData.passes==0 then - self:MessageToPlayer(playerData, "Group name contains \"Groove\". Happy groove testing.") - playerData.attitudemonitor=true - playerData.step=AIRBOSS.PatternStep.FINAL - self:_AddFlightToPatternQueue(playerData) - self.dTstatus=0.1 + if playerData.group:GetName():match( "Groove" ) and playerData.passes == 0 then + self:MessageToPlayer( playerData, "Group name contains \"Groove\". Happy groove testing." ) + playerData.attitudemonitor = true + playerData.step = AIRBOSS.PatternStep.FINAL + self:_AddFlightToPatternQueue( playerData ) + self.dTstatus = 0.1 end return playerData end - --- Get flight from group in a queue. -- @param #AIRBOSS 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 #AIRBOSS.FlightGroup Flight group or nil. -- @return #number Queue index or nil. -function AIRBOSS:_GetFlightFromGroupInQueue(group, queue) +function AIRBOSS:_GetFlightFromGroupInQueue( group, queue ) if group then -- Group name - local name=group:GetName() + local name = group:GetName() -- Loop over all flight groups in queue - for i,_flight in pairs(queue) do - local flight=_flight --#AIRBOSS.FlightGroup + for i, _flight in pairs( queue ) do + local flight = _flight -- #AIRBOSS.FlightGroup - if flight.groupname==name then + 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)) + 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!")) + self:T2( self.lid .. string.format( "WARNING: Flight group could not be found in queue. Group is nil!" ) ) return nil, nil end @@ -105018,30 +111506,30 @@ end -- @return #AIRBOSS.FlightElement Element of the flight or nil. -- @return #number Element index or nil. -- @return #AIRBOSS.FlightGroup The Flight group or nil -function AIRBOSS:_GetFlightElement(unitname) +function AIRBOSS:_GetFlightElement( unitname ) -- Get the unit. - local unit=UNIT:FindByName(unitname) + local unit = UNIT:FindByName( unitname ) -- Check if unit exists. if unit then -- Get flight element from all flights. - local flight=self:_GetFlightFromGroupInQueue(unit:GetGroup(), self.flights) + local flight = self:_GetFlightFromGroupInQueue( unit:GetGroup(), self.flights ) -- 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 --#AIRBOSS.FlightElement + for i, _element in pairs( flight.elements ) do + local element = _element -- #AIRBOSS.FlightElement - if element.unit:GetName()==unitname then + 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)) + self:T2( self.lid .. string.format( "WARNING: Flight element %s could not be found in flight group.", unitname, flight.groupname ) ) end end @@ -105052,16 +111540,16 @@ end -- @param #AIRBOSS self -- @param #string unitname Name of the unit. -- @return #boolean If true, element could be removed or nil otherwise. -function AIRBOSS:_RemoveFlightElement(unitname) +function AIRBOSS:_RemoveFlightElement( unitname ) -- Get table index. - local element,idx, flight=self:_GetFlightElement(unitname) + local element, idx, flight = self:_GetFlightElement( unitname ) if idx then - table.remove(flight.elements, idx) + table.remove( flight.elements, idx ) return true else - self:T("WARNING: Flight element could not be removed from flight group. Index=nil!") + self:T( "WARNING: Flight element could not be removed from flight group. Index=nil!" ) return nil end end @@ -105071,11 +111559,11 @@ end -- @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 AIRBOSS:_InQueue(queue, group) - local name=group:GetName() - for _,_flight in pairs(queue) do - local flight=_flight --#AIRBOSS.FlightGroup - if name==flight.groupname then +function AIRBOSS:_InQueue( queue, group ) + local name = group:GetName() + for _, _flight in pairs( queue ) do + local flight = _flight -- #AIRBOSS.FlightGroup + if name == flight.groupname then return true end end @@ -105089,29 +111577,29 @@ end function AIRBOSS:_RemoveDeadFlightGroups() -- Remove dead flights from all flights table. - for i=#self.flight,1,-1 do - local flight=self.flights[i] --#AIRBOSS.FlightGroup + for i = #self.flight, 1, -1 do + local flight = self.flights[i] -- #AIRBOSS.FlightGroup if not flight.group:IsAlive() then - self:T(string.format("Removing dead flight group %s from ALL flights table.", flight.groupname)) - table.remove(self.flights, i) + self:T( string.format( "Removing dead flight group %s from ALL flights table.", flight.groupname ) ) + table.remove( self.flights, i ) end end -- Remove dead flights from Marhal queue table. - for i=#self.Qmarshal,1,-1 do - local flight=self.Qmarshal[i] --#AIRBOSS.FlightGroup + for i = #self.Qmarshal, 1, -1 do + local flight = self.Qmarshal[i] -- #AIRBOSS.FlightGroup if not flight.group:IsAlive() then - self:T(string.format("Removing dead flight group %s from Marshal Queue table.", flight.groupname)) - table.remove(self.Qmarshal, i) + self:T( string.format( "Removing dead flight group %s from Marshal Queue table.", flight.groupname ) ) + table.remove( self.Qmarshal, i ) end end -- Remove dead flights from Pattern queue table. - for i=#self.Qpattern,1,-1 do - local flight=self.Qpattern[i] --#AIRBOSS.FlightGroup + for i = #self.Qpattern, 1, -1 do + local flight = self.Qpattern[i] -- #AIRBOSS.FlightGroup if not flight.group:IsAlive() then - self:T(string.format("Removing dead flight group %s from Pattern Queue table.", flight.groupname)) - table.remove(self.Qpattern, i) + self:T( string.format( "Removing dead flight group %s from Pattern Queue table.", flight.groupname ) ) + table.remove( self.Qpattern, i ) end end @@ -105121,14 +111609,14 @@ end -- @param #AIRBOSS self -- @param #AIRBOSS.FlightGroup flight Flight group to check. -- @return #AIRBOSS.FlightGroup Flight group of the leader or flight itself if no other leader. -function AIRBOSS:_GetLeadFlight(flight) +function AIRBOSS:_GetLeadFlight( flight ) -- Init. - local lead=flight + local lead = flight -- Only human players can be section leads of other players. - if flight.name~=flight.seclead then - lead=self.players[flight.seclead] + if flight.name ~= flight.seclead then + lead = self.players[flight.seclead] end return lead @@ -105139,31 +111627,31 @@ end -- @param #AIRBOSS self -- @param #AIRBOSS.FlightGroup flight Flight group to check. -- @return #boolean If true, all elements landed. -function AIRBOSS:_CheckSectionRecovered(flight) +function AIRBOSS:_CheckSectionRecovered( flight ) -- Nil check. - if flight==nil then + if flight == nil then return true end -- Get the lead flight first, so that we can also check all section members. - local lead=self:_GetLeadFlight(flight) + local lead = self:_GetLeadFlight( flight ) -- Check all elements of the lead flight group. - for _,_element in pairs(lead.elements) do - local element=_element --#AIRBOSS.FlightElement + for _, _element in pairs( lead.elements ) do + local element = _element -- #AIRBOSS.FlightElement if not element.recovered then return false end end -- Now check all section members, if any. - for _,_section in pairs(lead.section) do - local sectionmember=_section --#AIRBOSS.FlightGroup + for _, _section in pairs( lead.section ) do + local sectionmember = _section -- #AIRBOSS.FlightGroup -- Check all elements of the secmember flight group. - for _,_element in pairs(sectionmember.elements) do - local element=_element --#AIRBOSS.FlightElement + for _, _element in pairs( sectionmember.elements ) do + local element = _element -- #AIRBOSS.FlightElement if not element.recovered then return false end @@ -105171,17 +111659,17 @@ function AIRBOSS:_CheckSectionRecovered(flight) end -- Remove lead flight from pattern queue. It is this flight who is added to the queue. - self:_RemoveFlightFromQueue(self.Qpattern, lead) + self:_RemoveFlightFromQueue( self.Qpattern, lead ) -- Just for now, check if it is in other queues as well. - if self:_InQueue(self.Qmarshal, lead.group) then - self:E(self.lid..string.format("ERROR: lead flight group %s should not be in marshal queue", lead.groupname)) - self:_RemoveFlightFromMarshalQueue(lead, true) + if self:_InQueue( self.Qmarshal, lead.group ) then + self:E( self.lid .. string.format( "ERROR: lead flight group %s should not be in marshal queue", lead.groupname ) ) + self:_RemoveFlightFromMarshalQueue( lead, true ) end -- Just for now, check if it is in other queues as well. - if self:_InQueue(self.Qwaiting, lead.group) then - self:E(self.lid..string.format("ERROR: lead flight group %s should not be in pattern queue", lead.groupname)) - self:_RemoveFlightFromQueue(self.Qwaiting, lead) + if self:_InQueue( self.Qwaiting, lead.group ) then + self:E( self.lid .. string.format( "ERROR: lead flight group %s should not be in pattern queue", lead.groupname ) ) + self:_RemoveFlightFromQueue( self.Qwaiting, lead ) end return true @@ -105190,29 +111678,29 @@ end --- Add flight to pattern queue and set recoverd to false for all elements of the flight and its section members. -- @param #AIRBOSS self -- @param #AIRBOSS.FlightGroup Flight group of element. -function AIRBOSS:_AddFlightToPatternQueue(flight) +function AIRBOSS:_AddFlightToPatternQueue( flight ) -- Add flight to table. - table.insert(self.Qpattern, flight) + table.insert( self.Qpattern, flight ) -- Set flag to -1 (-1 is rather arbitrary but it should not be positive or -100 or -42). - flight.flag=-1 + flight.flag = -1 -- New time stamp for time in pattern. - flight.time=timer.getAbsTime() + flight.time = timer.getAbsTime() -- Init recovered switch. - flight.recovered=false - for _,elem in pairs(flight.elements) do - elem.recoverd=false + flight.recovered = false + for _, elem in pairs( flight.elements ) do + elem.recoverd = false end -- Set recovered for all section members. - for _,sec in pairs(flight.section) do + for _, sec in pairs( flight.section ) do -- Set flag and timestamp for section members - sec.flag=-1 - sec.time=timer.getAbsTime() - for _,elem in pairs(sec.elements) do - elem.recoverd=false + sec.flag = -1 + sec.time = timer.getAbsTime() + for _, elem in pairs( sec.elements ) do + elem.recoverd = false end end end @@ -105221,14 +111709,14 @@ end -- @param #AIRBOSS self -- @param Wrapper.Unit#UNIT unit The aircraft unit that was recovered. -- @return #AIRBOSS.FlightGroup Flight group of element. -function AIRBOSS:_RecoveredElement(unit) +function AIRBOSS:_RecoveredElement( unit ) -- Get element of flight. - local element, idx, flight=self:_GetFlightElement(unit:GetName()) --#AIRBOSS.FlightElement + local element, idx, flight = self:_GetFlightElement( unit:GetName() ) -- #AIRBOSS.FlightElement -- Nil check. Could be if a helo landed or something else we dont know! if element then - element.recovered=true + element.recovered = true end return flight @@ -105240,44 +111728,44 @@ end -- @param #boolean nopattern If true, flight is NOT going to landing pattern. -- @return #boolean True, flight was removed or false otherwise. -- @return #number Table index of the flight in the Marshal queue. -function AIRBOSS:_RemoveFlightFromMarshalQueue(flight, nopattern) +function AIRBOSS:_RemoveFlightFromMarshalQueue( flight, nopattern ) -- Remove flight from marshal queue if it is in. - local removed, idx=self:_RemoveFlightFromQueue(self.Qmarshal, flight) + local removed, idx = self:_RemoveFlightFromQueue( self.Qmarshal, flight ) -- Collapse marshal stack if flight was removed. if removed then -- Flight is not holding any more. - flight.holding=nil + flight.holding = nil -- Collapse marshal stack if flight was removed. - self:_CollapseMarshalStack(flight, nopattern) + self:_CollapseMarshalStack( flight, nopattern ) -- Stacks are only limited for Case I. - if flight.case==1 and #self.Qwaiting>0 then + if flight.case == 1 and #self.Qwaiting > 0 then -- Next flight in line waiting. - local nextflight=self.Qwaiting[1] --#AIRBOSS.FlightGroup + local nextflight = self.Qwaiting[1] -- #AIRBOSS.FlightGroup -- Get free stack. - local freestack=self:_GetFreeStack(nextflight.ai) + local freestack = self:_GetFreeStack( nextflight.ai ) -- Send next flight to marshal stack. if nextflight.ai then -- Send AI to Marshal Stack. - self:_MarshalAI(nextflight, freestack) + self:_MarshalAI( nextflight, freestack ) else -- Send player to Marshal stack. - self:_MarshalPlayer(nextflight, freestack) + self:_MarshalPlayer( nextflight, freestack ) end -- Remove flight from waiting queue. - self:_RemoveFlightFromQueue(self.Qwaiting, nextflight) + self:_RemoveFlightFromQueue( self.Qwaiting, nextflight ) end end @@ -105291,16 +111779,16 @@ end -- @param #AIRBOSS.FlightGroup flight Flight group that will be removed from queue. -- @return #boolean True, flight was in Queue and removed. False otherwise. -- @return #number Table index of removed queue element or nil. -function AIRBOSS:_RemoveFlightFromQueue(queue, flight) +function AIRBOSS:_RemoveFlightFromQueue( queue, flight ) -- Loop over all flights in group. - for i,_flight in pairs(queue) do - local qflight=_flight --#AIRBOSS.FlightGroup + for i, _flight in pairs( queue ) do + local qflight = _flight -- #AIRBOSS.FlightGroup -- Check for name. - if qflight.groupname==flight.groupname then - self:T(self.lid..string.format("Removing flight group %s from queue.", flight.groupname)) - table.remove(queue, i) + if qflight.groupname == flight.groupname then + self:T( self.lid .. string.format( "Removing flight group %s from queue.", flight.groupname ) ) + table.remove( queue, i ) return true, i end end @@ -105311,41 +111799,41 @@ end --- Remove a unit and its element from a flight group (e.g. when landed) and update all queues if the whole flight group is gone. -- @param #AIRBOSS self -- @param Wrapper.Unit#UNIT unit The unit to be removed. -function AIRBOSS:_RemoveUnitFromFlight(unit) +function AIRBOSS:_RemoveUnitFromFlight( unit ) -- Check if unit exists. - if unit and unit:IsInstanceOf("UNIT") then + if unit and unit:IsInstanceOf( "UNIT" ) then -- Get group. - local group=unit:GetGroup() + local group = unit:GetGroup() -- Check if group exists. if group then -- Get flight. - local flight=self:_GetFlightFromGroupInQueue(group, self.flights) + local flight = self:_GetFlightFromGroupInQueue( group, self.flights ) -- Check if flight exists. if flight then -- Remove element from flight group. - local removed=self:_RemoveFlightElement(unit:GetName()) + local removed = self:_RemoveFlightElement( unit:GetName() ) if removed then -- Get number of units (excluding section members). For AI only those that are still in air as we assume once they landed, they are out of the game. - local _,nunits=self:_GetFlightUnits(flight, not flight.ai) + local _, nunits = self:_GetFlightUnits( flight, not flight.ai ) -- Number of flight elements still left. - local nelements=#flight.elements + local nelements = #flight.elements -- Debug info. - self:T(self.lid..string.format("Removed unit %s: nunits=%d, nelements=%d", unit:GetName(), nunits, nelements)) + self:T( self.lid .. string.format( "Removed unit %s: nunits=%d, nelements=%d", unit:GetName(), nunits, nelements ) ) -- Check if no units are left. - if nunits==0 or nelements==0 then + if nunits == 0 or nelements == 0 then -- Remove flight from all queues. - self:_RemoveFlight(flight) + self:_RemoveFlight( flight ) end end @@ -105358,18 +111846,18 @@ end --- Remove a flight, which is a member of a section, from this section. -- @param #AIRBOSS self -- @param #AIRBOSS.FlightGroup flight The flight to be removed from the section -function AIRBOSS:_RemoveFlightFromSection(flight) +function AIRBOSS:_RemoveFlightFromSection( flight ) -- First check if player is not the lead. - if flight.name~=flight.seclead then + if flight.name ~= flight.seclead then -- Remove this flight group from the section of the leader. - local lead=self.players[flight.seclead] --#AIRBOSS.FlightGroup + local lead = self.players[flight.seclead] -- #AIRBOSS.FlightGroup if lead then - for i,sec in pairs(lead.section) do - local sectionmember=sec --#AIRBOSS.FlightGroup - if sectionmember.name==flight.name then - table.remove(lead.section, i) + for i, sec in pairs( lead.section ) do + local sectionmember = sec -- #AIRBOSS.FlightGroup + if sectionmember.name == flight.name then + table.remove( lead.section, i ) break end end @@ -105383,37 +111871,37 @@ end -- If removed flight is the section lead, we try to find a new leader. -- @param #AIRBOSS self -- @param #AIRBOSS.FlightGroup flight The flight to be removed. -function AIRBOSS:_UpdateFlightSection(flight) +function AIRBOSS:_UpdateFlightSection( flight ) -- Check if this player is the leader of a section. - if flight.seclead==flight.name then + if flight.seclead == flight.name then -------------------- -- Section Leader -- -------------------- -- This player is the leader ==> We need a new one. - if #flight.section>=1 then + if #flight.section >= 1 then -- New leader. - local newlead=flight.section[1] --#AIRBOSS.FlightGroup - newlead.seclead=newlead.name + local newlead = flight.section[1] -- #AIRBOSS.FlightGroup + newlead.seclead = newlead.name -- Adjust new section members. - for i=2,#flight.section do - local member=flight.section[i] --#AIRBOSS.FlightGroup + for i = 2, #flight.section do + local member = flight.section[i] -- #AIRBOSS.FlightGroup -- Add remaining members new leaders table. - table.insert(newlead.section, member) + table.insert( newlead.section, member ) -- Set new section lead of member. - member.seclead=newlead.name + member.seclead = newlead.name end end -- Flight section empty - flight.section={} + flight.section = {} else @@ -105422,7 +111910,7 @@ function AIRBOSS:_UpdateFlightSection(flight) -------------------- -- Remove flight from its leaders section. - self:_RemoveFlightFromSection(flight) + self:_RemoveFlightFromSection( flight ) end @@ -105433,28 +111921,28 @@ end -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData flight The flight to be removed. -- @param #boolean completely If true, also remove human flight from all flights table. -function AIRBOSS:_RemoveFlight(flight, completely) - self:F(self.lid.. string.format("Removing flight %s, ai=%s completely=%s.", tostring(flight.groupname), tostring(flight.ai), tostring(completely))) +function AIRBOSS:_RemoveFlight( flight, completely ) + self:F( self.lid .. string.format( "Removing flight %s, ai=%s completely=%s.", tostring( flight.groupname ), tostring( flight.ai ), tostring( completely ) ) ) -- Remove flight from all queues. - self:_RemoveFlightFromMarshalQueue(flight, true) - self:_RemoveFlightFromQueue(self.Qpattern, flight) - self:_RemoveFlightFromQueue(self.Qwaiting, flight) - self:_RemoveFlightFromQueue(self.Qspinning, flight) + self:_RemoveFlightFromMarshalQueue( flight, true ) + self:_RemoveFlightFromQueue( self.Qpattern, flight ) + self:_RemoveFlightFromQueue( self.Qwaiting, flight ) + self:_RemoveFlightFromQueue( self.Qspinning, flight ) -- Check if player or AI if flight.ai then -- Remove AI flight completely. Pure AI flights have no sections and cannot be members. - self:_RemoveFlightFromQueue(self.flights, flight) + self:_RemoveFlightFromQueue( self.flights, flight ) else -- Remove all grades until a final grade is reached. - local grades=self.playerscores[flight.name] - if grades and #grades>0 then - while #grades>0 and grades[#grades].finalscore==nil do - table.remove(grades, #grades) + local grades = self.playerscores[flight.name] + if grades and #grades > 0 then + while #grades > 0 and grades[#grades].finalscore == nil do + table.remove( grades, #grades ) end end @@ -105462,36 +111950,36 @@ function AIRBOSS:_RemoveFlight(flight, completely) if completely then -- Update flight section. Remove flight from section or find new section leader if flight was the lead. - self:_UpdateFlightSection(flight) + self:_UpdateFlightSection( flight ) -- Remove completely. - self:_RemoveFlightFromQueue(self.flights, flight) + self:_RemoveFlightFromQueue( self.flights, flight ) -- Remove player from players table. - local playerdata=self.players[flight.name] + local playerdata = self.players[flight.name] if playerdata then - self:I(self.lid..string.format("Removing player %s completely.", flight.name)) - self.players[flight.name]=nil + self:I( self.lid .. string.format( "Removing player %s completely.", flight.name ) ) + self.players[flight.name] = nil end -- Remove flight. - flight=nil + flight = nil else -- Set player step to undefined. - self:_SetPlayerStep(flight, AIRBOSS.PatternStep.UNDEFINED) + self:_SetPlayerStep( flight, AIRBOSS.PatternStep.UNDEFINED ) -- Also set this for the section members as they are in the same boat. - for _,sectionmember in pairs(flight.section) do - self:_SetPlayerStep(sectionmember, AIRBOSS.PatternStep.UNDEFINED) + for _, sectionmember in pairs( flight.section ) do + self:_SetPlayerStep( sectionmember, AIRBOSS.PatternStep.UNDEFINED ) -- Also remove section member in case they are in the spinning queue. - self:_RemoveFlightFromQueue(self.Qspinning, sectionmember) + self:_RemoveFlightFromQueue( self.Qspinning, sectionmember ) end -- What if flight is member of a section. His status is now undefined. Should he be removed from the section? -- I think yes, if he pulls the trigger. - self:_RemoveFlightFromSection(flight) + self:_RemoveFlightFromSection( flight ) end end @@ -105507,212 +111995,211 @@ end function AIRBOSS:_CheckPlayerStatus() -- Loop over all players. - for _playerName,_playerData in pairs(self.players) do - local playerData=_playerData --#AIRBOSS.PlayerData + for _playerName, _playerData in pairs( self.players ) do + local playerData = _playerData -- #AIRBOSS.PlayerData if playerData then -- Player unit. - local unit=playerData.unit + local unit = playerData.unit -- Check if unit is alive. if unit and unit:IsAlive() then -- Check if player is in carrier controlled area (zone with R=50 NM around the carrier). -- TODO: This might cause problems if the CCA is set to be very small! - if unit:IsInZone(self.zoneCCA) then + if unit:IsInZone( self.zoneCCA ) then -- Display aircraft attitude and other parameters as message text. if playerData.attitudemonitor then - self:_AttitudeMonitor(playerData) + self:_AttitudeMonitor( playerData ) end -- Check distance to other flights. - self:_CheckPlayerPatternDistance(playerData) + self:_CheckPlayerPatternDistance( playerData ) -- Foul deck check. - self:_CheckFoulDeck(playerData) + self:_CheckFoulDeck( playerData ) -- Check current step. - if playerData.step==AIRBOSS.PatternStep.UNDEFINED then + if playerData.step == AIRBOSS.PatternStep.UNDEFINED then -- Status undefined. - --local time=timer.getAbsTime() - --local clock=UTILS.SecondsToClock(time) - --self:T3(string.format("Player status undefined. Waiting for next step. Time %s", clock)) + -- local time=timer.getAbsTime() + -- local clock=UTILS.SecondsToClock(time) + -- self:T3(string.format("Player status undefined. Waiting for next step. Time %s", clock)) - elseif playerData.step==AIRBOSS.PatternStep.REFUELING then + elseif playerData.step == AIRBOSS.PatternStep.REFUELING then -- Nothing to do here at the moment. - elseif playerData.step==AIRBOSS.PatternStep.SPINNING then + elseif playerData.step == AIRBOSS.PatternStep.SPINNING then -- Player is spinning. - self:_Spinning(playerData) + self:_Spinning( playerData ) - elseif playerData.step==AIRBOSS.PatternStep.HOLDING then + elseif playerData.step == AIRBOSS.PatternStep.HOLDING then -- CASE I/II/III: In holding pattern. - self:_Holding(playerData) + self:_Holding( playerData ) - elseif playerData.step==AIRBOSS.PatternStep.WAITING then + elseif playerData.step == AIRBOSS.PatternStep.WAITING then -- CASE I: Waiting outside 10 NM zone for next free Marshal stack. - self:_Waiting(playerData) + self:_Waiting( playerData ) - elseif playerData.step==AIRBOSS.PatternStep.COMMENCING then + elseif playerData.step == AIRBOSS.PatternStep.COMMENCING then -- CASE I/II/III: New approach. - self:_Commencing(playerData, true) + self:_Commencing( playerData, true ) - elseif playerData.step==AIRBOSS.PatternStep.BOLTER then + elseif playerData.step == AIRBOSS.PatternStep.BOLTER then -- CASE I/II/III: Bolter pattern. - self:_BolterPattern(playerData) + self:_BolterPattern( playerData ) - elseif playerData.step==AIRBOSS.PatternStep.PLATFORM then + elseif playerData.step == AIRBOSS.PatternStep.PLATFORM then -- CASE II/III: Player has reached 5k "Platform". - self:_Platform(playerData) + self:_Platform( playerData ) - elseif playerData.step==AIRBOSS.PatternStep.ARCIN then + elseif playerData.step == AIRBOSS.PatternStep.ARCIN then -- Case II/III if offset. - self:_ArcInTurn(playerData) + self:_ArcInTurn( playerData ) - elseif playerData.step==AIRBOSS.PatternStep.ARCOUT then + elseif playerData.step == AIRBOSS.PatternStep.ARCOUT then -- Case II/III if offset. - self:_ArcOutTurn(playerData) + self:_ArcOutTurn( playerData ) - elseif playerData.step==AIRBOSS.PatternStep.DIRTYUP then + elseif playerData.step == AIRBOSS.PatternStep.DIRTYUP then -- CASE III: Player has descended to 1200 ft and is going level from now on. - self:_DirtyUp(playerData) + self:_DirtyUp( playerData ) - elseif playerData.step==AIRBOSS.PatternStep.BULLSEYE then + elseif playerData.step == AIRBOSS.PatternStep.BULLSEYE then -- CASE III: Player has intercepted the glide slope and should follow "Bullseye" (ICLS). - self:_Bullseye(playerData) + self:_Bullseye( playerData ) - elseif playerData.step==AIRBOSS.PatternStep.INITIAL then + elseif playerData.step == AIRBOSS.PatternStep.INITIAL then -- CASE I/II: Player is at the initial position entering the landing pattern. - self:_Initial(playerData) + self:_Initial( playerData ) - elseif playerData.step==AIRBOSS.PatternStep.BREAKENTRY then + elseif playerData.step == AIRBOSS.PatternStep.BREAKENTRY then -- CASE I/II: Break entry. - self:_BreakEntry(playerData) + self:_BreakEntry( playerData ) - elseif playerData.step==AIRBOSS.PatternStep.EARLYBREAK then + elseif playerData.step == AIRBOSS.PatternStep.EARLYBREAK then -- CASE I/II: Early break. - self:_Break(playerData, AIRBOSS.PatternStep.EARLYBREAK) + self:_Break( playerData, AIRBOSS.PatternStep.EARLYBREAK ) - elseif playerData.step==AIRBOSS.PatternStep.LATEBREAK then + elseif playerData.step == AIRBOSS.PatternStep.LATEBREAK then -- CASE I/II: Late break. - self:_Break(playerData, AIRBOSS.PatternStep.LATEBREAK) + self:_Break( playerData, AIRBOSS.PatternStep.LATEBREAK ) - elseif playerData.step==AIRBOSS.PatternStep.ABEAM then + elseif playerData.step == AIRBOSS.PatternStep.ABEAM then -- CASE I/II: Abeam position. - self:_Abeam(playerData) + self:_Abeam( playerData ) - elseif playerData.step==AIRBOSS.PatternStep.NINETY then + elseif playerData.step == AIRBOSS.PatternStep.NINETY then -- CASE:I/II: Check long down wind leg. - self:_CheckForLongDownwind(playerData) + self:_CheckForLongDownwind( playerData ) -- At the ninety. - self:_Ninety(playerData) + self:_Ninety( playerData ) - elseif playerData.step==AIRBOSS.PatternStep.WAKE then + elseif playerData.step == AIRBOSS.PatternStep.WAKE then -- CASE I/II: In the wake. - self:_Wake(playerData) + self:_Wake( playerData ) - elseif playerData.step==AIRBOSS.PatternStep.EMERGENCY then + elseif playerData.step == AIRBOSS.PatternStep.EMERGENCY then -- Emergency landing. Player pos is not checked. - self:_Final(playerData, true) + self:_Final( playerData, true ) - elseif playerData.step==AIRBOSS.PatternStep.FINAL then + elseif playerData.step == AIRBOSS.PatternStep.FINAL then -- CASE I/II: Turn to final and enter the groove. - self:_Final(playerData) + self:_Final( playerData ) - elseif playerData.step==AIRBOSS.PatternStep.GROOVE_XX or - playerData.step==AIRBOSS.PatternStep.GROOVE_IM or - playerData.step==AIRBOSS.PatternStep.GROOVE_IC or - playerData.step==AIRBOSS.PatternStep.GROOVE_AR or - playerData.step==AIRBOSS.PatternStep.GROOVE_AL or - playerData.step==AIRBOSS.PatternStep.GROOVE_LC or - playerData.step==AIRBOSS.PatternStep.GROOVE_IW then + elseif playerData.step == AIRBOSS.PatternStep.GROOVE_XX or + playerData.step == AIRBOSS.PatternStep.GROOVE_IM or + playerData.step == AIRBOSS.PatternStep.GROOVE_IC or + playerData.step == AIRBOSS.PatternStep.GROOVE_AR or + playerData.step == AIRBOSS.PatternStep.GROOVE_AL or + playerData.step == AIRBOSS.PatternStep.GROOVE_LC or + playerData.step == AIRBOSS.PatternStep.GROOVE_IW then -- CASE I/II: In the groove. - self:_Groove(playerData) + self:_Groove( playerData ) - elseif playerData.step==AIRBOSS.PatternStep.DEBRIEF then + elseif playerData.step == AIRBOSS.PatternStep.DEBRIEF then -- Debriefing in 5 seconds. - --SCHEDULER:New(nil, self._Debrief, {self, playerData}, 5) - playerData.debriefschedulerID=self:ScheduleOnce(5, self._Debrief, self, playerData) + -- SCHEDULER:New(nil, self._Debrief, {self, playerData}, 5) + playerData.debriefschedulerID = self:ScheduleOnce( 5, self._Debrief, self, playerData ) -- Undefined status. - playerData.step=AIRBOSS.PatternStep.UNDEFINED + playerData.step = AIRBOSS.PatternStep.UNDEFINED else -- Error, unknown step! - self:E(self.lid..string.format("ERROR: Unknown player step %s. Please report!", tostring(playerData.step))) + self:E( self.lid .. string.format( "ERROR: Unknown player step %s. Please report!", tostring( playerData.step ) ) ) end -- Check if player missed a step during Case II/III and allow him to enter the landing pattern. - self:_CheckMissedStepOnEntry(playerData) + self:_CheckMissedStepOnEntry( playerData ) else - self:T2(self.lid.."WARNING: Player unit not inside the CCA!") + self:T2( self.lid .. "WARNING: Player unit not inside the CCA!" ) end else -- Unit not alive. - self:T(self.lid.."WARNING: Player unit is not alive!") + self:T( self.lid .. "WARNING: Player unit is not alive!" ) end end end end - --- Checks if a player is in the pattern queue and has missed a step in Case II/III approach. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. -function AIRBOSS:_CheckMissedStepOnEntry(playerData) +function AIRBOSS:_CheckMissedStepOnEntry( playerData ) -- Conditions to be met: Case II/III, in pattern queue, flag!=42 (will be set to 42 at the end if player missed a step). - local rightcase=playerData.case>1 - local rightqueue=self:_InQueue(self.Qpattern, playerData.group) - local rightflag=playerData.flag~=-42 + local rightcase = playerData.case > 1 + local rightqueue = self:_InQueue( self.Qpattern, playerData.group ) + local rightflag = playerData.flag ~= -42 -- Steps that the player could have missed during Case II/III. - local step=playerData.step - local missedstep=step==AIRBOSS.PatternStep.PLATFORM or step==AIRBOSS.PatternStep.ARCIN or step==AIRBOSS.PatternStep.ARCOUT or step==AIRBOSS.PatternStep.DIRTYUP + local step = playerData.step + local missedstep = step == AIRBOSS.PatternStep.PLATFORM or step == AIRBOSS.PatternStep.ARCIN or step == AIRBOSS.PatternStep.ARCOUT or step == AIRBOSS.PatternStep.DIRTYUP -- Check if player is about to enter the initial or bullseye zones and maybe has missed a step in the pattern. if rightcase and rightqueue and rightflag then -- Get right zone. - local zone=nil - if playerData.case==2 and missedstep then + local zone = nil + if playerData.case == 2 and missedstep then - zone=self:_GetZoneInitial(playerData.case) + zone = self:_GetZoneInitial( playerData.case ) - elseif playerData.case==3 and missedstep then + elseif playerData.case == 3 and missedstep then - zone=self:_GetZoneBullseye(playerData.case) + zone = self:_GetZoneBullseye( playerData.case ) end @@ -105720,28 +112207,28 @@ function AIRBOSS:_CheckMissedStepOnEntry(playerData) if zone then -- Check if player is in initial or bullseye zone. - local inzone=playerData.unit:IsInZone(zone) + local inzone = playerData.unit:IsInZone( zone ) -- Relative heading to carrier direction. - local relheading=self:_GetRelativeHeading(playerData.unit, false) + local relheading = self:_GetRelativeHeading( playerData.unit, false ) -- Check if player is in zone and flying roughly in the right direction. - if inzone and math.abs(relheading)<60 then + if inzone and math.abs( relheading ) < 60 then -- Player is in one of the initial zones short before the landing pattern. - local text=string.format("you missed an important step in the pattern!\nYour next step would have been %s.", playerData.step) - self:MessageToPlayer(playerData, text, "AIRBOSS", nil, 5) + local text = string.format( "you missed an important step in the pattern!\nYour next step would have been %s.", playerData.step ) + self:MessageToPlayer( playerData, text, "AIRBOSS", nil, 5 ) - if playerData.case==2 then + if playerData.case == 2 then -- Set next step to initial. - playerData.step=AIRBOSS.PatternStep.INITIAL - elseif playerData.case==3 then + playerData.step = AIRBOSS.PatternStep.INITIAL + elseif playerData.case == 3 then -- Set next step to bullseye. - playerData.step=AIRBOSS.PatternStep.BULLSEYE + playerData.step = AIRBOSS.PatternStep.BULLSEYE end -- Set flag value to -42. This is the value to ensure that this routine is not called again! - playerData.flag=-42 + playerData.flag = -42 end end end @@ -105750,13 +112237,13 @@ end --- Set time in the groove for player. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. -function AIRBOSS:_SetTimeInGroove(playerData) +function AIRBOSS:_SetTimeInGroove( playerData ) -- Set time in the groove if playerData.TIG0 then - playerData.Tgroove=timer.getTime()-playerData.TIG0 + playerData.Tgroove = timer.getTime() - playerData.TIG0 else - playerData.Tgroove=999 + playerData.Tgroove = 999 end end @@ -105765,19 +112252,18 @@ end -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. -- @return #number Player's time in groove in seconds. -function AIRBOSS:_GetTimeInGroove(playerData) +function AIRBOSS:_GetTimeInGroove( playerData ) - local Tgroove=999 + local Tgroove = 999 -- Get time in the groove. if playerData.TIG0 then - Tgroove=timer.getTime()-playerData.TIG0 + Tgroove = timer.getTime() - playerData.TIG0 end return Tgroove end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- EVENT functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -105785,62 +112271,62 @@ end --- Airboss event handler for event birth. -- @param #AIRBOSS self -- @param Core.Event#EVENTDATA EventData -function AIRBOSS:OnEventBirth(EventData) - self:F3({eventbirth = EventData}) +function AIRBOSS:OnEventBirth( EventData ) + self:F3( { eventbirth = EventData } ) -- Nil checks. - if EventData==nil then - self:E(self.lid.."ERROR: EventData=nil in event BIRTH!") - self:E(EventData) + if EventData == nil then + self:E( self.lid .. "ERROR: EventData=nil in event BIRTH!" ) + self:E( EventData ) return end - if EventData.IniUnit==nil then - self:E(self.lid.."ERROR: EventData.IniUnit=nil in event BIRTH!") - self:E(EventData) + if EventData.IniUnit == nil then + self:E( self.lid .. "ERROR: EventData.IniUnit=nil in event BIRTH!" ) + self:E( EventData ) return end - local _unitName=EventData.IniUnitName - local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + local _unitName = EventData.IniUnitName + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) - self:T(self.lid.."BIRTH: unit = "..tostring(EventData.IniUnitName)) - self:T(self.lid.."BIRTH: group = "..tostring(EventData.IniGroupName)) - self:T(self.lid.."BIRTH: player = "..tostring(_playername)) + self:T( self.lid .. "BIRTH: unit = " .. tostring( EventData.IniUnitName ) ) + self:T( self.lid .. "BIRTH: group = " .. tostring( EventData.IniGroupName ) ) + self:T( self.lid .. "BIRTH: player = " .. tostring( _playername ) ) if _unit and _playername then - local _uid=_unit:GetID() - local _group=_unit:GetGroup() - local _callsign=_unit:GetCallsign() + local _uid = _unit:GetID() + local _group = _unit:GetGroup() + local _callsign = _unit:GetCallsign() -- Debug output. - local text=string.format("Pilot %s, callsign %s entered unit %s of group %s.", _playername, _callsign, _unitName, _group:GetName()) - self:T(self.lid..text) - MESSAGE:New(text, 5):ToAllIf(self.Debug) + local text = string.format( "Pilot %s, callsign %s entered unit %s of group %s.", _playername, _callsign, _unitName, _group:GetName() ) + self:T( self.lid .. text ) + MESSAGE:New( text, 5 ):ToAllIf( self.Debug ) -- Check if aircraft type the player occupies is carrier capable. - local rightaircraft=self:_IsCarrierAircraft(_unit) - if rightaircraft==false then - local text=string.format("Player aircraft type %s not supported by AIRBOSS class.", _unit:GetTypeName()) - MESSAGE:New(text, 30):ToAllIf(self.Debug) - self:T2(self.lid..text) + local rightaircraft = self:_IsCarrierAircraft( _unit ) + if rightaircraft == false then + local text = string.format( "Player aircraft type %s not supported by AIRBOSS class.", _unit:GetTypeName() ) + MESSAGE:New( text, 30 ):ToAllIf( self.Debug ) + self:T2( self.lid .. text ) return end -- Check that coalition of the carrier and aircraft match. - if self:GetCoalition()~=_unit:GetCoalition() then - local text=string.format("Player entered aircraft of other coalition.") - MESSAGE:New(text, 30):ToAllIf(self.Debug) - self:T(self.lid..text) + if self:GetCoalition() ~= _unit:GetCoalition() then + local text = string.format( "Player entered aircraft of other coalition." ) + MESSAGE:New( text, 30 ):ToAllIf( self.Debug ) + self:T( self.lid .. text ) return end -- Add Menu commands. - self:_AddF10Commands(_unitName) + self:_AddF10Commands( _unitName ) -- Delaying the new player for a second, because AI units of the flight would not be registered correctly. - --SCHEDULER:New(nil, self._NewPlayer, {self, _unitName}, 1) - self:ScheduleOnce(1, self._NewPlayer, self, _unitName) + -- SCHEDULER:New(nil, self._NewPlayer, {self, _unitName}, 1) + self:ScheduleOnce( 1, self._NewPlayer, self, _unitName ) end end @@ -105848,51 +112334,51 @@ end --- Airboss event handler for event land. -- @param #AIRBOSS self -- @param Core.Event#EVENTDATA EventData -function AIRBOSS:OnEventLand(EventData) - self:F3({eventland = EventData}) +function AIRBOSS:OnEventLand( EventData ) + self:F3( { eventland = EventData } ) -- Nil checks. - if EventData==nil then - self:E(self.lid.."ERROR: EventData=nil in event LAND!") - self:E(EventData) + if EventData == nil then + self:E( self.lid .. "ERROR: EventData=nil in event LAND!" ) + self:E( EventData ) return end - if EventData.IniUnit==nil then - self:E(self.lid.."ERROR: EventData.IniUnit=nil in event LAND!") - self:E(EventData) + if EventData.IniUnit == nil then + self:E( self.lid .. "ERROR: EventData.IniUnit=nil in event LAND!" ) + self:E( EventData ) return end -- Get unit name that landed. - local _unitName=EventData.IniUnitName + local _unitName = EventData.IniUnitName -- Check if this was a player. - local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) -- Debug output. - self:T(self.lid.."LAND: unit = "..tostring(EventData.IniUnitName)) - self:T(self.lid.."LAND: group = "..tostring(EventData.IniGroupName)) - self:T(self.lid.."LAND: player = "..tostring(_playername)) + self:T( self.lid .. "LAND: unit = " .. tostring( EventData.IniUnitName ) ) + self:T( self.lid .. "LAND: group = " .. tostring( EventData.IniGroupName ) ) + self:T( self.lid .. "LAND: player = " .. tostring( _playername ) ) -- This would be the closest airbase. - local airbase=EventData.Place + local airbase = EventData.Place -- Nil check for airbase. Crashed as player gave me no airbase. - if airbase==nil then + if airbase == nil then return end -- Get airbase name. - local airbasename=tostring(airbase:GetName()) + local airbasename = tostring( airbase:GetName() ) -- Check if aircraft landed on the right airbase. - if airbasename==self.airbase:GetName() then + if airbasename == self.airbase:GetName() then -- Stern coordinate at the rundown. - local stern=self:_GetSternCoord() + local stern = self:_GetSternCoord() -- Polygon zone close around the carrier. - local zoneCarrier=self:_GetZoneCarrierBox() + local zoneCarrier = self:_GetZoneCarrierBox() -- Check if player or AI landed. if _unit and _playername then @@ -105902,41 +112388,41 @@ function AIRBOSS:OnEventLand(EventData) ------------------------- -- Get info. - local _uid=_unit:GetID() - local _group=_unit:GetGroup() - local _callsign=_unit:GetCallsign() + local _uid = _unit:GetID() + local _group = _unit:GetGroup() + local _callsign = _unit:GetCallsign() -- Debug output. - local text=string.format("Player %s, callsign %s unit %s (ID=%d) of group %s landed at airbase %s", _playername, _callsign, _unitName, _uid, _group:GetName(), airbasename) - self:T(self.lid..text) - MESSAGE:New(text, 5, "DEBUG"):ToAllIf(self.Debug) + local text = string.format( "Player %s, callsign %s unit %s (ID=%d) of group %s landed at airbase %s", _playername, _callsign, _unitName, _uid, _group:GetName(), airbasename ) + self:T( self.lid .. text ) + MESSAGE:New( text, 5, "DEBUG" ):ToAllIf( self.Debug ) -- Player data. - local playerData=self.players[_playername] --#AIRBOSS.PlayerData + local playerData = self.players[_playername] -- #AIRBOSS.PlayerData -- Check if playerData is okay. - if playerData==nil then - self:E(self.lid..string.format("ERROR: playerData nil in landing event. unit=%s player=%s", tostring(_unitName), tostring(_playername))) + if playerData == nil then + self:E( self.lid .. string.format( "ERROR: playerData nil in landing event. unit=%s player=%s", tostring( _unitName ), tostring( _playername ) ) ) return end -- Check that player landed on the carrier. - if _unit:IsInZone(zoneCarrier) then + if _unit:IsInZone( zoneCarrier ) then -- Check if this was a valid approach. if not playerData.valid then -- Player missed at least one step in the pattern. - local text=string.format("you missed at least one important step in the pattern!\nYour next step would have been %s.\nThis pass is INVALID.", playerData.step) - self:MessageToPlayer(playerData, text, "AIRBOSS", nil, 30, true, 5) + local text = string.format( "you missed at least one important step in the pattern!\nYour next step would have been %s.\nThis pass is INVALID.", playerData.step ) + self:MessageToPlayer( playerData, text, "AIRBOSS", nil, 30, true, 5 ) -- Clear queues just in case. - self:_RemoveFlightFromMarshalQueue(playerData, true) - self:_RemoveFlightFromQueue(self.Qpattern, playerData) - self:_RemoveFlightFromQueue(self.Qwaiting, playerData) - self:_RemoveFlightFromQueue(self.Qspinning, playerData) + self:_RemoveFlightFromMarshalQueue( playerData, true ) + self:_RemoveFlightFromQueue( self.Qpattern, playerData ) + self:_RemoveFlightFromQueue( self.Qwaiting, playerData ) + self:_RemoveFlightFromQueue( self.Qspinning, playerData ) -- Reinitialize player data. - self:_InitPlayer(playerData) + self:_InitPlayer( playerData ) return end @@ -105944,57 +112430,57 @@ function AIRBOSS:OnEventLand(EventData) -- Check if player already landed. We dont need a second time. if playerData.landed then - self:E(self.lid..string.format("Player %s just landed a second time.", _playername)) + self:E( self.lid .. string.format( "Player %s just landed a second time.", _playername ) ) else -- We did land. - playerData.landed=true + playerData.landed = true -- Switch attitude monitor off if on. - playerData.attitudemonitor=false + playerData.attitudemonitor = false -- Coordinate at landing event. - local coord=playerData.unit:GetCoordinate() + local coord = playerData.unit:GetCoordinate() -- Get distances relative to - local X,Z,rho,phi=self:_GetDistances(_unit) + local X, Z, rho, phi = self:_GetDistances( _unit ) -- Landing distance wrt to stern position. - local dist=coord:Get2DDistance(stern) + local dist = coord:Get2DDistance( stern ) -- Debug mark of player landing coord. if self.Debug and false then -- Debug mark of player landing coord. - local lp=coord:MarkToAll("Landing coord.") + local lp = coord:MarkToAll( "Landing coord." ) coord:SmokeGreen() end -- Set time in the groove of player. - self:_SetTimeInGroove(playerData) + self:_SetTimeInGroove( playerData ) -- Debug text. - local text=string.format("Player %s AC type %s landed at dist=%.1f m. Tgroove=%.1f sec.", playerData.name, playerData.actype, dist, self:_GetTimeInGroove(playerData)) - text=text..string.format(" X=%.1f m, Z=%.1f m, rho=%.1f m.", X, Z, rho) - self:T(self.lid..text) + local text = string.format( "Player %s AC type %s landed at dist=%.1f m. Tgroove=%.1f sec.", playerData.name, playerData.actype, dist, self:_GetTimeInGroove( playerData ) ) + text = text .. string.format( " X=%.1f m, Z=%.1f m, rho=%.1f m.", X, Z, rho ) + self:T( self.lid .. text ) -- Check carrier type. - if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then + if self.carriertype == AIRBOSS.CarrierType.HERMES or self.carriertype == AIRBOSS.CarrierType.TARAWA or self.carriertype == AIRBOSS.CarrierType.AMERICA or self.carriertype == AIRBOSS.CarrierType.JCARLOS or self.carriertype == AIRBOSS.CarrierType.CANBERRA then -- Power "Idle". - self:RadioTransmission(self.LSORadio, self.LSOCall.IDLE, false, 1, nil, true) + self:RadioTransmission( self.LSORadio, self.LSOCall.IDLE, false, 1, nil, true ) -- Next step debrief. - self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.DEBRIEF) + self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.DEBRIEF ) else -- Next step undefined until we know more. - self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.UNDEFINED) + self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.UNDEFINED ) -- Call trapped function in 1 second to make sure we did not bolter. - --SCHEDULER:New(nil, self._Trapped, {self, playerData}, 1) - self:ScheduleOnce(1, self._Trapped, self, playerData) + -- SCHEDULER:New(nil, self._Trapped, {self, playerData}, 1) + self:ScheduleOnce( 1, self._Trapped, self, playerData ) end @@ -106004,7 +112490,7 @@ function AIRBOSS:OnEventLand(EventData) -- Handle case where player did not land on the carrier. -- Well, I guess, he leaves the slot or ejects. Both should be handled. if playerData then - self:E(self.lid..string.format("Player %s did not land in carrier box zone. Maybe in the water near the carrier?", playerData.name)) + self:E( self.lid .. string.format( "Player %s did not land in carrier box zone. Maybe in the water near the carrier?", playerData.name ) ) end end @@ -106014,31 +112500,32 @@ function AIRBOSS:OnEventLand(EventData) -- AI unit landed -- -------------------- - if self.carriertype~=AIRBOSS.CarrierType.TARAWA or self.carriertype~=AIRBOSS.CarrierType.AMERICA or self.carriertype~=AIRBOSS.CarrierType.JCARLOS then + if self.carriertype ~= AIRBOSS.CarrierType.HERMES or self.carriertype ~= AIRBOSS.CarrierType.TARAWA or self.carriertype ~= AIRBOSS.CarrierType.AMERICA or self.carriertype ~= AIRBOSS.CarrierType.JCARLOS or self.carriertype ~= AIRBOSS.CarrierType.CANBERRA then + -- Coordinate at landing event - local coord=EventData.IniUnit:GetCoordinate() + local coord = EventData.IniUnit:GetCoordinate() -- Debug mark of player landing coord. - local dist=coord:Get2DDistance(self:GetCoordinate()) + local dist = coord:Get2DDistance( self:GetCoordinate() ) -- Get wire - local wire=self:_GetWire(coord, 0) + local wire = self:_GetWire( coord, 0 ) -- Aircraft type. - local _type=EventData.IniUnit:GetTypeName() + local _type = EventData.IniUnit:GetTypeName() -- Debug text. - local text=string.format("AI unit %s of type %s landed at dist=%.1f m. Trapped wire=%d.", _unitName, _type, dist, wire) - self:T(self.lid..text) + local text = string.format( "AI unit %s of type %s landed at dist=%.1f m. Trapped wire=%d.", _unitName, _type, dist, wire ) + self:T( self.lid .. text ) end -- AI always lands ==> remove unit from flight group and queues. - local flight=self:_RecoveredElement(EventData.IniUnit) + local flight = self:_RecoveredElement( EventData.IniUnit ) -- Check if all were recovered. If so update pattern queue. - self:_CheckSectionRecovered(flight) + self:_CheckSectionRecovered( flight ) end end @@ -106047,62 +112534,61 @@ end --- Airboss event handler for event that a unit shuts down its engines. -- @param #AIRBOSS self -- @param Core.Event#EVENTDATA EventData -function AIRBOSS:OnEventEngineShutdown(EventData) - self:F3({eventengineshutdown=EventData}) +function AIRBOSS:OnEventEngineShutdown( EventData ) + self:F3( { eventengineshutdown = EventData } ) -- Nil checks. - if EventData==nil then - self:E(self.lid.."ERROR: EventData=nil in event ENGINESHUTDOWN!") - self:E(EventData) + if EventData == nil then + self:E( self.lid .. "ERROR: EventData=nil in event ENGINESHUTDOWN!" ) + self:E( EventData ) return end - if EventData.IniUnit==nil then - self:E(self.lid.."ERROR: EventData.IniUnit=nil in event ENGINESHUTDOWN!") - self:E(EventData) + if EventData.IniUnit == nil then + self:E( self.lid .. "ERROR: EventData.IniUnit=nil in event ENGINESHUTDOWN!" ) + self:E( EventData ) return end + local _unitName = EventData.IniUnitName + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) - local _unitName=EventData.IniUnitName - local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) - - self:T3(self.lid.."ENGINESHUTDOWN: unit = "..tostring(EventData.IniUnitName)) - self:T3(self.lid.."ENGINESHUTDOWN: group = "..tostring(EventData.IniGroupName)) - self:T3(self.lid.."ENGINESHUTDOWN: player = "..tostring(_playername)) + self:T3( self.lid .. "ENGINESHUTDOWN: unit = " .. tostring( EventData.IniUnitName ) ) + self:T3( self.lid .. "ENGINESHUTDOWN: group = " .. tostring( EventData.IniGroupName ) ) + self:T3( self.lid .. "ENGINESHUTDOWN: player = " .. tostring( _playername ) ) if _unit and _playername then -- Debug message. - self:T(self.lid..string.format("Player %s shut down its engines!",_playername)) + self:T( self.lid .. string.format( "Player %s shut down its engines!", _playername ) ) else -- Debug message. - self:T(self.lid..string.format("AI unit %s shut down its engines!", _unitName)) + self:T( self.lid .. string.format( "AI unit %s shut down its engines!", _unitName ) ) -- Get flight. - local flight=self:_GetFlightFromGroupInQueue(EventData.IniGroup, self.flights) + local flight = self:_GetFlightFromGroupInQueue( EventData.IniGroup, self.flights ) -- Only AI flights. if flight and flight.ai then -- Check if all elements were recovered. - local recovered=self:_CheckSectionRecovered(flight) + local recovered = self:_CheckSectionRecovered( flight ) -- Despawn group and completely remove flight. if recovered then - self:T(self.lid..string.format("AI group %s completely recovered. Despawning group after engine shutdown event as requested in 5 seconds.", tostring(EventData.IniGroupName))) + self:T( self.lid .. string.format( "AI group %s completely recovered. Despawning group after engine shutdown event as requested in 5 seconds.", tostring( EventData.IniGroupName ) ) ) -- Remove flight. - self:_RemoveFlight(flight) + self:_RemoveFlight( flight ) -- Check if this is a tanker or AWACS associated with the carrier. - local istanker=self.tanker and self.tanker.tanker:GetName()==EventData.IniGroupName - local isawacs=self.awacs and self.awacs.tanker:GetName()==EventData.IniGroupName + local istanker = self.tanker and self.tanker.tanker:GetName() == EventData.IniGroupName + local isawacs = self.awacs and self.awacs.tanker:GetName() == EventData.IniGroupName -- Destroy group if desired. Recovery tankers have their own logic for despawning. if self.despawnshutdown and not (istanker or isawacs) then - EventData.IniGroup:Destroy(nil, 5) + EventData.IniGroup:Destroy( nil, 5 ) end end @@ -106114,61 +112600,60 @@ end --- Airboss event handler for event that a unit takes off. -- @param #AIRBOSS self -- @param Core.Event#EVENTDATA EventData -function AIRBOSS:OnEventTakeoff(EventData) - self:F3({eventtakeoff=EventData}) +function AIRBOSS:OnEventTakeoff( EventData ) + self:F3( { eventtakeoff = EventData } ) -- Nil checks. - if EventData==nil then - self:E(self.lid.."ERROR: EventData=nil in event TAKEOFF!") - self:E(EventData) + if EventData == nil then + self:E( self.lid .. "ERROR: EventData=nil in event TAKEOFF!" ) + self:E( EventData ) return end - if EventData.IniUnit==nil then - self:E(self.lid.."ERROR: EventData.IniUnit=nil in event TAKEOFF!") - self:E(EventData) + if EventData.IniUnit == nil then + self:E( self.lid .. "ERROR: EventData.IniUnit=nil in event TAKEOFF!" ) + self:E( EventData ) return end + local _unitName = EventData.IniUnitName + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) - local _unitName=EventData.IniUnitName - local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) - - self:T3(self.lid.."TAKEOFF: unit = "..tostring(EventData.IniUnitName)) - self:T3(self.lid.."TAKEOFF: group = "..tostring(EventData.IniGroupName)) - self:T3(self.lid.."TAKEOFF: player = "..tostring(_playername)) + self:T3( self.lid .. "TAKEOFF: unit = " .. tostring( EventData.IniUnitName ) ) + self:T3( self.lid .. "TAKEOFF: group = " .. tostring( EventData.IniGroupName ) ) + self:T3( self.lid .. "TAKEOFF: player = " .. tostring( _playername ) ) -- Airbase. - local airbase=EventData.Place + local airbase = EventData.Place -- Airbase name. - local airbasename="unknown" + local airbasename = "unknown" if airbase then - airbasename=airbase:GetName() + airbasename = airbase:GetName() end -- Check right airbase. - if airbasename==self.airbase:GetName() then + if airbasename == self.airbase:GetName() then if _unit and _playername then -- Debug message. - self:T(self.lid..string.format("Player %s took off at %s!",_playername, airbasename)) + self:T( self.lid .. string.format( "Player %s took off at %s!", _playername, airbasename ) ) else -- Debug message. - self:T2(self.lid..string.format("AI unit %s took off at %s!", _unitName, airbasename)) + self:T2( self.lid .. string.format( "AI unit %s took off at %s!", _unitName, airbasename ) ) -- Get flight. - local flight=self:_GetFlightFromGroupInQueue(EventData.IniGroup, self.flights) + local flight = self:_GetFlightFromGroupInQueue( EventData.IniGroup, self.flights ) if flight then -- Set ballcall and recoverd status. - for _,elem in pairs(flight.elements) do - local element=elem --#AIRBOSS.FlightElement - element.ballcall=false - element.recovered=nil + for _, elem in pairs( flight.elements ) do + local element = elem -- #AIRBOSS.FlightElement + element.ballcall = false + element.recovered = nil end end end @@ -106179,48 +112664,47 @@ end --- Airboss event handler for event crash. -- @param #AIRBOSS self -- @param Core.Event#EVENTDATA EventData -function AIRBOSS:OnEventCrash(EventData) - self:F3({eventcrash = EventData}) +function AIRBOSS:OnEventCrash( EventData ) + self:F3( { eventcrash = EventData } ) -- Nil checks. - if EventData==nil then - self:E(self.lid.."ERROR: EventData=nil in event CRASH!") - self:E(EventData) + if EventData == nil then + self:E( self.lid .. "ERROR: EventData=nil in event CRASH!" ) + self:E( EventData ) return end - if EventData.IniUnit==nil then - self:E(self.lid.."ERROR: EventData.IniUnit=nil in event CRASH!") - self:E(EventData) + if EventData.IniUnit == nil then + self:E( self.lid .. "ERROR: EventData.IniUnit=nil in event CRASH!" ) + self:E( EventData ) return end + local _unitName = EventData.IniUnitName + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) - local _unitName=EventData.IniUnitName - local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) - - self:T3(self.lid.."CRASH: unit = "..tostring(EventData.IniUnitName)) - self:T3(self.lid.."CRASH: group = "..tostring(EventData.IniGroupName)) - self:T3(self.lid.."CARSH: player = "..tostring(_playername)) + self:T3( self.lid .. "CRASH: unit = " .. tostring( EventData.IniUnitName ) ) + self:T3( self.lid .. "CRASH: group = " .. tostring( EventData.IniGroupName ) ) + self:T3( self.lid .. "CARSH: player = " .. tostring( _playername ) ) if _unit and _playername then -- Debug message. - self:T(self.lid..string.format("Player %s crashed!",_playername)) + self:T( self.lid .. string.format( "Player %s crashed!", _playername ) ) -- Get player flight. - local flight=self.players[_playername] + local flight = self.players[_playername] -- Remove flight completely from all queues and collapse marshal if necessary. -- This also updates the section, if any and removes any unfinished gradings of the player. if flight then - self:_RemoveFlight(flight, true) + self:_RemoveFlight( flight, true ) end else -- Debug message. - self:T2(self.lid..string.format("AI unit %s crashed!", EventData.IniUnitName)) + self:T2( self.lid .. string.format( "AI unit %s crashed!", EventData.IniUnitName ) ) -- Remove unit from flight and queues. - self:_RemoveUnitFromFlight(EventData.IniUnit) + self:_RemoveUnitFromFlight( EventData.IniUnit ) end end @@ -106228,51 +112712,50 @@ end --- Airboss event handler for event Ejection. -- @param #AIRBOSS self -- @param Core.Event#EVENTDATA EventData -function AIRBOSS:OnEventEjection(EventData) - self:F3({eventland = EventData}) +function AIRBOSS:OnEventEjection( EventData ) + self:F3( { eventland = EventData } ) -- Nil checks. - if EventData==nil then - self:E(self.lid.."ERROR: EventData=nil in event EJECTION!") - self:E(EventData) + if EventData == nil then + self:E( self.lid .. "ERROR: EventData=nil in event EJECTION!" ) + self:E( EventData ) return end - if EventData.IniUnit==nil then - self:E(self.lid.."ERROR: EventData.IniUnit=nil in event EJECTION!") - self:E(EventData) + if EventData.IniUnit == nil then + self:E( self.lid .. "ERROR: EventData.IniUnit=nil in event EJECTION!" ) + self:E( EventData ) return end + local _unitName = EventData.IniUnitName + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) - local _unitName=EventData.IniUnitName - local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) - - self:T3(self.lid.."EJECT: unit = "..tostring(EventData.IniUnitName)) - self:T3(self.lid.."EJECT: group = "..tostring(EventData.IniGroupName)) - self:T3(self.lid.."EJECT: player = "..tostring(_playername)) + self:T3( self.lid .. "EJECT: unit = " .. tostring( EventData.IniUnitName ) ) + self:T3( self.lid .. "EJECT: group = " .. tostring( EventData.IniGroupName ) ) + self:T3( self.lid .. "EJECT: player = " .. tostring( _playername ) ) if _unit and _playername then - self:T(self.lid..string.format("Player %s ejected!",_playername)) + self:T( self.lid .. string.format( "Player %s ejected!", _playername ) ) -- Get player flight. - local flight=self.players[_playername] + local flight = self.players[_playername] -- Remove flight completely from all queues and collapse marshal if necessary. if flight then - self:_RemoveFlight(flight, true) + self:_RemoveFlight( flight, true ) end else -- Debug message. - self:T(self.lid..string.format("AI unit %s ejected!", EventData.IniUnitName)) + self:T( self.lid .. string.format( "AI unit %s ejected!", EventData.IniUnitName ) ) -- Remove element/unit from flight group and from all queues if no elements alive. - self:_RemoveUnitFromFlight(EventData.IniUnit) + self:_RemoveUnitFromFlight( EventData.IniUnit ) -- What could happen is, that another element has landed (recovered) already and this one crashes. -- This would mean that the flight would not be deleted from the queue ==> Check if section recovered. - local flight=self:_GetFlightFromGroupInQueue(EventData.IniGroup, self.flights) - self:_CheckSectionRecovered(flight) + local flight = self:_GetFlightFromGroupInQueue( EventData.IniGroup, self.flights ) + self:_CheckSectionRecovered( flight ) end end @@ -106280,51 +112763,50 @@ end --- Airboss event handler for event REMOVEUNIT. -- @param #AIRBOSS self -- @param Core.Event#EVENTDATA EventData -function AIRBOSS:OnEventRemoveUnit(EventData) - self:F3({eventland = EventData}) +function AIRBOSS:OnEventRemoveUnit( EventData ) + self:F3( { eventland = EventData } ) -- Nil checks. - if EventData==nil then - self:E(self.lid.."ERROR: EventData=nil in event REMOVEUNIT!") - self:E(EventData) + if EventData == nil then + self:E( self.lid .. "ERROR: EventData=nil in event REMOVEUNIT!" ) + self:E( EventData ) return end - if EventData.IniUnit==nil then - self:E(self.lid.."ERROR: EventData.IniUnit=nil in event REMOVEUNIT!") - self:E(EventData) + if EventData.IniUnit == nil then + self:E( self.lid .. "ERROR: EventData.IniUnit=nil in event REMOVEUNIT!" ) + self:E( EventData ) return end + local _unitName = EventData.IniUnitName + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) - local _unitName=EventData.IniUnitName - local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) - - self:T3(self.lid.."EJECT: unit = "..tostring(EventData.IniUnitName)) - self:T3(self.lid.."EJECT: group = "..tostring(EventData.IniGroupName)) - self:T3(self.lid.."EJECT: player = "..tostring(_playername)) + self:T3( self.lid .. "EJECT: unit = " .. tostring( EventData.IniUnitName ) ) + self:T3( self.lid .. "EJECT: group = " .. tostring( EventData.IniGroupName ) ) + self:T3( self.lid .. "EJECT: player = " .. tostring( _playername ) ) if _unit and _playername then - self:T(self.lid..string.format("Player %s removed!",_playername)) + self:T( self.lid .. string.format( "Player %s removed!", _playername ) ) -- Get player flight. - local flight=self.players[_playername] + local flight = self.players[_playername] -- Remove flight completely from all queues and collapse marshal if necessary. if flight then - self:_RemoveFlight(flight, true) + self:_RemoveFlight( flight, true ) end else -- Debug message. - self:T(self.lid..string.format("AI unit %s removed!", EventData.IniUnitName)) + self:T( self.lid .. string.format( "AI unit %s removed!", EventData.IniUnitName ) ) -- Remove element/unit from flight group and from all queues if no elements alive. - self:_RemoveUnitFromFlight(EventData.IniUnit) + self:_RemoveUnitFromFlight( EventData.IniUnit ) -- What could happen is, that another element has landed (recovered) already and this one crashes. -- This would mean that the flight would not be deleted from the queue ==> Check if section recovered. - local flight=self:_GetFlightFromGroupInQueue(EventData.IniGroup, self.flights) - self:_CheckSectionRecovered(flight) + local flight = self:_GetFlightFromGroupInQueue( EventData.IniGroup, self.flights ) + self:_CheckSectionRecovered( flight ) end end @@ -106332,41 +112814,40 @@ end --- Airboss event handler for event player leave unit. -- @param #AIRBOSS self -- @param Core.Event#EVENTDATA EventData ---function AIRBOSS:OnEventPlayerLeaveUnit(EventData) -function AIRBOSS:_PlayerLeft(EventData) - self:F3({eventleave=EventData}) +-- function AIRBOSS:OnEventPlayerLeaveUnit(EventData) +function AIRBOSS:_PlayerLeft( EventData ) + self:F3( { eventleave = EventData } ) -- Nil checks. - if EventData==nil then - self:E(self.lid.."ERROR: EventData=nil in event PLAYERLEFTUNIT!") - self:E(EventData) + if EventData == nil then + self:E( self.lid .. "ERROR: EventData=nil in event PLAYERLEFTUNIT!" ) + self:E( EventData ) return end - if EventData.IniUnit==nil then - self:E(self.lid.."ERROR: EventData.IniUnit=nil in event PLAYERLEFTUNIT!") - self:E(EventData) + if EventData.IniUnit == nil then + self:E( self.lid .. "ERROR: EventData.IniUnit=nil in event PLAYERLEFTUNIT!" ) + self:E( EventData ) return end + local _unitName = EventData.IniUnitName + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) - local _unitName=EventData.IniUnitName - local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) - - self:T3(self.lid.."PLAYERLEAVEUNIT: unit = "..tostring(EventData.IniUnitName)) - self:T3(self.lid.."PLAYERLEAVEUNIT: group = "..tostring(EventData.IniGroupName)) - self:T3(self.lid.."PLAYERLEAVEUNIT: player = "..tostring(_playername)) + self:T3( self.lid .. "PLAYERLEAVEUNIT: unit = " .. tostring( EventData.IniUnitName ) ) + self:T3( self.lid .. "PLAYERLEAVEUNIT: group = " .. tostring( EventData.IniGroupName ) ) + self:T3( self.lid .. "PLAYERLEAVEUNIT: player = " .. tostring( _playername ) ) if _unit and _playername then -- Debug info. - self:T(self.lid..string.format("Player %s left unit %s!",_playername, _unitName)) + self:T( self.lid .. string.format( "Player %s left unit %s!", _playername, _unitName ) ) -- Get player flight. - local flight=self.players[_playername] + local flight = self.players[_playername] -- Remove flight completely from all queues and collapse marshal if necessary. if flight then - self:_RemoveFlight(flight, true) + self:_RemoveFlight( flight, true ) end end @@ -106377,8 +112858,8 @@ end -- Handles the case when the mission is ended. -- @param #AIRBOSS self -- @param Core.Event#EVENTDATA EventData Event data. -function AIRBOSS:OnEventMissionEnd(EventData) - self:T3(self.lid.."Mission Ended") +function AIRBOSS:OnEventMissionEnd( EventData ) + self:T3( self.lid .. "Mission Ended" ) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -106388,31 +112869,31 @@ end --- Spinning -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. -function AIRBOSS:_Spinning(playerData) +function AIRBOSS:_Spinning( playerData ) -- Early break. - local SpinIt={} - SpinIt.name="Spinning" - SpinIt.Xmin=-UTILS.NMToMeters(6) -- Not more than 5 NM behind the boat. - SpinIt.Xmax= UTILS.NMToMeters(5) -- Not more than 5 NM in front of the boat. - SpinIt.Zmin=-UTILS.NMToMeters(6) -- Not more than 5 NM port. - SpinIt.Zmax= UTILS.NMToMeters(2) -- Not more than 3 NM starboard. - SpinIt.LimitXmin=-100 -- 100 meters behind the boat - SpinIt.LimitXmax=nil - SpinIt.LimitZmin=-UTILS.NMToMeters(1) -- 1 NM port - SpinIt.LimitZmax=nil + local SpinIt = {} + SpinIt.name = "Spinning" + SpinIt.Xmin = -UTILS.NMToMeters( 6 ) -- Not more than 5 NM behind the boat. + SpinIt.Xmax = UTILS.NMToMeters( 5 ) -- Not more than 5 NM in front of the boat. + SpinIt.Zmin = -UTILS.NMToMeters( 6 ) -- Not more than 5 NM port. + SpinIt.Zmax = UTILS.NMToMeters( 2 ) -- Not more than 3 NM starboard. + SpinIt.LimitXmin = -100 -- 100 meters behind the boat + SpinIt.LimitXmax = nil + SpinIt.LimitZmin = -UTILS.NMToMeters( 1 ) -- 1 NM port + SpinIt.LimitZmax = nil -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local X, Z, rho, phi=self:_GetDistances(playerData.unit) + local X, Z, rho, phi = self:_GetDistances( playerData.unit ) -- Check if we are in front of the boat (diffX > 0). - if self:_CheckLimits(X, Z, SpinIt) then + if self:_CheckLimits( X, Z, SpinIt ) then -- Player is "de-spinned". Should go to initial again. - self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.INITIAL) + self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.INITIAL ) -- Remove player from spinning queue. - self:_RemoveFlightFromQueue(self.Qspinning, playerData) + self:_RemoveFlightFromQueue( self.Qspinning, playerData ) end @@ -106421,28 +112902,28 @@ end --- Waiting outside 10 NM zone for free Marshal stack. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. -function AIRBOSS:_Waiting(playerData) +function AIRBOSS:_Waiting( playerData ) -- Create 10 NM zone around the carrier. - local radius=UTILS.NMToMeters(10) - local zone=ZONE_RADIUS:New("Carrier 10 NM Zone", self.carrier:GetVec2(), radius) + local radius = UTILS.NMToMeters( 10 ) + local zone = ZONE_RADIUS:New( "Carrier 10 NM Zone", self.carrier:GetVec2(), radius ) -- Check if player is inside 10 NM radius of the carrier. - local inzone=playerData.unit:IsInZone(zone) + local inzone = playerData.unit:IsInZone( zone ) -- Time player is waiting. - local Twaiting=timer.getAbsTime()-playerData.time + local Twaiting = timer.getAbsTime() - playerData.time -- Warning if player is inside the zone. - if inzone and Twaiting>3*60 and not playerData.warning then - local text=string.format("You are supposed to wait outside the 10 NM zone.") - self:MessageToPlayer(playerData, text, "AIRBOSS") - playerData.warning=true + if inzone and Twaiting > 3 * 60 and not playerData.warning then + local text = string.format( "You are supposed to wait outside the 10 NM zone." ) + self:MessageToPlayer( playerData, text, "AIRBOSS" ) + playerData.warning = true end -- Reset warning. - if inzone==false and playerData.warning==true then - playerData.warning=nil + if inzone == false and playerData.warning == true then + playerData.warning = nil end end @@ -106450,18 +112931,18 @@ end --- Holding. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. -function AIRBOSS:_Holding(playerData) +function AIRBOSS:_Holding( playerData ) -- Player unit and flight. - local unit=playerData.unit + local unit = playerData.unit -- Current stack. - local stack=playerData.flag + local stack = playerData.flag -- Check for reported error. - if stack<=0 then - local text=string.format("ERROR: player %s in step %s is holding but has stack=%s (<=0)", playerData.name, playerData.step, tostring(stack)) - self:E(self.lid..text) + if stack <= 0 then + local text = string.format( "ERROR: player %s in step %s is holding but has stack=%s (<=0)", playerData.name, playerData.step, tostring( stack ) ) + self:E( self.lid .. text ) end --------------------------- @@ -106469,99 +112950,99 @@ function AIRBOSS:_Holding(playerData) --------------------------- -- Pattern altitude. - local patternalt=self:_GetMarshalAltitude(stack, playerData.case) + local patternalt = self:_GetMarshalAltitude( stack, playerData.case ) -- Player altitude. - local playeralt=unit:GetAltitude() + local playeralt = unit:GetAltitude() -- Get holding zone of player. - local zoneHolding=self:_GetZoneHolding(playerData.case, stack) + local zoneHolding = self:_GetZoneHolding( playerData.case, stack ) -- Nil check. - if zoneHolding==nil then - self:E(self.lid.."ERROR: zoneHolding is nil!") - self:E({playerData=playerData}) + if zoneHolding == nil then + self:E( self.lid .. "ERROR: zoneHolding is nil!" ) + self:E( { playerData = playerData } ) return end -- Check if player is in holding zone. - local inholdingzone=unit:IsInZone(zoneHolding) + local inholdingzone = unit:IsInZone( zoneHolding ) -- Altitude difference between player and assigned stack. - local altdiff=playeralt-patternalt + local altdiff = playeralt - patternalt -- Acceptable altitude depending on player skill. - local altgood=UTILS.FeetToMeters(500) - if playerData.difficulty==AIRBOSS.Difficulty.HARD then + local altgood = UTILS.FeetToMeters( 500 ) + if playerData.difficulty == AIRBOSS.Difficulty.HARD then -- Pros can be expected to be within +-200 ft. - altgood=UTILS.FeetToMeters(200) - elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + altgood = UTILS.FeetToMeters( 200 ) + elseif playerData.difficulty == AIRBOSS.Difficulty.NORMAL then -- Normal guys should be within +-350 ft. - altgood=UTILS.FeetToMeters(350) - elseif playerData.difficulty==AIRBOSS.Difficulty.EASY then + altgood = UTILS.FeetToMeters( 350 ) + elseif playerData.difficulty == AIRBOSS.Difficulty.EASY then -- Students should be within +-500 ft. - altgood=UTILS.FeetToMeters(500) + altgood = UTILS.FeetToMeters( 500 ) end -- When back to good altitude = 50%. - local altback=altgood*0.5 + local altback = altgood * 0.5 -- Check if stack just collapsed and give the player one minute to change the altitude. - local justcollapsed=false + local justcollapsed = false if self.Tcollapse then -- Time since last stack change. - local dT=timer.getTime()-self.Tcollapse + local dT = timer.getTime() - self.Tcollapse -- TODO: check if this works. - --local dT=timer.getAbsTime()-playerData.time + -- local dT=timer.getAbsTime()-playerData.time -- Check if less then 90 seconds. - if dT<=90 then - justcollapsed=true + if dT <= 90 then + justcollapsed = true end end -- Check if altitude is acceptable. - local goodalt=math.abs(altdiff)altgood then + if altdiff > altgood then -- Issue warning for being too high. if not playerData.warning then - text=text..string.format("You left your assigned altitude. Descent to angels %d.", angels) - playerData.warning=true + text = text .. string.format( "You left your assigned altitude. Descent to angels %d.", angels ) + playerData.warning = true end - elseif altdiff<-altgood then + elseif altdiff < -altgood then -- Issue warning for being too low. if not playerData.warning then - text=text..string.format("You left your assigned altitude. Climb to angels %d.", angels) - playerData.warning=true + text = text .. string.format( "You left your assigned altitude. Climb to angels %d.", angels ) + playerData.warning = true end end @@ -106569,62 +113050,61 @@ function AIRBOSS:_Holding(playerData) end -- Back to assigned altitude. - if playerData.warning and math.abs(altdiff)<=altback then - text=text..string.format("Altitude is looking good again.") - playerData.warning=nil + if playerData.warning and math.abs( altdiff ) <= altback then + text = text .. string.format( "Altitude is looking good again." ) + playerData.warning = nil end - elseif playerData.holding==false then + elseif playerData.holding == false then -- Player left holding zone if inholdingzone then -- Player is back in the holding zone. - text=text..string.format("You are back in the holding zone. Now stay there!") - playerData.holding=true + text = text .. string.format( "You are back in the holding zone. Now stay there!" ) + playerData.holding = true else -- Player is still outside the holding zone. - self:T3("Player still outside the holding zone. What are you doing man?!") + self:T3( "Player still outside the holding zone. What are you doing man?!" ) end - elseif playerData.holding==nil then + elseif playerData.holding == nil then -- Player did not entered the holding zone yet. if inholdingzone then -- Player arrived in holding zone. - playerData.holding=true + playerData.holding = true -- Inform player. - text=text..string.format("You arrived at the holding zone.") + text = text .. string.format( "You arrived at the holding zone." ) -- Feedback on altitude. if goodalt then - text=text..string.format(" Altitude is good.") + text = text .. string.format( " Altitude is good." ) else - if altdiff<0 then - text=text..string.format(" But you're too low.") + if altdiff < 0 then + text = text .. string.format( " But you're too low." ) else - text=text..string.format(" But you're too high.") + text = text .. string.format( " But you're too high." ) end - text=text..string.format("\nCurrently assigned altitude is %d ft.", UTILS.MetersToFeet(patternalt)) - playerData.warning=true + text = text .. string.format( "\nCurrently assigned altitude is %d ft.", UTILS.MetersToFeet( patternalt ) ) + playerData.warning = true end else -- Player did not yet arrive in holding zone. - self:T3("Waiting for player to arrive in the holding zone.") + self:T3( "Waiting for player to arrive in the holding zone." ) end end -- Send message. if playerData.showhints then - self:MessageToPlayer(playerData, text, "MARSHAL") + self:MessageToPlayer( playerData, text, "MARSHAL" ) end end - --- Commence approach. This step initializes the player data. Section members are also set to commence. Next step depends on recovery case: -- -- * Case 1: Initial @@ -106633,24 +113113,24 @@ end -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. -- @param #boolean zonecheck If true, zone is checked before player is released. -function AIRBOSS:_Commencing(playerData, zonecheck) +function AIRBOSS:_Commencing( playerData, zonecheck ) -- Check for auto commence if zonecheck then -- Get auto commence zone. - local zoneCommence=self:_GetZoneCommence(playerData.case, playerData.flag) + local zoneCommence = self:_GetZoneCommence( playerData.case, playerData.flag ) -- Check if unit is in the zone. - local inzone=playerData.unit:IsInZone(zoneCommence) + local inzone = playerData.unit:IsInZone( zoneCommence ) -- Skip the rest if not in the zone yet. if not inzone then -- Friendly reminder. - if timer.getAbsTime()-playerData.time>180 then - self:_MarshalCallClearedForRecovery(playerData.onboard, playerData.case) - playerData.time=timer.getAbsTime() + if timer.getAbsTime() - playerData.time > 180 then + self:_MarshalCallClearedForRecovery( playerData.onboard, playerData.case ) + playerData.time = timer.getAbsTime() end -- Skip the rest. @@ -106660,48 +113140,48 @@ function AIRBOSS:_Commencing(playerData, zonecheck) end -- Remove flight from Marshal queue. If flight was in queue, stack is collapsed and flight added to the pattern queue. - self:_RemoveFlightFromMarshalQueue(playerData) + self:_RemoveFlightFromMarshalQueue( playerData ) -- Initialize player data for new approach. - self:_InitPlayer(playerData) + self:_InitPlayer( playerData ) -- Commencing message to player only. - if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + if playerData.difficulty ~= AIRBOSS.Difficulty.HARD then -- Text - local text="" + local text = "" -- Positive response. - if playerData.case==1 then - text=text.."Proceed to initial." + if playerData.case == 1 then + text = text .. "Proceed to initial." else - text=text.."Descent to platform." - if playerData.difficulty==AIRBOSS.Difficulty.EASY and playerData.showhints then - text=text.." VSI 4000 ft/min until you reach 5000 ft." + text = text .. "Descent to platform." + if playerData.difficulty == AIRBOSS.Difficulty.EASY and playerData.showhints then + text = text .. " VSI 4000 ft/min until you reach 5000 ft." end end -- Message to player. - self:MessageToPlayer(playerData, text, "MARSHAL") + self:MessageToPlayer( playerData, text, "MARSHAL" ) end -- Next step: depends on case recovery. local nextstep - if playerData.case==1 then + if playerData.case == 1 then -- CASE I: Player has to fly to the initial which is 3 NM DME astern of the boat. - nextstep=AIRBOSS.PatternStep.INITIAL + nextstep = AIRBOSS.PatternStep.INITIAL else -- CASE II/III: Player has to start the descent at 4000 ft/min to the platform at 5k ft. - nextstep=AIRBOSS.PatternStep.PLATFORM + nextstep = AIRBOSS.PatternStep.PLATFORM end -- Next step hint. - self:_SetPlayerStep(playerData, nextstep) + self:_SetPlayerStep( playerData, nextstep ) -- Commence section members as well but dont check the zone. - for i,_flight in pairs(playerData.section) do - local flight=_flight --#AIRBOSS.PlayerData - self:_Commencing(flight, false) + for i, _flight in pairs( playerData.section ) do + local flight = _flight -- #AIRBOSS.PlayerData + self:_Commencing( flight, false ) end end @@ -106710,40 +113190,40 @@ end -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. -- @return #boolean True if player is in the initial zone. -function AIRBOSS:_Initial(playerData) +function AIRBOSS:_Initial( playerData ) -- Check if player is in initial zone and entering the CASE I pattern. - local inzone=playerData.unit:IsInZone(self:_GetZoneInitial(playerData.case)) + local inzone = playerData.unit:IsInZone( self:_GetZoneInitial( playerData.case ) ) -- Relative heading to carrier direction. - local relheading=self:_GetRelativeHeading(playerData.unit, false) + local relheading = self:_GetRelativeHeading( playerData.unit, false ) -- Alitude of player in feet. - local altitude=playerData.unit:GetAltitude() + local altitude = playerData.unit:GetAltitude() -- Check if player is in zone and flying roughly in the right direction. - if inzone and math.abs(relheading)<60 and altitude<=self.initialmaxalt then + if inzone and math.abs( relheading ) < 60 and altitude <= self.initialmaxalt then -- Send message for normal and easy difficulty. if playerData.showhints then -- Inform player. - local hint=string.format("Initial") + local hint = string.format( "Initial" ) -- Hook down for students. - if playerData.difficulty==AIRBOSS.Difficulty.EASY and playerData.actype~=AIRBOSS.AircraftCarrier.AV8B then - if playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then - hint=hint.." - Hook down, SAS on, Wing Sweep 68°!" + if playerData.difficulty == AIRBOSS.Difficulty.EASY and playerData.actype ~= AIRBOSS.AircraftCarrier.AV8B then + if playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B then + hint = hint .. " - Hook down, SAS on, Wing Sweep 68°!" else - hint=hint.." - Hook down!" + hint = hint .. " - Hook down!" end end - self:MessageToPlayer(playerData, hint, "MARSHAL") + self:MessageToPlayer( playerData, hint, "MARSHAL" ) end -- Next step: Break entry. - self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.BREAKENTRY) + self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.BREAKENTRY ) return true end @@ -106754,24 +113234,24 @@ end --- Check if player is in CASE II/III approach corridor. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. -function AIRBOSS:_CheckCorridor(playerData) +function AIRBOSS:_CheckCorridor( playerData ) -- Check if player is in valid zone - local validzone=self:_GetZoneCorridor(playerData.case) + local validzone = self:_GetZoneCorridor( playerData.case ) -- Check if we are inside the moving zone. - local invalid=playerData.unit:IsNotInZone(validzone) + local invalid = playerData.unit:IsNotInZone( validzone ) -- Issue warning. if invalid and (not playerData.warning) then - self:MessageToPlayer(playerData, "you left the approach corridor!", "AIRBOSS") - playerData.warning=true + self:MessageToPlayer( playerData, "you left the approach corridor!", "AIRBOSS" ) + playerData.warning = true end -- Back in zone. if (not invalid) and playerData.warning then - self:MessageToPlayer(playerData, "you're back in the approach corridor.", "AIRBOSS") - playerData.warning=false + self:MessageToPlayer( playerData, "you're back in the approach corridor.", "AIRBOSS" ) + playerData.warning = false end end @@ -106779,60 +113259,59 @@ end --- Platform at 5k ft for case II/III recoveries. Descent at 2000 ft/min. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. -function AIRBOSS:_Platform(playerData) +function AIRBOSS:_Platform( playerData ) -- Check if player left or got back to the approach corridor. - self:_CheckCorridor(playerData) + self:_CheckCorridor( playerData ) -- Check if we are inside the moving zone. - local inzone=playerData.unit:IsInZone(self:_GetZonePlatform(playerData.case)) + local inzone = playerData.unit:IsInZone( self:_GetZonePlatform( playerData.case ) ) -- Check if we are in zone. if inzone then -- Hint for player about altitude, AoA etc. - self:_PlayerHint(playerData) + self:_PlayerHint( playerData ) -- Next step: depends. local nextstep - if math.abs(self.holdingoffset)>0 and playerData.case>1 then + if math.abs( self.holdingoffset ) > 0 and playerData.case > 1 then -- Turn to BRC (case II) or FB (case III). - nextstep=AIRBOSS.PatternStep.ARCIN + nextstep = AIRBOSS.PatternStep.ARCIN else - if playerData.case==2 then + if playerData.case == 2 then -- Case II: Initial zone then Case I recovery. - nextstep=AIRBOSS.PatternStep.INITIAL - elseif playerData.case==3 then + nextstep = AIRBOSS.PatternStep.INITIAL + elseif playerData.case == 3 then -- CASE III: Dirty up. - nextstep=AIRBOSS.PatternStep.DIRTYUP + nextstep = AIRBOSS.PatternStep.DIRTYUP end end -- Next step hint. - self:_SetPlayerStep(playerData, nextstep) + self:_SetPlayerStep( playerData, nextstep ) end end - --- Arc in turn for case II/III recoveries. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. -function AIRBOSS:_ArcInTurn(playerData) +function AIRBOSS:_ArcInTurn( playerData ) -- Check if player left or got back to the approach corridor. - self:_CheckCorridor(playerData) + self:_CheckCorridor( playerData ) -- Check if we are inside the moving zone. - local inzone=playerData.unit:IsInZone(self:_GetZoneArcIn(playerData.case)) + local inzone = playerData.unit:IsInZone( self:_GetZoneArcIn( playerData.case ) ) if inzone then -- Hint for player about altitude, AoA etc. - self:_PlayerHint(playerData) + self:_PlayerHint( playerData ) -- Next step: Arc Out Turn. - self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.ARCOUT) + self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.ARCOUT ) end end @@ -106840,62 +113319,62 @@ end --- Arc out turn for case II/III recoveries. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. -function AIRBOSS:_ArcOutTurn(playerData) +function AIRBOSS:_ArcOutTurn( playerData ) -- Check if player left or got back to the approach corridor. - self:_CheckCorridor(playerData) + self:_CheckCorridor( playerData ) -- Check if we are inside the moving zone. - local inzone=playerData.unit:IsInZone(self:_GetZoneArcOut(playerData.case)) + local inzone = playerData.unit:IsInZone( self:_GetZoneArcOut( playerData.case ) ) if inzone then -- Hint for player about altitude, AoA etc. - self:_PlayerHint(playerData) + self:_PlayerHint( playerData ) -- Next step: local nextstep - if playerData.case==3 then + if playerData.case == 3 then -- Case III: Dirty up. - nextstep=AIRBOSS.PatternStep.DIRTYUP + nextstep = AIRBOSS.PatternStep.DIRTYUP else -- Case II: Initial. - nextstep=AIRBOSS.PatternStep.INITIAL + nextstep = AIRBOSS.PatternStep.INITIAL end -- Next step hint. - self:_SetPlayerStep(playerData, nextstep) + self:_SetPlayerStep( playerData, nextstep ) end end --- Dirty up and level out at 1200 ft for case III recovery. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. -function AIRBOSS:_DirtyUp(playerData) +function AIRBOSS:_DirtyUp( playerData ) -- Check if player left or got back to the approach corridor. - self:_CheckCorridor(playerData) + self:_CheckCorridor( playerData ) -- Check if we are inside the moving zone. - local inzone=playerData.unit:IsInZone(self:_GetZoneDirtyUp(playerData.case)) + local inzone = playerData.unit:IsInZone( self:_GetZoneDirtyUp( playerData.case ) ) if inzone then -- Hint for player about altitude, AoA etc. - self:_PlayerHint(playerData) + self:_PlayerHint( playerData ) -- Radio call "Say/Fly needles". Delayed by 10/15 seconds. - if playerData.actype==AIRBOSS.AircraftCarrier.HORNET or playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then - local callsay=self:_NewRadioCall(self.MarshalCall.SAYNEEDLES, nil, nil, 5, playerData.onboard) - local callfly=self:_NewRadioCall(self.MarshalCall.FLYNEEDLES, nil, nil, 5, playerData.onboard) - self:RadioTransmission(self.MarshalRadio, callsay, false, 55, nil, true) - self:RadioTransmission(self.MarshalRadio, callfly, false, 60, nil, true) + if playerData.actype == AIRBOSS.AircraftCarrier.HORNET or playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B then + local callsay = self:_NewRadioCall( self.MarshalCall.SAYNEEDLES, nil, nil, 5, playerData.onboard ) + local callfly = self:_NewRadioCall( self.MarshalCall.FLYNEEDLES, nil, nil, 5, playerData.onboard ) + self:RadioTransmission( self.MarshalRadio, callsay, false, 55, nil, true ) + self:RadioTransmission( self.MarshalRadio, callfly, false, 60, nil, true ) end -- TODO: Make Fly Bullseye call if no automatic ICLS is active. -- Next step: CASE III: Intercept glide slope and follow bullseye (ICLS). - self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.BULLSEYE) + self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.BULLSEYE ) end end @@ -106904,32 +113383,34 @@ end -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. -- @return #boolean If true, player is in bullseye zone. -function AIRBOSS:_Bullseye(playerData) +function AIRBOSS:_Bullseye( playerData ) -- Check if player left or got back to the approach corridor. - self:_CheckCorridor(playerData) + self:_CheckCorridor( playerData ) -- Check if we are inside the moving zone. - local inzone=playerData.unit:IsInZone(self:_GetZoneBullseye(playerData.case)) + local inzone = playerData.unit:IsInZone( self:_GetZoneBullseye( playerData.case ) ) -- Relative heading to carrier direction of the runway. - local relheading=self:_GetRelativeHeading(playerData.unit, true) + local relheading = self:_GetRelativeHeading( playerData.unit, true ) -- Check if player is in zone and flying roughly in the right direction. - if inzone and math.abs(relheading)<60 then + if inzone and math.abs( relheading ) < 60 then -- Hint for player about altitude, AoA etc. - self:_PlayerHint(playerData) + self:_PlayerHint( playerData ) -- LSO expect spot 5 or 7.5 call - if playerData.actype==AIRBOSS.AircraftCarrier.AV8B and self.carriertype==AIRBOSS.CarrierType.JCARLOS then - self:RadioTransmission(self.LSORadio, self.LSOCall.EXPECTSPOT5, nil, nil, nil, true) - elseif playerData.actype==AIRBOSS.AircraftCarrier.AV8B then - self:RadioTransmission(self.LSORadio, self.LSOCall.EXPECTSPOT75, nil, nil, nil, true) + if playerData.actype == AIRBOSS.AircraftCarrier.AV8B and self.carriertype == AIRBOSS.CarrierType.JCARLOS then + self:RadioTransmission( self.LSORadio, self.LSOCall.EXPECTSPOT5, nil, nil, nil, true ) + elseif playerData.actype == AIRBOSS.AircraftCarrier.AV8B and self.carriertype == AIRBOSS.CarrierType.CANBERRA then + self:RadioTransmission( self.LSORadio, self.LSOCall.EXPECTSPOT5, nil, nil, nil, true ) + elseif playerData.actype == AIRBOSS.AircraftCarrier.AV8B then + self:RadioTransmission( self.LSORadio, self.LSOCall.EXPECTSPOT75, nil, nil, nil, true ) end -- Next step: Groove Call the ball. - self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_XX) + self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.GROOVE_XX ) end end @@ -106937,150 +113418,147 @@ end --- Bolter pattern. Sends player to abeam for Case I/II or Bullseye for Case III ops. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. -function AIRBOSS:_BolterPattern(playerData) +function AIRBOSS:_BolterPattern( playerData ) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local X, Z, rho, phi=self:_GetDistances(playerData.unit) + local X, Z, rho, phi = self:_GetDistances( playerData.unit ) -- Bolter Pattern thresholds. - local Bolter={} - Bolter.name="Bolter Pattern" - Bolter.Xmin=-UTILS.NMToMeters(5) -- Not more then 5 NM astern of boat. - Bolter.Xmax= UTILS.NMToMeters(3) -- Not more then 3 NM ahead of boat. - Bolter.Zmin=-UTILS.NMToMeters(5) -- Not more than 2 NM port. - Bolter.Zmax= UTILS.NMToMeters(1) -- Not more than 1 NM starboard. - Bolter.LimitXmin= 100 -- Check that 100 meter ahead and port - Bolter.LimitXmax= nil - Bolter.LimitZmin= nil - Bolter.LimitZmax= nil + local Bolter = {} + Bolter.name = "Bolter Pattern" + Bolter.Xmin = -UTILS.NMToMeters( 5 ) -- Not more then 5 NM astern of boat. + Bolter.Xmax = UTILS.NMToMeters( 3 ) -- Not more then 3 NM ahead of boat. + Bolter.Zmin = -UTILS.NMToMeters( 5 ) -- Not more than 2 NM port. + Bolter.Zmax = UTILS.NMToMeters( 1 ) -- Not more than 1 NM starboard. + Bolter.LimitXmin = 100 -- Check that 100 meter ahead and port + Bolter.LimitXmax = nil + Bolter.LimitZmin = nil + Bolter.LimitZmax = nil -- Check if we are in front of the boat (diffX > 0). - if self:_CheckLimits(X, Z, Bolter) then + if self:_CheckLimits( X, Z, Bolter ) then local nextstep - if playerData.case<3 then - nextstep=AIRBOSS.PatternStep.ABEAM + if playerData.case < 3 then + nextstep = AIRBOSS.PatternStep.ABEAM else - nextstep=AIRBOSS.PatternStep.BULLSEYE + nextstep = AIRBOSS.PatternStep.BULLSEYE end - self:_SetPlayerStep(playerData, nextstep) + self:_SetPlayerStep( playerData, nextstep ) end end - - --- Break entry for case I/II recoveries. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. -function AIRBOSS:_BreakEntry(playerData) +function AIRBOSS:_BreakEntry( playerData ) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local X, Z=self:_GetDistances(playerData.unit) + local X, Z = self:_GetDistances( playerData.unit ) -- Abort condition check. - if self:_CheckAbort(X, Z, self.BreakEntry) then - self:_AbortPattern(playerData, X, Z, self.BreakEntry, true) + if self:_CheckAbort( X, Z, self.BreakEntry ) then + self:_AbortPattern( playerData, X, Z, self.BreakEntry, true ) return end -- Check if we are in front of the boat (diffX > 0). - if self:_CheckLimits(X, Z, self.BreakEntry) then + if self:_CheckLimits( X, Z, self.BreakEntry ) then -- Hint for player about altitude, AoA etc. - self:_PlayerHint(playerData) + self:_PlayerHint( playerData ) -- Next step: Early Break. - self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.EARLYBREAK) + self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.EARLYBREAK ) end end - --- Break. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. -- @param #string part Part of the break. -function AIRBOSS:_Break(playerData, part) +function AIRBOSS:_Break( playerData, part ) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local X, Z=self:_GetDistances(playerData.unit) + local X, Z = self:_GetDistances( playerData.unit ) -- Early or late break. local breakpoint = self.BreakEarly - if part==AIRBOSS.PatternStep.LATEBREAK then + if part == AIRBOSS.PatternStep.LATEBREAK then breakpoint = self.BreakLate end -- Check abort conditions. - if self:_CheckAbort(X, Z, breakpoint) then - self:_AbortPattern(playerData, X, Z, breakpoint, true) + if self:_CheckAbort( X, Z, breakpoint ) then + self:_AbortPattern( playerData, X, Z, breakpoint, true ) return end -- Player made a very tight turn and did not trigger the latebreak threshold at 0.8 NM. - local tooclose=false - if part==AIRBOSS.PatternStep.LATEBREAK then - local close=0.8 - if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then - close=0.5 + local tooclose = false + if part == AIRBOSS.PatternStep.LATEBREAK then + local close = 0.8 + if playerData.actype == AIRBOSS.AircraftCarrier.AV8B then + close = 0.5 end - if X<0 and Z90 and self:_CheckLimits(X, Z, self.Wake) then + elseif relheading > 90 and self:_CheckLimits( X, Z, self.Wake ) then -- Message to player. - self:MessageToPlayer(playerData, "you are already at the wake and have not passed the 90. Turn faster next time!", "LSO") - self:RadioTransmission(self.LSORadio, self.LSOCall.DEPARTANDREENTER, nil, nil, nil, true) - playerData.wop=true + self:MessageToPlayer( playerData, "you are already at the wake and have not passed the 90. Turn faster next time!", "LSO" ) + self:RadioTransmission( self.LSORadio, self.LSOCall.DEPARTANDREENTER, nil, nil, nil, true ) + playerData.wop = true -- Debrief. - self:_AddToDebrief(playerData, "Overshoot at wake - Pattern Waveoff!") - self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.DEBRIEF) + self:_AddToDebrief( playerData, "Overshoot at wake - Pattern Waveoff!" ) + self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.DEBRIEF ) end end --- At the Wake. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. -function AIRBOSS:_Wake(playerData) +function AIRBOSS:_Wake( playerData ) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local X, Z=self:_GetDistances(playerData.unit) + local X, Z = self:_GetDistances( playerData.unit ) -- Check abort conditions. - if self:_CheckAbort(X, Z, self.Wake) then - self:_AbortPattern(playerData, X, Z, self.Wake, true) + if self:_CheckAbort( X, Z, self.Wake ) then + self:_AbortPattern( playerData, X, Z, self.Wake, true ) return end -- Right behind the wake of the carrier dZ>0. - if self:_CheckLimits(X, Z, self.Wake) then + if self:_CheckLimits( X, Z, self.Wake ) then -- Hint for player about altitude, AoA etc. - self:_PlayerHint(playerData) + self:_PlayerHint( playerData ) -- Next step: Final. - self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.FINAL) + self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.FINAL ) end end @@ -107194,53 +113675,53 @@ end -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. -- @return #AIRBOSS.GrooveData Groove data table. -function AIRBOSS:_GetGrooveData(playerData) +function AIRBOSS:_GetGrooveData( playerData ) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier). - local X, Z=self:_GetDistances(playerData.unit) + local X, Z = self:_GetDistances( playerData.unit ) -- Stern position at the rundown. - local stern=self:_GetSternCoord() + local stern = self:_GetSternCoord() -- Distance from rundown to player aircraft. - local rho=stern:Get2DDistance(playerData.unit:GetCoordinate()) + local rho = stern:Get2DDistance( playerData.unit:GetCoordinate() ) -- Aircraft is behind the carrier. - local astern=X5. This would mean the player has not turned in correctly! -- Groove data. - playerData.groove.X0=UTILS.DeepCopy(groovedata) + playerData.groove.X0 = UTILS.DeepCopy( groovedata ) -- Set time stamp. Next call in 4 seconds. - playerData.Tlso=timer.getTime() + playerData.Tlso = timer.getTime() -- Next step: X start. - self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_XX) + self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.GROOVE_XX ) end -- Groovedata step. - groovedata.Step=playerData.step + groovedata.Step = playerData.step end --- In the groove. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. -function AIRBOSS:_Groove(playerData) +function AIRBOSS:_Groove( playerData ) -- Ranges in the groove. - local RX0=UTILS.NMToMeters(1.000) -- Everything before X 1.00 = 1852 m - local RXX=UTILS.NMToMeters(0.750) -- Start of groove. 0.75 = 1389 m - local RIM=UTILS.NMToMeters(0.500) -- In the Middle 0.50 = 926 m (middle one third of the glideslope) - local RIC=UTILS.NMToMeters(0.250) -- In Close 0.25 = 463 m (last one third of the glideslope) - local RAR=UTILS.NMToMeters(0.040) -- At the Ramp. 0.04 = 75 m + local RX0 = UTILS.NMToMeters( 1.000 ) -- Everything before X 1.00 = 1852 m + local RXX = UTILS.NMToMeters( 0.750 ) -- Start of groove. 0.75 = 1389 m + local RIM = UTILS.NMToMeters( 0.500 ) -- In the Middle 0.50 = 926 m (middle one third of the glideslope) + local RIC = UTILS.NMToMeters( 0.250 ) -- In Close 0.25 = 463 m (last one third of the glideslope) + local RAR = UTILS.NMToMeters( 0.040 ) -- At the Ramp. 0.04 = 75 m -- Groove data. - local groovedata=self:_GetGrooveData(playerData) + local groovedata = self:_GetGrooveData( playerData ) -- Add data to trapsheet. - table.insert(playerData.trapsheet, groovedata) + table.insert( playerData.trapsheet, groovedata ) -- Coords. - local X=groovedata.X - local Z=groovedata.Z + local X = groovedata.X + local Z = groovedata.Z -- Check abort conditions. - if self:_CheckAbort(groovedata.X, groovedata.Z, self.Groove) then - self:_AbortPattern(playerData, groovedata.X, groovedata.Z, self.Groove, true) + if self:_CheckAbort( groovedata.X, groovedata.Z, self.Groove ) then + self:_AbortPattern( playerData, groovedata.X, groovedata.Z, self.Groove, true ) return end -- Shortcuts. - local rho=groovedata.Rho - local lineupError=groovedata.LUE - local glideslopeError=groovedata.GSE - local AoA=groovedata.AoA + local rho = groovedata.Rho + local lineupError = groovedata.LUE + local glideslopeError = groovedata.GSE + local AoA = groovedata.AoA - - if rho<=RXX and playerData.step==AIRBOSS.PatternStep.GROOVE_XX and (math.abs(groovedata.Roll)<=4.0 or playerData.unit:IsInZone(self:_GetZoneLineup())) then + if rho <= RXX and playerData.step == AIRBOSS.PatternStep.GROOVE_XX and (math.abs( groovedata.Roll ) <= 4.0 or playerData.unit:IsInZone( self:_GetZoneLineup() )) then -- Start time in groove - playerData.TIG0=timer.getTime() + playerData.TIG0 = timer.getTime() -- LSO "Call the ball" call. - self:RadioTransmission(self.LSORadio, self.LSOCall.CALLTHEBALL, nil, nil, nil, true) - playerData.Tlso=timer.getTime() + self:RadioTransmission( self.LSORadio, self.LSOCall.CALLTHEBALL, nil, nil, nil, true ) + playerData.Tlso = timer.getTime() -- Pilot "405, Hornet Ball, 3.2". -- LSO "Roger ball" call in three seconds. - self:RadioTransmission(self.LSORadio, self.LSOCall.ROGERBALL, false, nil, 2, true) + self:RadioTransmission( self.LSORadio, self.LSOCall.ROGERBALL, false, nil, 2, true ) -- Store data. - playerData.groove.XX=UTILS.DeepCopy(groovedata) + playerData.groove.XX = UTILS.DeepCopy( groovedata ) -- This is a valid approach and player did not miss any important steps in the pattern. - playerData.valid=true + playerData.valid = true -- Next step: in the middle. - self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_IM) + self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.GROOVE_IM ) - elseif rho<=RIM and playerData.step==AIRBOSS.PatternStep.GROOVE_IM then + elseif rho <= RIM and playerData.step == AIRBOSS.PatternStep.GROOVE_IM then -- Store data. - playerData.groove.IM=UTILS.DeepCopy(groovedata) + playerData.groove.IM = UTILS.DeepCopy( groovedata ) -- Next step: in close. - self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_IC) + self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.GROOVE_IC ) - elseif rho<=RIC and playerData.step==AIRBOSS.PatternStep.GROOVE_IC then + elseif rho <= RIC and playerData.step == AIRBOSS.PatternStep.GROOVE_IC then -- Store data. - playerData.groove.IC=UTILS.DeepCopy(groovedata) + playerData.groove.IC = UTILS.DeepCopy( groovedata ) -- Next step: AR at the ramp. - self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_AR) + self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.GROOVE_AR ) - elseif rho<=RAR and playerData.step==AIRBOSS.PatternStep.GROOVE_AR then + elseif rho <= RAR and playerData.step == AIRBOSS.PatternStep.GROOVE_AR then -- Store data. - playerData.groove.AR=UTILS.DeepCopy(groovedata) + playerData.groove.AR = UTILS.DeepCopy( groovedata ) -- Next step: in the wires. - if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then - self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_AL) + if playerData.actype == AIRBOSS.AircraftCarrier.AV8B then + self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.GROOVE_AL ) else - self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_IW) + self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.GROOVE_IW ) end - elseif rho<=RAR and playerData.step==AIRBOSS.PatternStep.GROOVE_AL then + elseif rho <= RAR and playerData.step == AIRBOSS.PatternStep.GROOVE_AL then -- Store data. - playerData.groove.AL=UTILS.DeepCopy(groovedata) + playerData.groove.AL = UTILS.DeepCopy( groovedata ) -- Get zone abeam LDG spot. - local ZoneALS=self:_GetZoneAbeamLandingSpot() + local ZoneALS = self:_GetZoneAbeamLandingSpot() -- Get player velocity in km/h. - local vplayer=playerData.unit:GetVelocityKMH() + local vplayer = playerData.unit:GetVelocityKMH() -- Get carrier velocity in km/h. - local vcarrier=self.carrier:GetVelocityKMH() + local vcarrier = self.carrier:GetVelocityKMH() -- Speed difference. - local dv=math.abs(vplayer-vcarrier) + local dv = math.abs( vplayer - vcarrier ) - -- Stable when speed difference < 10 km/h. - local stable=dv<10 + + -- Stable when speed difference < 30 km/h.(16 Kts)Pene Testing + local stable=dv<30 -- Check if player is inside the zone. - if playerData.unit:IsInZone(ZoneALS) and stable then + if playerData.unit:IsInZone( ZoneALS ) and stable then -- Radio Transmission "Cleared to land" once the aircraft is inside the zone. - self:RadioTransmission(self.LSORadio, self.LSOCall.CLEAREDTOLAND, nil, nil, nil, true) + self:RadioTransmission( self.LSORadio, self.LSOCall.CLEAREDTOLAND, nil, nil, nil, true ) -- Next step: Level cross. - self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_LC) + self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.GROOVE_LC ) + -- Set Stable Hover + playerData.stable = true + playerData.hover = true end - elseif rho<=RAR and playerData.step==AIRBOSS.PatternStep.GROOVE_LC then + elseif rho <= RAR and playerData.step == AIRBOSS.PatternStep.GROOVE_LC then -- Store data. - playerData.groove.LC=UTILS.DeepCopy(groovedata) + playerData.groove.LC = UTILS.DeepCopy( groovedata ) -- Get zone primary LDG spot. - local ZoneLS=self:_GetZoneLandingSpot() + local ZoneLS = self:_GetZoneLandingSpot() -- Get player velocity in km/h. - local vplayer=playerData.unit:GetVelocityKMH() + local vplayer = playerData.unit:GetVelocityKMH() -- Get carrier velocity in km/h. - local vcarrier=self.carrier:GetVelocityKMH() + local vcarrier = self.carrier:GetVelocityKMH() -- Speed difference. - local dv=math.abs(vplayer-vcarrier) + local dv = math.abs( vplayer - vcarrier ) - -- Stable when v<7.5 km/h. - local stable=dv<7.5 + -- Stable when v<15 km/h. + local stable=dv<15 - -- Radio Transmission "Cleared to land" once the aircraft is inside the zone. - if playerData.unit:IsInZone(ZoneLS) and stable and playerData.warning==false then - self:RadioTransmission(self.LSORadio, self.LSOCall.STABILIZED, nil, nil, nil, true) - playerData.warning=true + -- Radio Transmission "Stabilized" once the aircraft has been cleared to cross and is over the Landing Spot and stable. + if playerData.unit:IsInZone( ZoneLS ) and stable and playerData.stable == true then + self:RadioTransmission( self.LSORadio, self.LSOCall.STABILIZED, nil, nil, nil, false ) + playerData.stable = false + playerData.warning = true end -- We keep it in this step until landed. @@ -107451,110 +113936,128 @@ function AIRBOSS:_Groove(playerData) -------------- -- Between IC and AR check for wave off. - if rho>=RAR and rho<=RIC and not playerData.waveoff then + if rho >= RAR and rho <= RIC and not playerData.waveoff then -- Check if player should wave off. - local waveoff=self:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) + local waveoff = self:_CheckWaveOff( glideslopeError, lineupError, AoA, playerData ) -- Let's see.. if waveoff then -- Debug info. - self:T3(self.lid..string.format("Waveoff distance rho=%.1f m", rho)) + self:T3( self.lid .. string.format( "Waveoff distance rho=%.1f m", rho ) ) -- LSO Wave off! - self:RadioTransmission(self.LSORadio, self.LSOCall.WAVEOFF, nil, nil, nil, true) - playerData.Tlso=timer.getTime() + self:RadioTransmission( self.LSORadio, self.LSOCall.WAVEOFF, nil, nil, nil, true ) + playerData.Tlso = timer.getTime() -- Player was waved off! - playerData.waveoff=true + playerData.waveoff = true -- Nothing else necessary. return end - end + end + + -- Long V/STOL groove time Wave Off over 75 seconds to IC - TOPGUN level Only. --pene testing (WIP)--- Need to think more about this. + + --if rho>=RAR and rho<=RIC and not playerData.waveoff and playerData.difficulty==AIRBOSS.Difficulty.HARD and playerData.actype== AIRBOSS.AircraftCarrier.AV8B then + -- Get groove time + --local vSlow=groovedata.time + -- If too slow wave off. + --if vSlow >75 then + + -- LSO Wave off! + --self:RadioTransmission(self.LSORadio, self.LSOCall.WAVEOFF, nil, nil, nil, true) + --playerData.Tlso=timer.getTime() + + -- Player was waved Off + --playerData.waveoff=true + --return + --end + --end -- Groovedata step. - groovedata.Step=playerData.step + groovedata.Step = playerData.step ----------------- -- Groove Data -- ----------------- -- Check if we are beween 3/4 NM and end of ship. - if rho>=RAR and rho= RAR and rho < RX0 and playerData.waveoff == false then -- Get groove step short hand of the previous step. - local gs=self:_GS(playerData.step, -1) + local gs = self:_GS( playerData.step, -1 ) -- Get current groove data. - local gd=playerData.groove[gs] --#AIRBOSS.GrooveData + local gd = playerData.groove[gs] -- #AIRBOSS.GrooveData if gd then - self:T3(gd) + self:T3( gd ) -- Distance in NM. - local d=UTILS.MetersToNM(rho) + local d = UTILS.MetersToNM( rho ) - -- Drift on lineup. - if rho>=RAR and rho<=RIM then - if gd.LUE>0.22 and lineupError<-0.22 then - env.info" Drift Right across centre ==> DR-" - gd.Drift=" DR" - self:T(self.lid..string.format("Got Drift Right across centre step %s, d=%.3f: Max LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError)) - elseif gd.LUE<-0.22 and lineupError>0.22 then - env.info" Drift Left ==> DL-" - gd.Drift=" DL" - self:T(self.lid..string.format("Got Drift Left across centre at step %s, d=%.3f: Min LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError)) - elseif gd.LUE>0.13 and lineupError<-0.14 then - env.info" Little Drift Right across centre ==> (DR-)" - gd.Drift=" (DR)" - self:T(self.lid..string.format("Got Little Drift Right across centre at step %s, d=%.3f: Max LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError)) - elseif gd.LUE<-0.13 and lineupError>0.14 then - env.info" Little Drift Left across centre ==> (DL-)" - gd.Drift=" (DL)" - self:E(self.lid..string.format("Got Little Drift Left across centre at step %s, d=%.3f: Min LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError)) - end + -- Drift on lineup. + if rho >= RAR and rho <= RIM then + if gd.LUE > 0.22 and lineupError < -0.22 then + env.info " Drift Right across centre ==> DR-" + gd.Drift = " DR" + self:T( self.lid .. string.format( "Got Drift Right across centre step %s, d=%.3f: Max LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError ) ) + elseif gd.LUE < -0.22 and lineupError > 0.22 then + env.info " Drift Left ==> DL-" + gd.Drift = " DL" + self:T( self.lid .. string.format( "Got Drift Left across centre at step %s, d=%.3f: Min LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError ) ) + elseif gd.LUE > 0.13 and lineupError < -0.14 then + env.info " Little Drift Right across centre ==> (DR-)" + gd.Drift = " (DR)" + self:T( self.lid .. string.format( "Got Little Drift Right across centre at step %s, d=%.3f: Max LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError ) ) + elseif gd.LUE < -0.13 and lineupError > 0.14 then + env.info " Little Drift Left across centre ==> (DL-)" + gd.Drift = " (DL)" + self:E( self.lid .. string.format( "Got Little Drift Left across centre at step %s, d=%.3f: Min LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError ) ) + end end -- Update max deviation of line up error. - if math.abs(lineupError)>math.abs(gd.LUE) then - self:T(self.lid..string.format("Got bigger LUE at step %s, d=%.3f: LUE %.3f>%.3f", gs, d, lineupError, gd.LUE)) - gd.LUE=lineupError + if math.abs( lineupError ) > math.abs( gd.LUE ) then + self:T( self.lid .. string.format( "Got bigger LUE at step %s, d=%.3f: LUE %.3f>%.3f", gs, d, lineupError, gd.LUE ) ) + gd.LUE = lineupError end -- Fly through good window of glideslope. - if gd.GSE>0.4 and glideslopeError<-0.3 then + if gd.GSE > 0.4 and glideslopeError < -0.3 then -- Fly through down ==> "\" - gd.FlyThrough="\\" - self:T(self.lid..string.format("Got Fly through DOWN at step %s, d=%.3f: Max GSE=%.3f, lower GSE=%.3f", gs, d, gd.GSE, glideslopeError)) - elseif gd.GSE<-0.3 and glideslopeError>0.4 then + gd.FlyThrough = "\\" + self:T( self.lid .. string.format( "Got Fly through DOWN at step %s, d=%.3f: Max GSE=%.3f, lower GSE=%.3f", gs, d, gd.GSE, glideslopeError ) ) + elseif gd.GSE < -0.3 and glideslopeError > 0.4 then -- Fly through up ==> "/" - gd.FlyThrough="/" - self:E(self.lid..string.format("Got Fly through UP at step %s, d=%.3f: Min GSE=%.3f, lower GSE=%.3f", gs, d, gd.GSE, glideslopeError)) + gd.FlyThrough = "/" + self:E( self.lid .. string.format( "Got Fly through UP at step %s, d=%.3f: Min GSE=%.3f, lower GSE=%.3f", gs, d, gd.GSE, glideslopeError ) ) end -- Update max deviation of glideslope error. - if math.abs(glideslopeError)>math.abs(gd.GSE) then - self:T(self.lid..string.format("Got bigger GSE at step %s, d=%.3f: GSE |%.3f|>|%.3f|", gs, d, glideslopeError, gd.GSE)) - gd.GSE=glideslopeError + if math.abs( glideslopeError ) > math.abs( gd.GSE ) then + self:T( self.lid .. string.format( "Got bigger GSE at step %s, d=%.3f: GSE |%.3f|>|%.3f|", gs, d, glideslopeError, gd.GSE ) ) + gd.GSE = glideslopeError end -- Get aircraft AoA parameters. - local aircraftaoa=self:_GetAircraftAoA(playerData) + local aircraftaoa = self:_GetAircraftAoA( playerData ) -- On Speed AoA. - local aoaopt=aircraftaoa.OnSpeed + local aoaopt = aircraftaoa.OnSpeed -- Compare AoAs wrt on speed AoA and update max deviation. - if math.abs(AoA-aoaopt)>math.abs(gd.AoA-aoaopt) then - self:T(self.lid..string.format("Got bigger AoA error at step %s, d=%.3f: AoA %.3f>%.3f.", gs, d, AoA, gd.AoA)) - gd.AoA=AoA + if math.abs( AoA - aoaopt ) > math.abs( gd.AoA - aoaopt ) then + self:T( self.lid .. string.format( "Got bigger AoA error at step %s, d=%.3f: AoA %.3f>%.3f.", gs, d, AoA, gd.AoA ) ) + gd.AoA = AoA end - --local gs2=self:_GS(groovedata.Step, -1) - --env.info(string.format("groovestep %s %s d=%.3f NM: GSE=%.3f %.3f, LUE=%.3f %.3f, AoA=%.3f %.3f", gs, gs2, d, groovedata.GSE, gd.GSE, groovedata.LUE, gd.LUE, groovedata.AoA, gd.AoA)) + -- local gs2=self:_GS(groovedata.Step, -1) + -- env.info(string.format("groovestep %s %s d=%.3f NM: GSE=%.3f %.3f, LUE=%.3f %.3f, AoA=%.3f %.3f", gs, gs2, d, groovedata.GSE, gd.GSE, groovedata.LUE, gd.LUE, groovedata.AoA, gd.AoA)) end @@ -107563,17 +114066,17 @@ function AIRBOSS:_Groove(playerData) --------------- -- Time since last LSO call. - local deltaT=timer.getTime()-playerData.Tlso + local deltaT = timer.getTime() - playerData.Tlso -- Wait until player passed the 0.75 NM distance. - local _advice=true - if playerData.TIG0==nil and playerData.difficulty~=AIRBOSS.Difficulty.EASY then --rho>RXX - _advice=false + local _advice = true + if playerData.TIG0 == nil and playerData.difficulty ~= AIRBOSS.Difficulty.EASY then -- rho>RXX + _advice = false end -- LSO call if necessary. - if deltaT>=self.LSOdT and _advice then - self:_LSOadvice(playerData, glideslopeError, lineupError) + if deltaT >= self.LSOdT and _advice then + self:_LSOadvice( playerData, glideslopeError, lineupError ) end end @@ -107583,37 +114086,37 @@ function AIRBOSS:_Groove(playerData) ---------------------------------------------------------- -- Player infront of the carrier X>~77 m. - if X>self.carrierparam.totlength+self.carrierparam.sterndist then + if X > self.carrierparam.totlength + self.carrierparam.sterndist then if playerData.waveoff then if playerData.landed then -- This should not happen because landing event was triggered. - self:_AddToDebrief(playerData, "You were waved off but landed anyway. Airboss wants to talk to you!") + self:_AddToDebrief( playerData, "You were waved off but landed anyway. Airboss wants to talk to you!" ) else - self:_AddToDebrief(playerData, "You were waved off.") + self:_AddToDebrief( playerData, "You were waved off." ) end elseif playerData.boltered then -- This should not happen because landing event was triggered. - self:_AddToDebrief(playerData, "You boltered.") + self:_AddToDebrief( playerData, "You boltered." ) else -- This should not happen. - self:T("Player was not waved off but flew past the carrier without landing ==> Own wave off!") + self:T( "Player was not waved off but flew past the carrier without landing ==> Own wave off!" ) -- We count this as OWO. - self:_AddToDebrief(playerData, "Own waveoff.") + self:_AddToDebrief( playerData, "Own waveoff." ) -- Set Owo - playerData.owo=true + playerData.owo = true end - -- Next step: debrief. - self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.DEBRIEF) + -- Next step: debrief. + self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.DEBRIEF ) end @@ -107631,61 +114134,62 @@ end -- @param #number AoA Angle of attack of player aircraft. -- @param #AIRBOSS.PlayerData playerData Player data. -- @return #boolean If true, player should wave off! -function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) +function AIRBOSS:_CheckWaveOff( glideslopeError, lineupError, AoA, playerData ) -- Assume we're all good. - local waveoff=false + local waveoff = false -- Parameters - local glMax= 1.8 - local glMin=-1.2 - local luAbs= 3.0 + local glMax = 1.8 + local glMin = -1.2 + local luAbs = 3.0 -- For the harrier, we allow a bit more room. - if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then - glMax= 4.0 - glMin=-3.0 - luAbs= 5.0 - -- No waveoff for harrier pilots at the moment. - return false + if playerData.actype == AIRBOSS.AircraftCarrier.AV8B then + glMax = 2.6 + glMin = -2.2 -- Testing, @Engines may be just dragging it in on Hermes, or the carrier parameters need adjusting. + luAbs = 4.1 -- Testing Pene. + end -- Too high or too low? - if glideslopeError>glMax then - local text=string.format("\n- Waveoff due to glideslope error %.2f > %.1f degrees!", glideslopeError, glMax) - self:T(self.lid..string.format("%s: %s", playerData.name, text)) - self:_AddToDebrief(playerData, text) - waveoff=true - elseif glideslopeError glMax then + local text = string.format( "\n- Waveoff due to glideslope error %.2f > %.1f degrees!", glideslopeError, glMax ) + self:T( self.lid .. string.format( "%s: %s", playerData.name, text ) ) + self:_AddToDebrief( playerData, text ) + waveoff = true + elseif glideslopeError < glMin then + local text = string.format( "\n- Waveoff due to glideslope error %.2f < %.1f degrees!", glideslopeError, glMin ) + self:T( self.lid .. string.format( "%s: %s", playerData.name, text ) ) + self:_AddToDebrief( playerData, text ) + waveoff = true end -- Too far from centerline? - if math.abs(lineupError)>luAbs then - local text=string.format("\n- Waveoff due to line up error |%.1f| > %.1f degrees!", lineupError, luAbs) - self:T(self.lid..string.format("%s: %s", playerData.name, text)) - self:_AddToDebrief(playerData, text) - waveoff=true + if math.abs( lineupError ) > luAbs then + local text = string.format( "\n- Waveoff due to line up error |%.1f| > %.1f degrees!", lineupError, luAbs ) + self:T( self.lid .. string.format( "%s: %s", playerData.name, text ) ) + self:_AddToDebrief( playerData, text ) + waveoff = true end - -- Too slow or too fast? Only for pros. - if playerData.difficulty==AIRBOSS.Difficulty.HARD then - -- Get aircraft specific AoA values - local aoaac=self:_GetAircraftAoA(playerData) + -- Too slow or too fast? Only for pros. + + if playerData.difficulty == AIRBOSS.Difficulty.HARD and playerData.actype ~= AIRBOSS.AircraftCarrier.AV8B then + -- Get aircraft specific AoA values. Not for AV-8B due to transition to Stable Hover. + local aoaac = self:_GetAircraftAoA( playerData ) -- Check too slow or too fast. - if AoAaoaac.SLOW then - local text=string.format("\n- Waveoff due to AoA %.1f > %.1f!", AoA, aoaac.SLOW) - self:T(self.lid..string.format("%s: %s", playerData.name, text)) - self:_AddToDebrief(playerData, text) - waveoff=true + if AoA < aoaac.FAST then + local text = string.format( "\n- Waveoff due to AoA %.1f < %.1f!", AoA, aoaac.FAST ) + self:T( self.lid .. string.format( "%s: %s", playerData.name, text ) ) + self:_AddToDebrief( playerData, text ) + waveoff = true + elseif AoA > aoaac.SLOW then + local text = string.format( "\n- Waveoff due to AoA %.1f > %.1f!", AoA, aoaac.SLOW ) + self:T( self.lid .. string.format( "%s: %s", playerData.name, text ) ) + self:_AddToDebrief( playerData, text ) + waveoff = true + end end @@ -107696,107 +114200,104 @@ end -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. -- @return boolean If true, we have a foul deck. -function AIRBOSS:_CheckFoulDeck(playerData) +function AIRBOSS:_CheckFoulDeck( playerData ) -- Assume no check necessary. - local check=false + local check = false -- CVN: Check at IM and IC. - if playerData.step==AIRBOSS.PatternStep.GROOVE_IM or - playerData.step==AIRBOSS.PatternStep.GROOVE_IC then - check=true + if playerData.step == AIRBOSS.PatternStep.GROOVE_IM or playerData.step == AIRBOSS.PatternStep.GROOVE_IC then + check = true end -- AV-8B check until - if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then - if playerData.step==AIRBOSS.PatternStep.GROOVE_AR or - playerData.step==AIRBOSS.PatternStep.GROOVE_AL then - check=true + if playerData.actype == AIRBOSS.AircraftCarrier.AV8B then + if playerData.step == AIRBOSS.PatternStep.GROOVE_AR or playerData.step == AIRBOSS.PatternStep.GROOVE_AL then + check = true end end -- Check if player was already waved off. Should not be necessary as player step is set to debrief afterwards! - if playerData.wofd==true or check==false then + if playerData.wofd == true or check == false then -- Player was already waved off. return end -- Landing runway zone. - local runway=self:_GetZoneRunwayBox() + local runway = self:_GetZoneRunwayBox() -- For AB-8B we just check the primary landing spot. - if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then - runway=self:_GetZoneLandingSpot() + if playerData.actype == AIRBOSS.AircraftCarrier.AV8B then + runway = self:_GetZoneLandingSpot() end -- Scan radius. - local R=250 + local R = 250 -- Debug info. - self:T(self.lid..string.format("Foul deck check: Scanning Carrier Runway Area. Radius=%.1f m.", R)) + self:T( self.lid .. string.format( "Foul deck check: Scanning Carrier Runway Area. Radius=%.1f m.", R ) ) -- Scan units in carrier zone. - local _,_,_,unitscan=self:GetCoordinate():ScanObjects(R, true, false, false) + local _, _, _, unitscan = self:GetCoordinate():ScanObjects( R, true, false, false ) -- Loop over all scanned units and check if they are on the runway. - local fouldeck=false - local foulunit=nil --Wrapper.Unit#UNIT - for _,_unit in pairs(unitscan) do - local unit=_unit --Wrapper.Unit#UNIT + local fouldeck = false + local foulunit = nil -- Wrapper.Unit#UNIT + for _, _unit in pairs( unitscan ) do + local unit = _unit -- Wrapper.Unit#UNIT -- Check if unit is in zone. - local inzone=unit:IsInZone(runway) + local inzone = unit:IsInZone( runway ) -- Check if aircraft and in air. - local isaircraft=unit:IsAir() - local isairborn =unit:InAir() + local isaircraft = unit:IsAir() + local isairborn = unit:InAir() if inzone and isaircraft and not isairborn then - local text=string.format("Unit %s on landing runway ==> Foul deck!", unit:GetName()) - self:T(self.lid..text) - MESSAGE:New(text, 10):ToAllIf(self.Debug) + local text = string.format( "Unit %s on landing runway ==> Foul deck!", unit:GetName() ) + self:T( self.lid .. text ) + MESSAGE:New( text, 10 ):ToAllIf( self.Debug ) if self.Debug then - runway:FlareZone(FLARECOLOR.Red, 30) + runway:FlareZone( FLARECOLOR.Red, 30 ) end - fouldeck=true - foulunit=unit + fouldeck = true + foulunit = unit end end - -- Add to debrief and if playerData and fouldeck then -- Debrief text. - local text=string.format("Foul deck waveoff due to aircraft %s!", foulunit:GetName()) - self:T(self.lid..string.format("%s: %s", playerData.name, text)) - self:_AddToDebrief(playerData, text) + local text = string.format( "Foul deck waveoff due to aircraft %s!", foulunit:GetName() ) + self:T( self.lid .. string.format( "%s: %s", playerData.name, text ) ) + self:_AddToDebrief( playerData, text ) -- Foul deck + wave off radio message. - self:RadioTransmission(self.LSORadio, self.LSOCall.FOULDECK, false, 1) - self:RadioTransmission(self.LSORadio, self.LSOCall.WAVEOFF, false, 1.2, nil, true) + self:RadioTransmission( self.LSORadio, self.LSOCall.FOULDECK, false, 1 ) + self:RadioTransmission( self.LSORadio, self.LSOCall.WAVEOFF, false, 1.2, nil, true ) -- Player hint for flight students. if playerData.showhints then - local text=string.format("overfly landing area and enter bolter pattern.") - self:MessageToPlayer(playerData, text, "LSO", nil, nil, false, 3) + local text = string.format( "overfly landing area and enter bolter pattern." ) + self:MessageToPlayer( playerData, text, "LSO", nil, nil, false, 3 ) end -- Set player parameters for foul deck. - playerData.wofd=true + playerData.wofd = true -- Debrief. - playerData.step=AIRBOSS.PatternStep.DEBRIEF - playerData.warning=nil + playerData.step = AIRBOSS.PatternStep.DEBRIEF + playerData.warning = nil -- Pass would be invalid if the player lands. - playerData.valid=false + playerData.valid = false -- Send a message to the player that blocks the runway. if foulunit then - local foulflight=self:_GetFlightFromGroupInQueue(foulunit:GetGroup(), self.flights) + local foulflight = self:_GetFlightFromGroupInQueue( foulunit:GetGroup(), self.flights ) if foulflight and not foulflight.ai then - self:MessageToPlayer(foulflight, "move your ass from my runway. NOW!", "AIRBOSS") + self:MessageToPlayer( foulflight, "move your ass from my runway. NOW!", "AIRBOSS" ) end end end @@ -107810,32 +114311,38 @@ end function AIRBOSS:_GetSternCoord() -- Heading of carrier (true). - local hdg=self.carrier:GetHeading() + local hdg = self.carrier:GetHeading() -- Final bearing (true). local FB=self:GetFinalBearing() + local case=self.case -- Stern coordinate (sterndist<0). Also translate 10 meters starboard wrt Final bearing. - self.sterncoord:UpdateFromCoordinate(self:GetCoordinate()) - --local stern=self:GetCoordinate() + self.sterncoord:UpdateFromCoordinate( self:GetCoordinate() ) + -- local stern=self:GetCoordinate() - -- Stern coordinate (sterndist<0). - if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then - -- Tarawa: Translate 8 meters port. + -- Stern coordinate (sterndist<0). --Pene testing Case III + if self.carriertype==AIRBOSS.CarrierType.HERMES or self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS or self.carriertype==AIRBOSS.CarrierType.CANBERRA then + if case==3 then + -- CASE III V/STOL translation Due over deck approach if needed. self.sterncoord:Translate(self.carrierparam.sterndist, hdg, true, true):Translate(8, FB-90, true, true) + elseif case==2 or case==1 then + -- V/Stol: Translate 8 meters port. + self.sterncoord:Translate(self.carrierparam.sterndist, hdg, true, true):Translate(8, FB-90, true, true) + end elseif self.carriertype==AIRBOSS.CarrierType.STENNIS then -- Stennis: translate 7 meters starboard wrt Final bearing. - self.sterncoord:Translate(self.carrierparam.sterndist, hdg, true, true):Translate(7, FB+90, true, true) - elseif self.carriertype==AIRBOSS.CarrierType.FORRESTAL then + self.sterncoord:Translate( self.carrierparam.sterndist, hdg, true, true ):Translate( 7, FB + 90, true, true ) + elseif self.carriertype == AIRBOSS.CarrierType.FORRESTAL then -- Forrestal - self.sterncoord:Translate(self.carrierparam.sterndist, hdg, true, true):Translate(7.5, FB+90, true, true) + self.sterncoord:Translate( self.carrierparam.sterndist, hdg, true, true ):Translate( 7.5, FB + 90, true, true ) else -- Nimitz SC: translate 8 meters starboard wrt Final bearing. - self.sterncoord:Translate(self.carrierparam.sterndist, hdg, true, true):Translate(9.5, FB+90, true, true) + self.sterncoord:Translate( self.carrierparam.sterndist, hdg, true, true ):Translate( 9.5, FB + 90, true, true ) end -- Set altitude. - self.sterncoord:SetAltitude(self.carrierparam.deckheight) + self.sterncoord:SetAltitude( self.carrierparam.deckheight ) return self.sterncoord end @@ -107845,73 +114352,74 @@ end -- @param Core.Point#COORDINATE Lcoord Landing position. -- @param #number dc Distance correction. Shift the landing coord back if dc>0 and forward if dc<0. -- @return #number Trapped wire (1-4) or 99 if no wire was trapped. -function AIRBOSS:_GetWire(Lcoord, dc) +function AIRBOSS:_GetWire( Lcoord, dc ) -- Final bearing (true). - local FB=self:GetFinalBearing() + local FB = self:GetFinalBearing() -- Stern coordinate (sterndist<0). Also translate 10 meters starboard wrt Final bearing. - local Scoord=self:_GetSternCoord() + local Scoord = self:_GetSternCoord() -- Distance to landing coord. - local Ldist=Lcoord:Get2DDistance(Scoord) + local Ldist = Lcoord:Get2DDistance( Scoord ) -- For human (not AI) the lading event is delayed unfortunately. Therefore, we need another correction factor. - dc= dc or 65 + dc = dc or 65 -- Corrected landing distance wrt to stern. Landing distance needs to be reduced due to delayed landing event for human players. - local d=Ldist-dc + local d = Ldist - dc -- Multiplayer wire correction. if self.mpWireCorrection then - d=d-self.mpWireCorrection + d = d - self.mpWireCorrection + end -- Shift wires from stern to their correct position. - local w1=self.carrierparam.wire1 - local w2=self.carrierparam.wire2 - local w3=self.carrierparam.wire3 - local w4=self.carrierparam.wire4 + local w1 = self.carrierparam.wire1 + local w2 = self.carrierparam.wire2 + local w3 = self.carrierparam.wire3 + local w4 = self.carrierparam.wire4 -- Which wire was caught? local wire - if d wire=%d (dc=%.1f)", Ldist, Ldist-dc, wire, dc)) + self:T( string.format( "GetWire: L=%.1f, L-dc=%.1f ==> wire=%d (dc=%.1f)", Ldist, Ldist - dc, wire, dc ) ) return wire end @@ -107927,61 +114435,61 @@ end --- Trapped? Check if in air or not after landing event. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. -function AIRBOSS:_Trapped(playerData) +function AIRBOSS:_Trapped( playerData ) - if playerData.unit:InAir()==false then + if playerData.unit:InAir() == false then -- Seems we have successfully landed. -- Lets see if we can get a good wire. - local unit=playerData.unit + local unit = playerData.unit -- Coordinate of player aircraft. - local coord=unit:GetCoordinate() + local coord = unit:GetCoordinate() -- Get velocity in km/h. We need to substrackt the carrier velocity. - local v=unit:GetVelocityKMH()-self.carrier:GetVelocityKMH() + local v = unit:GetVelocityKMH() - self.carrier:GetVelocityKMH() -- Stern coordinate. - local stern=self:_GetSternCoord() + local stern = self:_GetSternCoord() -- Distance to stern pos. - local s=stern:Get2DDistance(coord) + local s = stern:Get2DDistance( coord ) -- Get current wire (estimate). This now based on the position where the player comes to a standstill which should reflect the trapped wire better. - local dcorr=100 - if playerData.actype==AIRBOSS.AircraftCarrier.HORNET then - dcorr=100 - elseif playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then + local dcorr = 100 + if playerData.actype == AIRBOSS.AircraftCarrier.HORNET then + dcorr = 100 + elseif playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B then -- TODO: Check Tomcat. - dcorr=100 - elseif playerData.actype==AIRBOSS.AircraftCarrier.A4EC then + dcorr = 100 + elseif playerData.actype == AIRBOSS.AircraftCarrier.A4EC then -- A-4E gets slowed down much faster the the F/A-18C! - dcorr=56 - elseif playerData.actype==AIRBOSS.AircraftCarrier.T45C then - -- T-45 also gets slowed down much faster the the F/A-18C. - dcorr=56 + dcorr = 56 + elseif playerData.actype == AIRBOSS.AircraftCarrier.T45C then + -- T-45 also gets slowed down much faster the the F/A-18C. + dcorr = 56 end -- Get wire. - local wire=self:_GetWire(coord, dcorr) + local wire = self:_GetWire( coord, dcorr ) -- Debug. - local text=string.format("Player %s _Trapped: v=%.1f km/h, s-dcorr=%.1f m ==> wire=%d (dcorr=%d)", playerData.name, v, s-dcorr, wire, dcorr) - self:T(self.lid..text) + local text = string.format( "Player %s _Trapped: v=%.1f km/h, s-dcorr=%.1f m ==> wire=%d (dcorr=%d)", playerData.name, v, s - dcorr, wire, dcorr ) + self:T( self.lid .. text ) -- Call this function again until v < threshold. Player comes to a standstill ==> Get wire! - if v>5 then + if v > 5 then -- Check if we passed all wires. - if wire>4 and v>10 and not playerData.warning then + if wire > 4 and v > 10 and not playerData.warning then -- Looks like we missed the wires ==> Bolter! - self:RadioTransmission(self.LSORadio, self.LSOCall.BOLTER, nil, nil, nil, true) - playerData.warning=true + self:RadioTransmission( self.LSORadio, self.LSOCall.BOLTER, nil, nil, nil, true ) + playerData.warning = true end -- Call function again and check if converged or back in air. - --SCHEDULER:New(nil, self._Trapped, {self, playerData}, 0.1) - self:ScheduleOnce(0.1, self._Trapped, self, playerData) + -- SCHEDULER:New(nil, self._Trapped, {self, playerData}, 0.1) + self:ScheduleOnce( 0.1, self._Trapped, self, playerData ) return end @@ -107992,47 +114500,47 @@ function AIRBOSS:_Trapped(playerData) -- Put some smoke and a mark. if self.Debug then coord:SmokeBlue() - coord:MarkToAll(text) - stern:MarkToAll("Stern") + coord:MarkToAll( text ) + stern:MarkToAll( "Stern" ) end -- Set player wire. - playerData.wire=wire + playerData.wire = wire -- Message to player. - local text=string.format("Trapped %d-wire.", wire) - if wire==3 then - text=text.." Well done!" - elseif wire==2 then - text=text.." Not bad, maybe you even get the 3rd next time." - elseif wire==4 then - text=text.." That was scary. You can do better than this!" - elseif wire==1 then - text=text.." Try harder next time!" + local text = string.format( "Trapped %d-wire.", wire ) + if wire == 3 then + text = text .. " Well done!" + elseif wire == 2 then + text = text .. " Not bad, maybe you even get the 3rd next time." + elseif wire == 4 then + text = text .. " That was scary. You can do better than this!" + elseif wire == 1 then + text = text .. " Try harder next time!" end -- Message to player. - self:MessageToPlayer(playerData, text, "LSO", "") + self:MessageToPlayer( playerData, text, "LSO", "" ) -- Debrief. - local hint = string.format("Trapped %d-wire.", wire) - self:_AddToDebrief(playerData, hint, "Groove: IW") + local hint = string.format( "Trapped %d-wire.", wire ) + self:_AddToDebrief( playerData, hint, "Groove: IW" ) else - --Again in air ==> Boltered! - local text=string.format("Player %s boltered in trapped function.", playerData.name) - self:T(self.lid..text) - MESSAGE:New(text, 5, "DEBUG"):ToAllIf(self.debug) + -- Again in air ==> Boltered! + local text = string.format( "Player %s boltered in trapped function.", playerData.name ) + self:T( self.lid .. text ) + MESSAGE:New( text, 5, "DEBUG" ):ToAllIf( self.debug ) -- Bolter switch on. - playerData.boltered=true + playerData.boltered = true end -- Next step: debriefing. - playerData.step=AIRBOSS.PatternStep.DEBRIEF - playerData.warning=nil + playerData.step = AIRBOSS.PatternStep.DEBRIEF + playerData.warning = nil end ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -108043,53 +114551,53 @@ end -- @param #AIRBOSS self -- @param #number case Recovery Case. -- @return Core.Zone#ZONE_POLYGON_BASE Initial zone. -function AIRBOSS:_GetZoneInitial(case) +function AIRBOSS:_GetZoneInitial( case ) - self.zoneInitial=self.zoneInitial or ZONE_POLYGON_BASE:New("Zone CASE I/II Initial") + self.zoneInitial = self.zoneInitial or ZONE_POLYGON_BASE:New( "Zone CASE I/II Initial" ) -- Get radial, i.e. inverse of BRC. - local radial=self:GetRadial(2, false, false) + local radial = self:GetRadial( 2, false, false ) -- Carrier coordinate. - local cv=self:GetCoordinate() + local cv = self:GetCoordinate() -- Vec2 array. - local vec2={} + local vec2 = {} - if case==1 then + if case == 1 then -- Case I - local c1=cv:Translate(UTILS.NMToMeters(0.5), radial-90) -- 0.0 0.5 starboard - local c2=cv:Translate(UTILS.NMToMeters(1.3), radial-90):Translate(UTILS.NMToMeters(3), radial) -- -3.0 1.3 starboard, astern - local c3=cv:Translate(UTILS.NMToMeters(0.4), radial+90):Translate(UTILS.NMToMeters(3), radial) -- -3.0 -0.4 port, astern - local c4=cv:Translate(UTILS.NMToMeters(1.0), radial) - local c5=cv + local c1 = cv:Translate( UTILS.NMToMeters( 0.5 ), radial - 90 ) -- 0.0 0.5 starboard + local c2 = cv:Translate( UTILS.NMToMeters( 1.3 ), radial - 90 ):Translate( UTILS.NMToMeters( 3 ), radial ) -- -3.0 1.3 starboard, astern + local c3 = cv:Translate( UTILS.NMToMeters( 0.4 ), radial + 90 ):Translate( UTILS.NMToMeters( 3 ), radial ) -- -3.0 -0.4 port, astern + local c4 = cv:Translate( UTILS.NMToMeters( 1.0 ), radial ) + local c5 = cv -- Vec2 array. - vec2={c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2()} + vec2 = { c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2() } else -- Case II -- Funnel. - local c1=cv:Translate(UTILS.NMToMeters(0.5), radial-90) -- 0.0, 0.5 - local c2=c1:Translate(UTILS.NMToMeters(0.5), radial) -- 0.5, 0.5 - local c3=cv:Translate(UTILS.NMToMeters(1.2), radial-90):Translate(UTILS.NMToMeters(3), radial) -- 3.0, 1.2 - local c4=cv:Translate(UTILS.NMToMeters(1.2), radial+90):Translate(UTILS.NMToMeters(3), radial) -- 3.0,-1.2 - local c5=cv:Translate(UTILS.NMToMeters(0.5), radial) - local c6=cv + local c1 = cv:Translate( UTILS.NMToMeters( 0.5 ), radial - 90 ) -- 0.0, 0.5 + local c2 = c1:Translate( UTILS.NMToMeters( 0.5 ), radial ) -- 0.5, 0.5 + local c3 = cv:Translate( UTILS.NMToMeters( 1.2 ), radial - 90 ):Translate( UTILS.NMToMeters( 3 ), radial ) -- 3.0, 1.2 + local c4 = cv:Translate( UTILS.NMToMeters( 1.2 ), radial + 90 ):Translate( UTILS.NMToMeters( 3 ), radial ) -- 3.0,-1.2 + local c5 = cv:Translate( UTILS.NMToMeters( 0.5 ), radial ) + local c6 = cv -- Vec2 array. - vec2={c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2(), c6:GetVec2()} + vec2 = { c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2(), c6:GetVec2() } end -- Polygon zone. - --local zone=ZONE_POLYGON_BASE:New("Zone CASE I/II Initial", vec2) + -- local zone=ZONE_POLYGON_BASE:New("Zone CASE I/II Initial", vec2) - self.zoneInitial:UpdateFromVec2(vec2) + self.zoneInitial:UpdateFromVec2( vec2 ) - --return zone + -- return zone return self.zoneInitial end @@ -108098,70 +114606,69 @@ end -- @return Core.Zone#ZONE_POLYGON_BASE Lineup zone. function AIRBOSS:_GetZoneLineup() - self.zoneLineup=self.zoneLineup or ZONE_POLYGON_BASE:New("Zone Lineup") + self.zoneLineup = self.zoneLineup or ZONE_POLYGON_BASE:New( "Zone Lineup" ) -- Get radial, i.e. inverse of BRC. - local fbi=self:GetRadial(1, false, false) + local fbi = self:GetRadial( 1, false, false ) -- Stern coordinate. - local st=self:_GetOptLandingCoordinate() + local st = self:_GetOptLandingCoordinate() -- Zone points. - local c1=st - local c2=st:Translate(UTILS.NMToMeters(0.50), fbi+15) - local c3=st:Translate(UTILS.NMToMeters(0.50), fbi+self.lue._max-0.05) - local c4=st:Translate(UTILS.NMToMeters(0.77), fbi+self.lue._max-0.05) - local c5=c4:Translate(UTILS.NMToMeters(0.25), fbi-90) + local c1 = st + local c2 = st:Translate( UTILS.NMToMeters( 0.50 ), fbi + 15 ) + local c3 = st:Translate( UTILS.NMToMeters( 0.50 ), fbi + self.lue._max - 0.05 ) + local c4 = st:Translate( UTILS.NMToMeters( 0.77 ), fbi + self.lue._max - 0.05 ) + local c5 = c4:Translate( UTILS.NMToMeters( 0.25 ), fbi - 90 ) -- Vec2 array. - local vec2={c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2()} + local vec2 = { c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2() } - self.zoneLineup:UpdateFromVec2(vec2) + self.zoneLineup:UpdateFromVec2( vec2 ) -- Polygon zone. - --local zone=ZONE_POLYGON_BASE:New("Zone Lineup", vec2) - --return zone + -- local zone=ZONE_POLYGON_BASE:New("Zone Lineup", vec2) + -- return zone return self.zoneLineup end - --- Get groove zone. -- @param #AIRBOSS self -- @param #number l Length of the groove in NM. Default 1.5 NM. -- @param #number w Width of the groove in NM. Default 0.25 NM. -- @param #number b Width of the beginning in NM. Default 0.10 NM. -- @return Core.Zone#ZONE_POLYGON_BASE Groove zone. -function AIRBOSS:_GetZoneGroove(l, w, b) +function AIRBOSS:_GetZoneGroove( l, w, b ) - self.zoneGroove=self.zoneGroove or ZONE_POLYGON_BASE:New("Zone Groove") + self.zoneGroove = self.zoneGroove or ZONE_POLYGON_BASE:New( "Zone Groove" ) - l=l or 1.50 - w=w or 0.25 - b=b or 0.10 + l = l or 1.50 + w = w or 0.25 + b = b or 0.10 -- Get radial, i.e. inverse of BRC. - local fbi=self:GetRadial(1, false, false) + local fbi = self:GetRadial( 1, false, false ) -- Stern coordinate. - local st=self:_GetSternCoord() + local st = self:_GetSternCoord() -- Zone points. - local c1=st:Translate(self.carrierparam.totwidthstarboard, fbi-90) - local c2=st:Translate(UTILS.NMToMeters(0.10), fbi-90):Translate(UTILS.NMToMeters(0.3), fbi) - local c3=st:Translate(UTILS.NMToMeters(0.25), fbi-90):Translate(UTILS.NMToMeters(l), fbi) - local c4=st:Translate(UTILS.NMToMeters(w/2), fbi+90):Translate(UTILS.NMToMeters(l), fbi) - local c5=st:Translate(UTILS.NMToMeters(b), fbi+90):Translate(UTILS.NMToMeters(0.3), fbi) - local c6=st:Translate(self.carrierparam.totwidthport, fbi+90) + local c1 = st:Translate( self.carrierparam.totwidthstarboard, fbi - 90 ) + local c2 = st:Translate( UTILS.NMToMeters( 0.10 ), fbi - 90 ):Translate( UTILS.NMToMeters( 0.3 ), fbi ) + local c3 = st:Translate( UTILS.NMToMeters( 0.25 ), fbi - 90 ):Translate( UTILS.NMToMeters( l ), fbi ) + local c4 = st:Translate( UTILS.NMToMeters( w / 2 ), fbi + 90 ):Translate( UTILS.NMToMeters( l ), fbi ) + local c5 = st:Translate( UTILS.NMToMeters( b ), fbi + 90 ):Translate( UTILS.NMToMeters( 0.3 ), fbi ) + local c6 = st:Translate( self.carrierparam.totwidthport, fbi + 90 ) -- Vec2 array. - local vec2={c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2(), c6:GetVec2()} + local vec2 = { c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2(), c6:GetVec2() } - self.zoneGroove:UpdateFromVec2(vec2) + self.zoneGroove:UpdateFromVec2( vec2 ) -- Polygon zone. - --local zone=ZONE_POLYGON_BASE:New("Zone Groove", vec2) - --return zone + -- local zone=ZONE_POLYGON_BASE:New("Zone Groove", vec2) + -- return zone return self.zoneGroove end @@ -108170,49 +114677,49 @@ end -- @param #AIRBOSS self -- @param #number case Recovery case. -- @return Core.Zone#ZONE_RADIUS Arc in zone. -function AIRBOSS:_GetZoneBullseye(case) +function AIRBOSS:_GetZoneBullseye( case ) -- Radius = 1 NM. - local radius=UTILS.NMToMeters(1) + local radius = UTILS.NMToMeters( 1 ) -- Distance = 3 NM - local distance=UTILS.NMToMeters(3) + local distance = UTILS.NMToMeters( 3 ) -- Zone depends on Case recovery. - local radial=self:GetRadial(case, false, false) + local radial = self:GetRadial( case, false, false ) -- Get coordinate and vec2. - local coord=self:GetCoordinate():Translate(distance, radial) - local vec2=coord:GetVec2() + local coord = self:GetCoordinate():Translate( distance, radial ) + local vec2 = coord:GetVec2() -- Create zone. - local zone=ZONE_RADIUS:New("Zone Bullseye", vec2, radius) + local zone = ZONE_RADIUS:New( "Zone Bullseye", vec2, radius ) return zone - --self.zoneBullseye=self.zoneBullseye or ZONE_RADIUS:New("Zone Bullseye", vec2, radius) + -- self.zoneBullseye=self.zoneBullseye or ZONE_RADIUS:New("Zone Bullseye", vec2, radius) end --- Get dirty up zone with radius 1 NM and DME 9 NM from the carrier. Radial depends on recovery case. -- @param #AIRBOSS self -- @param #number case Recovery case. -- @return Core.Zone#ZONE_RADIUS Dirty up zone. -function AIRBOSS:_GetZoneDirtyUp(case) +function AIRBOSS:_GetZoneDirtyUp( case ) -- Radius = 1 NM. - local radius=UTILS.NMToMeters(1) + local radius = UTILS.NMToMeters( 1 ) -- Distance = 9 NM - local distance=UTILS.NMToMeters(9) + local distance = UTILS.NMToMeters( 9 ) -- Zone depends on Case recovery. - local radial=self:GetRadial(case, false, false) + local radial = self:GetRadial( case, false, false ) -- Get coordinate and vec2. - local coord=self:GetCoordinate():Translate(distance, radial) - local vec2=coord:GetVec2() + local coord = self:GetCoordinate():Translate( distance, radial ) + local vec2 = coord:GetVec2() -- Create zone. - local zone=ZONE_RADIUS:New("Zone Dirty Up", vec2, radius) + local zone = ZONE_RADIUS:New( "Zone Dirty Up", vec2, radius ) return zone end @@ -108221,22 +114728,22 @@ end -- @param #AIRBOSS self -- @param #number case Recovery case. -- @return Core.Zone#ZONE_RADIUS Arc in zone. -function AIRBOSS:_GetZoneArcOut(case) +function AIRBOSS:_GetZoneArcOut( case ) -- Radius = 1.25 NM. - local radius=UTILS.NMToMeters(1.25) + local radius = UTILS.NMToMeters( 1.25 ) -- Distance = 12 NM - local distance=UTILS.NMToMeters(11.75) + local distance = UTILS.NMToMeters( 11.75 ) -- Zone depends on Case recovery. - local radial=self:GetRadial(case, false, false) + local radial = self:GetRadial( case, false, false ) -- Get coordinate of carrier and translate. - local coord=self:GetCoordinate():Translate(distance, radial) + local coord = self:GetCoordinate():Translate( distance, radial ) -- Create zone. - local zone=ZONE_RADIUS:New("Zone Arc Out", coord:GetVec2(), radius) + local zone = ZONE_RADIUS:New( "Zone Arc Out", coord:GetVec2(), radius ) return zone end @@ -108245,28 +114752,28 @@ end -- @param #AIRBOSS self -- @param #number case Recovery case. -- @return Core.Zone#ZONE_RADIUS Arc in zone. -function AIRBOSS:_GetZoneArcIn(case) +function AIRBOSS:_GetZoneArcIn( case ) -- Radius = 1.25 NM. - local radius=UTILS.NMToMeters(1.25) + local radius = UTILS.NMToMeters( 1.25 ) -- Zone depends on Case recovery. - local radial=self:GetRadial(case, false, true) + local radial = self:GetRadial( case, false, true ) -- Angle between FB/BRC and holding zone. - local alpha=math.rad(self.holdingoffset) + local alpha = math.rad( self.holdingoffset ) -- 14+x NM from carrier - local x=14 --/math.cos(alpha) + local x = 14 -- /math.cos(alpha) -- Distance = 14 NM - local distance=UTILS.NMToMeters(x) + local distance = UTILS.NMToMeters( x ) -- Get coordinate. - local coord=self:GetCoordinate():Translate(distance, radial) + local coord = self:GetCoordinate():Translate( distance, radial ) -- Create zone. - local zone=ZONE_RADIUS:New("Zone Arc In", coord:GetVec2(), radius) + local zone = ZONE_RADIUS:New( "Zone Arc In", coord:GetVec2(), radius ) return zone end @@ -108275,79 +114782,78 @@ end -- @param #AIRBOSS self -- @param #number case Recovery case. -- @return Core.Zone#ZONE_RADIUS Circular platform zone. -function AIRBOSS:_GetZonePlatform(case) +function AIRBOSS:_GetZonePlatform( case ) -- Radius = 1 NM. - local radius=UTILS.NMToMeters(1) + local radius = UTILS.NMToMeters( 1 ) -- Zone depends on Case recovery. - local radial=self:GetRadial(case, false, true) + local radial = self:GetRadial( case, false, true ) -- Angle between FB/BRC and holding zone. - local alpha=math.rad(self.holdingoffset) + local alpha = math.rad( self.holdingoffset ) -- Distance = 19 NM - local distance=UTILS.NMToMeters(19) --/math.cos(alpha) + local distance = UTILS.NMToMeters( 19 ) -- /math.cos(alpha) -- Get coordinate. - local coord=self:GetCoordinate():Translate(distance, radial) + local coord = self:GetCoordinate():Translate( distance, radial ) -- Create zone. - local zone=ZONE_RADIUS:New("Zone Platform", coord:GetVec2(), radius) + local zone = ZONE_RADIUS:New( "Zone Platform", coord:GetVec2(), radius ) return zone end - --- Get approach corridor zone. Shape depends on recovery case. -- @param #AIRBOSS self -- @param #number case Recovery case. -- @param #number l Length of the zone in NM. Default 31 (=21+10) NM. -- @return Core.Zone#ZONE_POLYGON_BASE Box zone. -function AIRBOSS:_GetZoneCorridor(case, l) +function AIRBOSS:_GetZoneCorridor( case, l ) -- Total length. - l=l or 31 + l = l or 31 -- Radial and offset. - local radial=self:GetRadial(case, false, false) - local offset=self:GetRadial(case, false, true) + local radial = self:GetRadial( case, false, false ) + local offset = self:GetRadial( case, false, true ) -- Distance shift ahead of carrier to allow for some space to bolter. - local dx=5 + local dx = 5 -- Width of the box in NM. - local w=2 - local w2=w/2 + local w = 2 + local w2 = w / 2 -- Distance from carrier to arc out zone. - local d=12 + local d = 12 -- Carrier position. - local cv=self:GetCoordinate() + local cv = self:GetCoordinate() -- Polygon points. - local c={} + local c = {} -- First point. Carrier coordinate translated 5 NM in direction of travel to allow for bolter space. - c[1]=cv:Translate(-UTILS.NMToMeters(dx), radial) + c[1] = cv:Translate( -UTILS.NMToMeters( dx ), radial ) - if math.abs(self.holdingoffset)>=5 then + if math.abs( self.holdingoffset ) >= 5 then ----------------- -- Angled Case -- ----------------- - c[2]=c[1]:Translate( UTILS.NMToMeters(w2), radial-90) -- 1 Right of carrier, dx ahead. - c[3]=c[2]:Translate( UTILS.NMToMeters(d+dx+w2), radial) -- 13 "south" @ 1 right + c[2] = c[1]:Translate( UTILS.NMToMeters( w2 ), radial - 90 ) -- 1 Right of carrier, dx ahead. + c[3] = c[2]:Translate( UTILS.NMToMeters( d + dx + w2 ), radial ) -- 13 "south" @ 1 right - c[4]=cv:Translate(UTILS.NMToMeters(15), offset):Translate(UTILS.NMToMeters(1), offset-90) - c[5]=cv:Translate(UTILS.NMToMeters(l), offset):Translate(UTILS.NMToMeters(1), offset-90) - c[6]=cv:Translate(UTILS.NMToMeters(l), offset):Translate(UTILS.NMToMeters(1), offset+90) - c[7]=cv:Translate(UTILS.NMToMeters(13), offset):Translate(UTILS.NMToMeters(1), offset+90) - c[8]=cv:Translate(UTILS.NMToMeters(11), radial):Translate(UTILS.NMToMeters(1), radial+90) + c[4] = cv:Translate( UTILS.NMToMeters( 15 ), offset ):Translate( UTILS.NMToMeters( 1 ), offset - 90 ) + c[5] = cv:Translate( UTILS.NMToMeters( l ), offset ):Translate( UTILS.NMToMeters( 1 ), offset - 90 ) + c[6] = cv:Translate( UTILS.NMToMeters( l ), offset ):Translate( UTILS.NMToMeters( 1 ), offset + 90 ) + c[7] = cv:Translate( UTILS.NMToMeters( 13 ), offset ):Translate( UTILS.NMToMeters( 1 ), offset + 90 ) + c[8] = cv:Translate( UTILS.NMToMeters( 11 ), radial ):Translate( UTILS.NMToMeters( 1 ), radial + 90 ) - c[9]=c[1]:Translate(UTILS.NMToMeters(w2), radial+90) + c[9] = c[1]:Translate( UTILS.NMToMeters( w2 ), radial + 90 ) else @@ -108355,70 +114861,68 @@ function AIRBOSS:_GetZoneCorridor(case, l) -- Easy case of a long box -- ----------------------------- - c[2]=c[1]:Translate( UTILS.NMToMeters(w2), radial-90) - c[3]=c[2]:Translate( UTILS.NMToMeters(dx+l), radial) -- Stack 1 starts at 21 and is 7 NM. - c[4]=c[3]:Translate( UTILS.NMToMeters(w), radial+90) - c[5]=c[1]:Translate( UTILS.NMToMeters(w2), radial+90) + c[2] = c[1]:Translate( UTILS.NMToMeters( w2 ), radial - 90 ) + c[3] = c[2]:Translate( UTILS.NMToMeters( dx + l ), radial ) -- Stack 1 starts at 21 and is 7 NM. + c[4] = c[3]:Translate( UTILS.NMToMeters( w ), radial + 90 ) + c[5] = c[1]:Translate( UTILS.NMToMeters( w2 ), radial + 90 ) end - -- Create an array of a square! - local p={} - for _i,_c in ipairs(c) do + local p = {} + for _i, _c in ipairs( c ) do if self.Debug then - --_c:SmokeBlue() + -- _c:SmokeBlue() end - p[_i]=_c:GetVec2() + p[_i] = _c:GetVec2() end -- Square zone length=10NM width=6 NM behind the carrier starting at angels+15 NM behind the carrier. -- So stay 0-5 NM (+1 NM error margin) port of carrier. - local zone=ZONE_POLYGON_BASE:New("CASE II/III Approach Corridor", p) + local zone = ZONE_POLYGON_BASE:New( "CASE II/III Approach Corridor", p ) return zone end - --- Get zone of carrier. Carrier is approximated as rectangle. -- @param #AIRBOSS self -- @return Core.Zone#ZONE Zone surrounding the carrier. function AIRBOSS:_GetZoneCarrierBox() - self.zoneCarrierbox=self.zoneCarrierbox or ZONE_POLYGON_BASE:New("Carrier Box Zone") + self.zoneCarrierbox = self.zoneCarrierbox or ZONE_POLYGON_BASE:New( "Carrier Box Zone" ) -- Stern coordinate. - local S=self:_GetSternCoord() + local S = self:_GetSternCoord() -- Current carrier heading. - local hdg=self:GetHeading(false) + local hdg = self:GetHeading( false ) -- Coordinate array. - local p={} + local p = {} -- Starboard stern point. - p[1]=S:Translate(self.carrierparam.totwidthstarboard, hdg+90) + p[1] = S:Translate( self.carrierparam.totwidthstarboard, hdg + 90 ) -- Starboard bow point. - p[2]=p[1]:Translate(self.carrierparam.totlength, hdg) + p[2] = p[1]:Translate( self.carrierparam.totlength, hdg ) -- Port bow point. - p[3]=p[2]:Translate(self.carrierparam.totwidthstarboard+self.carrierparam.totwidthport, hdg-90) + p[3] = p[2]:Translate( self.carrierparam.totwidthstarboard + self.carrierparam.totwidthport, hdg - 90 ) -- Port stern point. - p[4]=p[3]:Translate(self.carrierparam.totlength, hdg-180) + p[4] = p[3]:Translate( self.carrierparam.totlength, hdg - 180 ) -- Convert to vec2. - local vec2={} - for _,coord in ipairs(p) do - table.insert(vec2, coord:GetVec2()) + local vec2 = {} + for _, coord in ipairs( p ) do + table.insert( vec2, coord:GetVec2() ) end -- Create polygon zone. - --local zone=ZONE_POLYGON_BASE:New("Carrier Box Zone", vec2) - --return zone + -- local zone=ZONE_POLYGON_BASE:New("Carrier Box Zone", vec2) + -- return zone - self.zoneCarrierbox:UpdateFromVec2(vec2) + self.zoneCarrierbox:UpdateFromVec2( vec2 ) return self.zoneCarrierbox end @@ -108428,167 +114932,165 @@ end -- @return Core.Zone#ZONE_POLYGON Zone surrounding landing runway. function AIRBOSS:_GetZoneRunwayBox() - self.zoneRunwaybox=self.zoneRunwaybox or ZONE_POLYGON_BASE:New("Landing Runway Zone") + self.zoneRunwaybox = self.zoneRunwaybox or ZONE_POLYGON_BASE:New( "Landing Runway Zone" ) -- Stern coordinate. - local S=self:_GetSternCoord() + local S = self:_GetSternCoord() -- Current carrier heading. - local FB=self:GetFinalBearing(false) + local FB = self:GetFinalBearing( false ) -- Coordinate array. - local p={} + local p = {} -- Points. - p[1]=S:Translate(self.carrierparam.rwywidth*0.5, FB+90) - p[2]=p[1]:Translate(self.carrierparam.rwylength, FB) - p[3]=p[2]:Translate(self.carrierparam.rwywidth, FB-90) - p[4]=p[3]:Translate(self.carrierparam.rwylength, FB-180) + p[1] = S:Translate( self.carrierparam.rwywidth * 0.5, FB + 90 ) + p[2] = p[1]:Translate( self.carrierparam.rwylength, FB ) + p[3] = p[2]:Translate( self.carrierparam.rwywidth, FB - 90 ) + p[4] = p[3]:Translate( self.carrierparam.rwylength, FB - 180 ) -- Convert to vec2. - local vec2={} - for _,coord in ipairs(p) do - table.insert(vec2, coord:GetVec2()) + local vec2 = {} + for _, coord in ipairs( p ) do + table.insert( vec2, coord:GetVec2() ) end -- Create polygon zone. - --local zone=ZONE_POLYGON_BASE:New("Landing Runway Zone", vec2) - --return zone + -- local zone=ZONE_POLYGON_BASE:New("Landing Runway Zone", vec2) + -- return zone - self.zoneRunwaybox:UpdateFromVec2(vec2) + self.zoneRunwaybox:UpdateFromVec2( vec2 ) return self.zoneRunwaybox end +--- Get zone of primary abeam landing position of HMS Hermes, USS Tarawa, USS America and Juan Carlos. Box length 50 meters and width 30 meters. ---- Get zone of primary abeam landing position of USS Tarawa. Box length and width 30 meters. +--- Allow for Clear to land call from LSO approaching abeam the landing spot if stable as per NATOPS 00-80T -- @param #AIRBOSS self -- @return Core.Zone#ZONE_POLYGON Zone surrounding landing runway. function AIRBOSS:_GetZoneAbeamLandingSpot() -- Primary landing Spot coordinate. - local S=self:_GetOptLandingCoordinate() + local S = self:_GetOptLandingCoordinate() -- Current carrier heading. - local FB=self:GetFinalBearing(false) + local FB = self:GetFinalBearing( false ) - -- Coordinate array. + -- Coordinate array. Pene Testing extended Abeam landing spot V/STOL. local p={} - + -- Points. - p[1]=S:Translate( 15, FB):Translate(15, FB+90) -- Top-Right - p[2]=S:Translate(-15, FB):Translate(15, FB+90) -- Bottom-Right - p[3]=S:Translate(-15, FB):Translate(15, FB-90) -- Bottom-Left - p[4]=S:Translate( 15, FB):Translate(15, FB-90) -- Top-Left + p[1] = S:Translate( 15, FB ):Translate( 15, FB + 90 ) -- Top-Right + p[2] = S:Translate( -45, FB ):Translate( 15, FB + 90 ) -- Bottom-Right + p[3] = S:Translate( -45, FB ):Translate( 15, FB - 90 ) -- Bottom-Left + p[4] = S:Translate( 15, FB ):Translate( 15, FB - 90 ) -- Top-Left -- Convert to vec2. - local vec2={} - for _,coord in ipairs(p) do - table.insert(vec2, coord:GetVec2()) + local vec2 = {} + for _, coord in ipairs( p ) do + table.insert( vec2, coord:GetVec2() ) end -- Create polygon zone. - local zone=ZONE_POLYGON_BASE:New("Abeam Landing Spot Zone", vec2) + local zone = ZONE_POLYGON_BASE:New( "Abeam Landing Spot Zone", vec2 ) return zone end - --- Get zone of the primary landing spot of the USS Tarawa. -- @param #AIRBOSS self -- @return Core.Zone#ZONE_POLYGON Zone surrounding landing runway. function AIRBOSS:_GetZoneLandingSpot() -- Primary landing Spot coordinate. - local S=self:_GetLandingSpotCoordinate() + local S = self:_GetLandingSpotCoordinate() -- Current carrier heading. - local FB=self:GetFinalBearing(false) + local FB = self:GetFinalBearing( false ) -- Coordinate array. - local p={} + local p = {} -- Points. - p[1]=S:Translate( 10, FB):Translate(10, FB+90) -- Top-Right - p[2]=S:Translate(-10, FB):Translate(10, FB+90) -- Bottom-Right - p[3]=S:Translate(-10, FB):Translate(10, FB-90) -- Bottom-Left - p[4]=S:Translate( 10, FB):Translate(10, FB-90) -- Top-left + p[1] = S:Translate( 10, FB ):Translate( 10, FB + 90 ) -- Top-Right + p[2] = S:Translate( -10, FB ):Translate( 10, FB + 90 ) -- Bottom-Right + p[3] = S:Translate( -10, FB ):Translate( 10, FB - 90 ) -- Bottom-Left + p[4] = S:Translate( 10, FB ):Translate( 10, FB - 90 ) -- Top-left -- Convert to vec2. - local vec2={} - for _,coord in ipairs(p) do - table.insert(vec2, coord:GetVec2()) + local vec2 = {} + for _, coord in ipairs( p ) do + table.insert( vec2, coord:GetVec2() ) end -- Create polygon zone. - local zone=ZONE_POLYGON_BASE:New("Landing Spot Zone", vec2) + local zone = ZONE_POLYGON_BASE:New( "Landing Spot Zone", vec2 ) return zone end - --- Get holding zone of player. -- @param #AIRBOSS self -- @param #number case Recovery case. -- @param #number stack Marshal stack number. -- @return Core.Zone#ZONE Holding zone. -function AIRBOSS:_GetZoneHolding(case, stack) +function AIRBOSS:_GetZoneHolding( case, stack ) -- Holding zone. - local zoneHolding=nil --Core.Zone#ZONE + local zoneHolding = nil -- Core.Zone#ZONE -- Stack is <= 0 ==> no marshal zone. - if stack<=0 then - self:E(self.lid.."ERROR: Stack <= 0 in _GetZoneHolding!") - self:E({case=case, stack=stack}) + if stack <= 0 then + self:E( self.lid .. "ERROR: Stack <= 0 in _GetZoneHolding!" ) + self:E( { case = case, stack = stack } ) return nil end -- Pattern altitude. - local patternalt, c1, c2=self:_GetMarshalAltitude(stack, case) + local patternalt, c1, c2 = self:_GetMarshalAltitude( stack, case ) -- Select case. - if case==1 then + if case == 1 then -- CASE I -- Get current carrier heading. - local hdg=self:GetHeading() + local hdg = self:GetHeading() -- Distance to the post. - local D=UTILS.NMToMeters(2.5) + local D = UTILS.NMToMeters( 2.5 ) -- Post 2.5 NM port of carrier. - local Post=self:GetCoordinate():Translate(D, hdg+270) + local Post = self:GetCoordinate():Translate( D, hdg + 270 ) - --TODO: update zone not creating a new one. + -- TODO: update zone not creating a new one. -- Create holding zone. - self.zoneHolding=ZONE_RADIUS:New("CASE I Holding Zone", Post:GetVec2(), self.marshalradius) + self.zoneHolding = ZONE_RADIUS:New( "CASE I Holding Zone", Post:GetVec2(), self.marshalradius ) -- Delta pattern. - if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then - self.zoneHolding=ZONE_RADIUS:New("CASE I Holding Zone", self.carrier:GetVec2(), UTILS.NMToMeters(5)) + if self.carriertype == AIRBOSS.CarrierType.HERMES or self.carriertype == AIRBOSS.CarrierType.TARAWA or self.carriertype == AIRBOSS.CarrierType.AMERICA or self.carriertype == AIRBOSS.CarrierType.JCARLOS or self.carriertype == AIRBOSS.CarrierType.CANBERRA then + self.zoneHolding = ZONE_RADIUS:New( "CASE I Holding Zone", self.carrier:GetVec2(), UTILS.NMToMeters( 5 ) ) end - else -- CASE II/II -- Get radial. - local radial=self:GetRadial(case, false, true) + local radial = self:GetRadial( case, false, true ) -- Create an array of a rectangle. Length is 7 NM, width is 8 NM. One NM starboard to line up with the approach corridor. - local p={} - p[1]=c2:Translate(UTILS.NMToMeters(1), radial-90):GetVec2() --c2 is at (angels+15) NM directly behind the carrier. We translate it 1 NM starboard. - p[2]=c1:Translate(UTILS.NMToMeters(1), radial-90):GetVec2() --c1 is 7 NM further behind. Also translated 1 NM starboard. - p[3]=c1:Translate(UTILS.NMToMeters(7), radial+90):GetVec2() --p3 7 NM port of carrier. - p[4]=c2:Translate(UTILS.NMToMeters(7), radial+90):GetVec2() --p4 7 NM port of carrier. + local p = {} + p[1] = c2:Translate( UTILS.NMToMeters( 1 ), radial - 90 ):GetVec2() -- c2 is at (angels+15) NM directly behind the carrier. We translate it 1 NM starboard. + p[2] = c1:Translate( UTILS.NMToMeters( 1 ), radial - 90 ):GetVec2() -- c1 is 7 NM further behind. Also translated 1 NM starboard. + p[3] = c1:Translate( UTILS.NMToMeters( 7 ), radial + 90 ):GetVec2() -- p3 7 NM port of carrier. + p[4] = c2:Translate( UTILS.NMToMeters( 7 ), radial + 90 ):GetVec2() -- p4 7 NM port of carrier. -- Square zone length=7NM width=6 NM behind the carrier starting at angels+15 NM behind the carrier. -- So stay 0-5 NM (+1 NM error margin) port of carrier. - self.zoneHolding=self.zoneHolding or ZONE_POLYGON_BASE:New("CASE II/III Holding Zone") + self.zoneHolding = self.zoneHolding or ZONE_POLYGON_BASE:New( "CASE II/III Holding Zone" ) - self.zoneHolding:UpdateFromVec2(p) + self.zoneHolding:UpdateFromVec2( p ) end return self.zoneHolding @@ -108599,75 +115101,74 @@ end -- @param #number case Recovery case. -- @param #number stack Stack for Case II/III as we commence from stack>=1. -- @return Core.Zone#ZONE Holding zone. -function AIRBOSS:_GetZoneCommence(case, stack) +function AIRBOSS:_GetZoneCommence( case, stack ) -- Commence zone. local zone - if case==1 then + if case == 1 then -- Case I -- Get current carrier heading. - local hdg=self:GetHeading() + local hdg = self:GetHeading() -- Distance to the zone. - local D=UTILS.NMToMeters(4.75) + local D = UTILS.NMToMeters( 4.75 ) -- Zone radius. - local R=UTILS.NMToMeters(1) + local R = UTILS.NMToMeters( 1 ) -- Three position - local Three=self:GetCoordinate():Translate(D, hdg+275) + local Three = self:GetCoordinate():Translate( D, hdg + 275 ) - if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then + if self.carriertype == AIRBOSS.CarrierType.HERMES or self.carriertype == AIRBOSS.CarrierType.TARAWA or self.carriertype == AIRBOSS.CarrierType.AMERICA or self.carriertype == AIRBOSS.CarrierType.JCARLOS or self.carriertype == AIRBOSS.CarrierType.CANBERRA then + local Dx = UTILS.NMToMeters( 2.25 ) - local Dx=UTILS.NMToMeters(2.25) + local Dz = UTILS.NMToMeters( 2.25 ) - local Dz=UTILS.NMToMeters(2.25) + R = UTILS.NMToMeters( 1 ) - R=UTILS.NMToMeters(1) - - Three=self:GetCoordinate():Translate(Dz, hdg-90):Translate(Dx, hdg-180) + Three = self:GetCoordinate():Translate( Dz, hdg - 90 ):Translate( Dx, hdg - 180 ) end -- Create holding zone. - self.zoneCommence=self.zoneCommence or ZONE_RADIUS:New("CASE I Commence Zone") + self.zoneCommence = self.zoneCommence or ZONE_RADIUS:New( "CASE I Commence Zone" ) - self.zoneCommence:UpdateFromVec2(Three:GetVec2(), R) + self.zoneCommence:UpdateFromVec2( Three:GetVec2(), R ) else -- Case II/III - stack=stack or 1 + stack = stack or 1 - -- Start point at 21 NM for stack=1. - local l=20+stack + -- Start point at 21 NM for stack=1. + local l = 20 + stack -- Offset angle - local offset=self:GetRadial(case, false, true) + local offset = self:GetRadial( case, false, true ) -- Carrier position. - local cv=self:GetCoordinate() + local cv = self:GetCoordinate() -- Polygon points. - local c={} + local c = {} - c[1]=cv:Translate(UTILS.NMToMeters(l), offset):Translate(UTILS.NMToMeters(1), offset-90) - c[2]=cv:Translate(UTILS.NMToMeters(l+2.5), offset):Translate(UTILS.NMToMeters(1), offset-90) - c[3]=cv:Translate(UTILS.NMToMeters(l+2.5), offset):Translate(UTILS.NMToMeters(1), offset+90) - c[4]=cv:Translate(UTILS.NMToMeters(l), offset):Translate(UTILS.NMToMeters(1), offset+90) + c[1] = cv:Translate( UTILS.NMToMeters( l ), offset ):Translate( UTILS.NMToMeters( 1 ), offset - 90 ) + c[2] = cv:Translate( UTILS.NMToMeters( l + 2.5 ), offset ):Translate( UTILS.NMToMeters( 1 ), offset - 90 ) + c[3] = cv:Translate( UTILS.NMToMeters( l + 2.5 ), offset ):Translate( UTILS.NMToMeters( 1 ), offset + 90 ) + c[4] = cv:Translate( UTILS.NMToMeters( l ), offset ):Translate( UTILS.NMToMeters( 1 ), offset + 90 ) -- Create an array of a square! - local p={} - for _i,_c in ipairs(c) do - p[_i]=_c:GetVec2() + local p = {} + for _i, _c in ipairs( c ) do + p[_i] = _c:GetVec2() end -- Zone polygon. - self.zoneCommence=self.zoneCommence or ZONE_POLYGON_BASE:New("CASE II/III Commence Zone") + self.zoneCommence = self.zoneCommence or ZONE_POLYGON_BASE:New( "CASE II/III Commence Zone" ) - self.zoneCommence:UpdateFromVec2(p) + self.zoneCommence:UpdateFromVec2( p ) end @@ -108681,90 +115182,90 @@ end --- Provide info about player status on the fly. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. -function AIRBOSS:_AttitudeMonitor(playerData) +function AIRBOSS:_AttitudeMonitor( playerData ) -- Player unit. - local unit=playerData.unit + local unit = playerData.unit -- Aircraft attitude. - local aoa=unit:GetAoA() - local yaw=unit:GetYaw() - local roll=unit:GetRoll() - local pitch=unit:GetPitch() + local aoa = unit:GetAoA() + local yaw = unit:GetYaw() + local roll = unit:GetRoll() + local pitch = unit:GetPitch() -- Distance to the boat. - local dist=playerData.unit:GetCoordinate():Get2DDistance(self:GetCoordinate()) - local dx,dz,rho,phi=self:_GetDistances(unit) + local dist = playerData.unit:GetCoordinate():Get2DDistance( self:GetCoordinate() ) + local dx, dz, rho, phi = self:_GetDistances( unit ) -- Wind vector. - local wind=unit:GetCoordinate():GetWindWithTurbulenceVec3() + local wind = unit:GetCoordinate():GetWindWithTurbulenceVec3() -- Aircraft veloecity vector. - local velo=unit:GetVelocityVec3() - local vabs=UTILS.VecNorm(velo) + local velo = unit:GetVelocityVec3() + local vabs = UTILS.VecNorm( velo ) - local rwy=false - local step=playerData.step - if playerData.step==AIRBOSS.PatternStep.FINAL or - playerData.step==AIRBOSS.PatternStep.GROOVE_XX or - playerData.step==AIRBOSS.PatternStep.GROOVE_IM or - playerData.step==AIRBOSS.PatternStep.GROOVE_IC or - playerData.step==AIRBOSS.PatternStep.GROOVE_AR or - playerData.step==AIRBOSS.PatternStep.GROOVE_AL or - playerData.step==AIRBOSS.PatternStep.GROOVE_LC or - playerData.step==AIRBOSS.PatternStep.GROOVE_IW then - step=self:_GS(step,-1) - rwy=true + local rwy = false + local step = playerData.step + if playerData.step == AIRBOSS.PatternStep.FINAL or + playerData.step == AIRBOSS.PatternStep.GROOVE_XX or + playerData.step == AIRBOSS.PatternStep.GROOVE_IM or + playerData.step == AIRBOSS.PatternStep.GROOVE_IC or + playerData.step == AIRBOSS.PatternStep.GROOVE_AR or + playerData.step == AIRBOSS.PatternStep.GROOVE_AL or + playerData.step == AIRBOSS.PatternStep.GROOVE_LC or + playerData.step == AIRBOSS.PatternStep.GROOVE_IW then + step = self:_GS( step, -1 ) + rwy = true end -- Relative heading Aircraft to Carrier. - local relhead=self:_GetRelativeHeading(playerData.unit, rwy) + local relhead = self:_GetRelativeHeading( playerData.unit, rwy ) - --local lc=self:_GetOptLandingCoordinate() - --lc:FlareRed() + -- local lc=self:_GetOptLandingCoordinate() + -- lc:FlareRed() -- Output - local text=string.format("Pattern step: %s", step) - text=text..string.format("\nAoA=%.1f° = %.1f Units | |V|=%.1f knots", aoa, self:_AoADeg2Units(playerData, aoa), UTILS.MpsToKnots(vabs)) + local text = string.format( "Pattern step: %s", step ) + text = text .. string.format( "\nAoA=%.1f° = %.1f Units | |V|=%.1f knots", aoa, self:_AoADeg2Units( playerData, aoa ), UTILS.MpsToKnots( vabs ) ) if self.Debug then -- Velocity vector. - text=text..string.format("\nVx=%.1f Vy=%.1f Vz=%.1f m/s", velo.x, velo.y, velo.z) - --Wind vector. - text=text..string.format("\nWind Vx=%.1f Vy=%.1f Vz=%.1f m/s", wind.x, wind.y, wind.z) + text = text .. string.format( "\nVx=%.1f Vy=%.1f Vz=%.1f m/s", velo.x, velo.y, velo.z ) + -- Wind vector. + text = text .. string.format( "\nWind Vx=%.1f Vy=%.1f Vz=%.1f m/s", wind.x, wind.y, wind.z ) end - text=text..string.format("\nPitch=%.1f° | Roll=%.1f° | Yaw=%.1f°", pitch, roll, yaw) - text=text..string.format("\nClimb Angle=%.1f° | Rate=%d ft/min", unit:GetClimbAngle(), velo.y*196.85) - local dist=self:_GetOptLandingCoordinate():Get3DDistance(playerData.unit) + text = text .. string.format( "\nPitch=%.1f° | Roll=%.1f° | Yaw=%.1f°", pitch, roll, yaw ) + text = text .. string.format( "\nClimb Angle=%.1f° | Rate=%d ft/min", unit:GetClimbAngle(), velo.y * 196.85 ) + local dist = self:_GetOptLandingCoordinate():Get3DDistance( playerData.unit ) -- Get player velocity in km/h. - local vplayer=playerData.unit:GetVelocityKMH() + local vplayer = playerData.unit:GetVelocityKMH() -- Get carrier velocity in km/h. - local vcarrier=self.carrier:GetVelocityKMH() + local vcarrier = self.carrier:GetVelocityKMH() -- Speed difference. - local dv=math.abs(vplayer-vcarrier) - local alt=self:_GetAltCarrier(playerData.unit) - text=text..string.format("\nDist=%.1f m Alt=%.1f m delta|V|=%.1f km/h", dist, alt, dv) + local dv = math.abs( vplayer - vcarrier ) + local alt = self:_GetAltCarrier( playerData.unit ) + text = text .. string.format( "\nDist=%.1f m Alt=%.1f m delta|V|=%.1f km/h", dist, alt, dv ) -- If in the groove, provide line up and glide slope error. - if playerData.step==AIRBOSS.PatternStep.FINAL or - playerData.step==AIRBOSS.PatternStep.GROOVE_XX or - playerData.step==AIRBOSS.PatternStep.GROOVE_IM or - playerData.step==AIRBOSS.PatternStep.GROOVE_IC or - playerData.step==AIRBOSS.PatternStep.GROOVE_AR or - playerData.step==AIRBOSS.PatternStep.GROOVE_AL or - playerData.step==AIRBOSS.PatternStep.GROOVE_LC or - playerData.step==AIRBOSS.PatternStep.GROOVE_IW then - local lue=self:_Lineup(playerData.unit, true) - local gle=self:_Glideslope(playerData.unit) - text=text..string.format("\nGamma=%.1f° | Rho=%.1f°", relhead, phi) - text=text..string.format("\nLineUp=%.2f° | GlideSlope=%.2f° | AoA=%.1f Units", lue, gle, self:_AoADeg2Units(playerData, aoa)) - local grade, points, analysis=self:_LSOgrade(playerData) - text=text..string.format("\nTgroove=%.1f sec", self:_GetTimeInGroove(playerData)) - text=text..string.format("\nGrade: %s %.1f PT - %s", grade, points, analysis) + if playerData.step == AIRBOSS.PatternStep.FINAL or + playerData.step == AIRBOSS.PatternStep.GROOVE_XX or + playerData.step == AIRBOSS.PatternStep.GROOVE_IM or + playerData.step == AIRBOSS.PatternStep.GROOVE_IC or + playerData.step == AIRBOSS.PatternStep.GROOVE_AR or + playerData.step == AIRBOSS.PatternStep.GROOVE_AL or + playerData.step == AIRBOSS.PatternStep.GROOVE_LC or + playerData.step == AIRBOSS.PatternStep.GROOVE_IW then + local lue = self:_Lineup( playerData.unit, true ) + local gle = self:_Glideslope( playerData.unit ) + text = text .. string.format( "\nGamma=%.1f° | Rho=%.1f°", relhead, phi ) + text = text .. string.format( "\nLineUp=%.2f° | GlideSlope=%.2f° | AoA=%.1f Units", lue, gle, self:_AoADeg2Units( playerData, aoa ) ) + local grade, points, analysis = self:_LSOgrade( playerData ) + text = text .. string.format( "\nTgroove=%.1f sec", self:_GetTimeInGroove( playerData ) ) + text = text .. string.format( "\nGrade: %s %.1f PT - %s", grade, points, analysis ) else - text=text..string.format("\nR=%.2f NM | X=%d Z=%d m", UTILS.MetersToNM(rho), dx, dz) - text=text..string.format("\nGamma=%.1f° | Rho=%.1f°", relhead, phi) + text = text .. string.format( "\nR=%.2f NM | X=%d Z=%d m", UTILS.MetersToNM( rho ), dx, dz ) + text = text .. string.format( "\nGamma=%.1f° | Rho=%.1f°", relhead, phi ) end - MESSAGE:New(text, 1, nil , true):ToClient(playerData.client) + MESSAGE:New( text, 1, nil, true ):ToClient( playerData.client ) end --- Get glide slope of aircraft unit. @@ -108772,34 +115273,34 @@ end -- @param Wrapper.Unit#UNIT unit Aircraft unit. -- @param #number optangle (Optional) Return glide slope relative to this angle, i.e. the error from the optimal glide slope ~3.5 degrees. -- @return #number Glide slope angle in degrees measured from the deck of the carrier and third wire. -function AIRBOSS:_Glideslope(unit, optangle) +function AIRBOSS:_Glideslope( unit, optangle ) - if optangle==nil then - if unit:GetTypeName()==AIRBOSS.AircraftCarrier.AV8B then - optangle=3.0 + if optangle == nil then + if unit:GetTypeName() == AIRBOSS.AircraftCarrier.AV8B then + optangle = 3.0 else - optangle=3.5 + optangle = 3.5 end end - -- Landing coordinate - local landingcoord=self:_GetOptLandingCoordinate() + -- Landing coordinate + local landingcoord = self:_GetOptLandingCoordinate() -- Distance from stern to aircraft. - local x=unit:GetCoordinate():Get2DDistance(landingcoord) + local x = unit:GetCoordinate():Get2DDistance( landingcoord ) -- Altitude of unit corrected by the deck height of the carrier. - local h=self:_GetAltCarrier(unit) + local h = self:_GetAltCarrier( unit ) -- Harrier should be 40-50 ft above the deck. - if unit:GetTypeName()==AIRBOSS.AircraftCarrier.AV8B then - h=unit:GetAltitude()-(UTILS.FeetToMeters(50)+self.carrierparam.deckheight+2) + if unit:GetTypeName() == AIRBOSS.AircraftCarrier.AV8B then + h = unit:GetAltitude() - (UTILS.FeetToMeters( 50 ) + self.carrierparam.deckheight + 2) end -- Glide slope. - local glideslope=math.atan(h/x) + local glideslope = math.atan( h / x ) -- Glide slope (error) in degrees. - local gs=math.deg(glideslope)-optangle + local gs = math.deg( glideslope ) - optangle return gs end @@ -108809,37 +115310,37 @@ end -- @param Wrapper.Unit#UNIT unit Aircraft unit. -- @param #number optangle (Optional) Return glide slope relative to this angle, i.e. the error from the optimal glide slope ~3.5 degrees. -- @return #number Glide slope angle in degrees measured from the deck of the carrier and third wire. -function AIRBOSS:_Glideslope2(unit, optangle) +function AIRBOSS:_Glideslope2( unit, optangle ) - if optangle==nil then - if unit:GetTypeName()==AIRBOSS.AircraftCarrier.AV8B then - optangle=3.0 + if optangle == nil then + if unit:GetTypeName() == AIRBOSS.AircraftCarrier.AV8B then + optangle = 3.0 else - optangle=3.5 + optangle = 3.5 end end - -- Landing coordinate - local landingcoord=self:_GetOptLandingCoordinate() + -- Landing coordinate + local landingcoord = self:_GetOptLandingCoordinate() -- Distance from stern to aircraft. - local x=unit:GetCoordinate():Get3DDistance(landingcoord) + local x = unit:GetCoordinate():Get3DDistance( landingcoord ) -- Altitude of unit corrected by the deck height of the carrier. - local h=self:_GetAltCarrier(unit) + local h = self:_GetAltCarrier( unit ) -- Harrier should be 40-50 ft above the deck. - if unit:GetTypeName()==AIRBOSS.AircraftCarrier.AV8B then - h=unit:GetAltitude()-(UTILS.FeetToMeters(50)+self.carrierparam.deckheight+2) + if unit:GetTypeName() == AIRBOSS.AircraftCarrier.AV8B then + h = unit:GetAltitude() - (UTILS.FeetToMeters( 50 ) + self.carrierparam.deckheight + 2) end -- Glide slope. - local glideslope=math.asin(h/x) + local glideslope = math.asin( h / x ) -- Glide slope (error) in degrees. - local gs=math.deg(glideslope)-optangle + local gs = math.deg( glideslope ) - optangle -- Debug. - self:T3(self.lid..string.format("Glide slope error = %.1f, x=%.1f h=%.1f", gs, x, h)) + self:T3( self.lid .. string.format( "Glide slope error = %.1f, x=%.1f h=%.1f", gs, x, h ) ) return gs end @@ -108849,49 +115350,49 @@ end -- @param Wrapper.Unit#UNIT unit Aircraft unit. -- @param #boolean runway If true, include angled runway. -- @return #number Line up with runway heading in degrees. 0 degrees = perfect line up. +1 too far left. -1 too far right. -function AIRBOSS:_Lineup(unit, runway) +function AIRBOSS:_Lineup( unit, runway ) -- Landing coordinate - local landingcoord=self:_GetOptLandingCoordinate() + local landingcoord = self:_GetOptLandingCoordinate() -- Vector to landing coord. - local A=landingcoord:GetVec3() + local A = landingcoord:GetVec3() -- Vector to player. - local B=unit:GetVec3() + local B = unit:GetVec3() -- Vector from player to carrier. - local C=UTILS.VecSubstract(A, B) + local C = UTILS.VecSubstract( A, B ) -- Only in 2D plane. - C.y=0.0 + C.y = 0.0 -- Orientation of carrier. - local X=self.carrier:GetOrientationX() - X.y=0.0 + local X = self.carrier:GetOrientationX() + X.y = 0.0 -- Rotate orientation to angled runway. if runway then - X=UTILS.Rotate2D(X, -self.carrierparam.rwyangle) + X = UTILS.Rotate2D( X, -self.carrierparam.rwyangle ) end -- Projection of player pos on x component. - local x=UTILS.VecDot(X, C) + local x = UTILS.VecDot( X, C ) -- Orientation of carrier. - local Z=self.carrier:GetOrientationZ() - Z.y=0.0 + local Z = self.carrier:GetOrientationZ() + Z.y = 0.0 -- Rotate orientation to angled runway. if runway then - Z=UTILS.Rotate2D(Z, -self.carrierparam.rwyangle) + Z = UTILS.Rotate2D( Z, -self.carrierparam.rwyangle ) end -- Projection of player pos on z component. - local z=UTILS.VecDot(Z, C) + local z = UTILS.VecDot( Z, C ) --- - local lineup=math.deg(math.atan2(z, x)) + local lineup = math.deg( math.atan2( z, x ) ) return lineup end @@ -108900,67 +115401,59 @@ end -- @param #AIRBOSS self -- @param Wrapper.Unit#UNIT unit Aircraft unit. -- @return #number Altitude in meters wrt carrier height. -function AIRBOSS:_GetAltCarrier(unit) +function AIRBOSS:_GetAltCarrier( unit ) -- TODO: Value 4 meters is for the Hornet. Adjust for Harrier, A4E and -- Altitude of unit corrected by the deck height of the carrier. - local h=unit:GetAltitude()-self.carrierparam.deckheight-2 + local h = unit:GetAltitude() - self.carrierparam.deckheight - 2 return h end ---- Get optimal landing position of the aircraft. Usually between second and third wire. In case of Tarawa and America we take the abeam landing spot 120 ft abeam the 7.5 position, for the Juan Carlos I it is 120 ft and abeam the 5 position. +--- Get optimal landing position of the aircraft. Usually between second and third wire. In case of Tarawa, Canberrra, Juan Carlos and America we take the abeam landing spot 120 ft above and 21 ft abeam the 7.5 position, for the Juan Carlos I and HMS Hermes it is 120 ft above and 21 ft abeam the 5 position. For CASE III it is 120ft directly above the landing spot. -- @param #AIRBOSS self -- @return Core.Point#COORDINATE Optimal landing coordinate. function AIRBOSS:_GetOptLandingCoordinate() -- Start with stern coordiante. - self.landingcoord:UpdateFromCoordinate(self:_GetSternCoord()) + self.landingcoord:UpdateFromCoordinate( self:_GetSternCoord() ) -- Stern coordinate. - --local stern=self:_GetSternCoord() - + -- local stern=self:_GetSternCoord() -- Final bearing. + local FB=self:GetFinalBearing(false) - - if self.carriertype==AIRBOSS.CarrierType.TARAWA then + local case=self.case + -- set Case III V/STOL abeam landing spot over deck -- Pene Testing + if self.carriertype==AIRBOSS.CarrierType.HERMES or self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS or self.carriertype==AIRBOSS.CarrierType.CANBERRA then + + if case==3 then + self.landingcoord:UpdateFromCoordinate(self:_GetLandingSpotCoordinate()) + -- Altitude 120ft -- is this corect for Case III? + self.landingcoord:SetAltitude(UTILS.FeetToMeters(120)) + + elseif case==2 or case==1 then -- Landing 100 ft abeam, 120 ft alt. - self.landingcoord:UpdateFromCoordinate(self:_GetLandingSpotCoordinate()):Translate(35, FB-90, true, true) + self.landingcoord:UpdateFromCoordinate(self:_GetLandingSpotCoordinate()):Translate(35, FB-90, true, true) --stern=self:_GetLandingSpotCoordinate():Translate(35, FB-90) -- Alitude 120 ft. self.landingcoord:SetAltitude(UTILS.FeetToMeters(120)) - elseif self.carriertype==AIRBOSS.CarrierType.AMERICA then - - -- Landing 100 ft abeam, 120 ft alt. To allow adjustments to match different deck configurations. - self.landingcoord:UpdateFromCoordinate(self:_GetLandingSpotCoordinate()):Translate(35, FB-90, true, true) - --stern=self:_GetLandingSpotCoordinate():Translate(35, FB-90) - - -- Alitude 120 ft. - self.landingcoord:SetAltitude(UTILS.FeetToMeters(120)) - - elseif self.carriertype==AIRBOSS.CarrierType.JCARLOS then - - -- Landing 100 ft abeam, 120 ft alt. - self.landingcoord:UpdateFromCoordinate(self:_GetLandingSpotCoordinate()):Translate(35, FB-100, true, true) - --stern=self:_GetLandingSpotCoordinate():Translate(35, FB-100) - - -- Alitude 120 ft. - self.landingcoord:SetAltitude(UTILS.FeetToMeters(120)) - + end + else -- Ideally we want to land between 2nd and 3rd wire. if self.carrierparam.wire3 then -- We take the position of the 3rd wire to approximately account for the length of the aircraft. - local w3=self.carrierparam.wire3 - self.landingcoord:Translate(w3, FB, true, true) + local w3 = self.carrierparam.wire3 + self.landingcoord:Translate( w3, FB, true, true ) end -- Add 2 meters to account for aircraft height. - self.landingcoord.y=self.landingcoord.y+2 + self.landingcoord.y = self.landingcoord.y + 2 end @@ -108972,34 +115465,48 @@ end -- @return Core.Point#COORDINATE Primary landing spot coordinate. function AIRBOSS:_GetLandingSpotCoordinate() - self.landingspotcoord:UpdateFromCoordinate(self:_GetSternCoord()) + self.landingspotcoord:UpdateFromCoordinate( self:_GetSternCoord() ) -- Stern coordinate. - --local stern=self:_GetSternCoord() + -- local stern=self:_GetSternCoord() - if self.carriertype==AIRBOSS.CarrierType.TARAWA then + if self.carriertype==AIRBOSS.CarrierType.HERMES then + + -- Landing 100 ft abeam, 100 alt. + local hdg = self:GetHeading() + + -- Primary landing spot 5 + self.landingspotcoord:Translate( 69, hdg, true, true ):SetAltitude( self.carrierparam.deckheight ) + elseif self.carriertype == AIRBOSS.CarrierType.TARAWA then -- Landing 100 ft abeam, 120 alt. - local hdg=self:GetHeading() + local hdg = self:GetHeading() -- Primary landing spot 7.5 - self.landingspotcoord:Translate(57, hdg, true, true):SetAltitude(self.carrierparam.deckheight) - elseif self.carriertype==AIRBOSS.CarrierType.AMERICA then + self.landingspotcoord:Translate( 57, hdg, true, true ):SetAltitude( self.carrierparam.deckheight ) + elseif self.carriertype == AIRBOSS.CarrierType.AMERICA then -- Landing 100 ft abeam, 120 alt. - local hdg=self:GetHeading() + local hdg = self:GetHeading() -- Primary landing spot 7.5 a little further forwad on the America - self.landingspotcoord:Translate(59, hdg, true, true):SetAltitude(self.carrierparam.deckheight) + self.landingspotcoord:Translate( 59, hdg, true, true ):SetAltitude( self.carrierparam.deckheight ) - elseif self.carriertype==AIRBOSS.CarrierType.JCARLOS then + elseif self.carriertype == AIRBOSS.CarrierType.JCARLOS then -- Landing 100 ft abeam, 120 alt. - local hdg=self:GetHeading() + local hdg = self:GetHeading() - -- Primary landing spot 5.0 -- TODO voice for different landing Spots. - self.landingspotcoord:Translate(89, hdg, true, true):SetAltitude(self.carrierparam.deckheight) + -- Primary landing spot 5.0 -- Done voice for different landing Spots. + self.landingspotcoord:Translate( 89, hdg, true, true ):SetAltitude( self.carrierparam.deckheight ) + elseif self.carriertype == AIRBOSS.CarrierType.CANBERRA then + + -- Landing 100 ft abeam, 120 alt. + local hdg = self:GetHeading() + + -- Primary landing spot 5.0 -- Done voice for different landing Spots. + self.landingspotcoord:Translate( 89, hdg, true, true ):SetAltitude( self.carrierparam.deckheight ) end return self.landingspotcoord @@ -109009,20 +115516,20 @@ end -- @param #AIRBOSS self -- @param #boolean magnetic If true, calculate magnetic heading. By default true heading is returned. -- @return #number Carrier heading in degrees. -function AIRBOSS:GetHeading(magnetic) - self:F3({magnetic=magnetic}) +function AIRBOSS:GetHeading( magnetic ) + self:F3( { magnetic = magnetic } ) -- Carrier heading - local hdg=self.carrier:GetHeading() + local hdg = self.carrier:GetHeading() -- Include magnetic declination. if magnetic then - hdg=hdg-self.magvar + hdg = hdg - self.magvar end -- Adjust negative values. - if hdg<0 then - hdg=hdg+360 + if hdg < 0 then + hdg = hdg + 360 end return hdg @@ -109033,30 +115540,30 @@ end -- @param #AIRBOSS self -- @return #number BRC in degrees. function AIRBOSS:GetBRC() - return self:GetHeading(true) + return self:GetHeading( true ) end --- Get wind direction and speed at carrier position. -- @param #AIRBOSS self --- @param #number alt Altitude ASL in meters. Default 50 m. +-- @param #number alt Altitude ASL in meters. Default 15 m. -- @param #boolean magnetic Direction including magnetic declination. -- @param Core.Point#COORDINATE coord (Optional) Coordinate at which to get the wind. Default is current carrier position. -- @return #number Direction the wind is blowing **from** in degrees. -- @return #number Wind speed in m/s. -function AIRBOSS:GetWind(alt, magnetic, coord) +function AIRBOSS:GetWind( alt, magnetic, coord ) -- Current position of the carrier or input. - local cv=coord or self:GetCoordinate() + local cv = coord or self:GetCoordinate() -- Wind direction and speed. By default at 50 meters ASL. - local Wdir, Wspeed=cv:GetWind(alt or 50) + local Wdir, Wspeed = cv:GetWind( alt or 15 ) -- Include magnetic declination. if magnetic then - Wdir=Wdir-self.magvar + Wdir = Wdir - self.magvar -- Adjust negative values. - if Wdir<0 then - Wdir=Wdir+360 + if Wdir < 0 then + Wdir = Wdir + 360 end end @@ -109069,72 +115576,71 @@ end -- @return #number Wind component parallel to runway im m/s. -- @return #number Wind component perpendicular to runway in m/s. -- @return #number Total wind strength in m/s. -function AIRBOSS:GetWindOnDeck(alt) +function AIRBOSS:GetWindOnDeck( alt ) -- Position of carrier. - local cv=self:GetCoordinate() + local cv = self:GetCoordinate() -- Velocity vector of carrier. - local vc=self.carrier:GetVelocityVec3() + local vc = self.carrier:GetVelocityVec3() -- Carrier orientation X. - local xc=self.carrier:GetOrientationX() + local xc = self.carrier:GetOrientationX() -- Carrier orientation Z. - local zc=self.carrier:GetOrientationZ() + local zc = self.carrier:GetOrientationZ() -- Rotate back so that angled deck points to wind. - xc=UTILS.Rotate2D(xc, -self.carrierparam.rwyangle) - zc=UTILS.Rotate2D(zc, -self.carrierparam.rwyangle) + xc = UTILS.Rotate2D( xc, -self.carrierparam.rwyangle ) + zc = UTILS.Rotate2D( zc, -self.carrierparam.rwyangle ) -- Wind (from) vector - local vw=cv:GetWindWithTurbulenceVec3(alt or 15) + local vw = cv:GetWindWithTurbulenceVec3( alt or 15 ) -- Total wind velocity vector. -- Carrier velocity has to be negative. If carrier drives in the direction the wind is blowing from, we have less wind in total. - local vT=UTILS.VecSubstract(vw, vc) + local vT = UTILS.VecSubstract( vw, vc ) -- || Parallel component. - local vpa=UTILS.VecDot(vT,xc) + local vpa = UTILS.VecDot( vT, xc ) -- == Perpendicular component. - local vpp=UTILS.VecDot(vT,zc) + local vpp = UTILS.VecDot( vT, zc ) -- Strength. - local vabs=UTILS.VecNorm(vT) + local vabs = UTILS.VecNorm( vT ) -- We return positive values as head wind and negative values as tail wind. - --TODO: Check minus sign. + -- TODO: Check minus sign. return -vpa, vpp, vabs end - --- Get true (or magnetic) heading of carrier into the wind. This accounts for the angled runway. -- @param #AIRBOSS self -- @param #boolean magnetic If true, calculate magnetic heading. By default true heading is returned. -- @param Core.Point#COORDINATE coord (Optional) Coodinate from which heading is calculated. Default is current carrier position. -- @return #number Carrier heading in degrees. -function AIRBOSS:GetHeadingIntoWind(magnetic, coord) +function AIRBOSS:GetHeadingIntoWind( magnetic, coord ) -- Get direction the wind is blowing from. This is where we want to go. - local windfrom, vwind=self:GetWind(nil, nil, coord) + local windfrom, vwind = self:GetWind( nil, nil, coord ) -- Actually, we want the runway in the wind. - local intowind=windfrom-self.carrierparam.rwyangle + local intowind = windfrom - self.carrierparam.rwyangle -- If no wind, take current heading. - if vwind<0.1 then - intowind=self:GetHeading() + if vwind < 0.1 then + intowind = self:GetHeading() end -- Magnetic heading. if magnetic then - intowind=intowind-self.magvar + intowind = intowind - self.magvar end -- Adjust negative values. - if intowind<0 then - intowind=intowind+360 + if intowind < 0 then + intowind = intowind + 360 end return intowind @@ -109146,27 +115652,26 @@ end -- @return #number BRC into the wind in degrees. function AIRBOSS:GetBRCintoWind() -- BRC is the magnetic heading. - return self:GetHeadingIntoWind(true) + return self:GetHeadingIntoWind( true ) end - --- Get final bearing (FB) of carrier. -- By default, the routine returns the magnetic FB depending on the current map (Caucasus, NTTR, Normandy, Persion Gulf etc). -- The true bearing can be obtained by setting the *TrueNorth* parameter to true. -- @param #AIRBOSS self -- @param #boolean magnetic If true, magnetic FB is returned. -- @return #number FB in degrees. -function AIRBOSS:GetFinalBearing(magnetic) +function AIRBOSS:GetFinalBearing( magnetic ) -- First get the heading. - local fb=self:GetHeading(magnetic) + local fb = self:GetHeading( magnetic ) -- Final baring = BRC including angled deck. - fb=fb+self.carrierparam.rwyangle + fb = fb + self.carrierparam.rwyangle -- Adjust negative values. - if fb<0 then - fb=fb+360 + if fb < 0 then + fb = fb + 360 end return fb @@ -109184,56 +115689,56 @@ end -- @param #boolean offset If true, inlcude holding offset. -- @param #boolean inverse Return inverse, i.e. radial-180 degrees. -- @return #number Radial in degrees. -function AIRBOSS:GetRadial(case, magnetic, offset, inverse) +function AIRBOSS:GetRadial( case, magnetic, offset, inverse ) -- Case or current case. - case=case or self.case + case = case or self.case -- Radial. local radial -- Select case. - if case==1 then + if case == 1 then -- Get radial. - radial=self:GetFinalBearing(magnetic)-180 + radial = self:GetFinalBearing( magnetic ) - 180 - elseif case==2 then + elseif case == 2 then -- Radial wrt to heading of carrier. - radial=self:GetHeading(magnetic)-180 + radial = self:GetHeading( magnetic ) - 180 -- Holding offset angle (+-15 or 30 degrees usually) if offset then - radial=radial+self.holdingoffset + radial = radial + self.holdingoffset end - elseif case==3 then + elseif case == 3 then -- Radial wrt angled runway. - radial=self:GetFinalBearing(magnetic)-180 + radial = self:GetFinalBearing( magnetic ) - 180 -- Holding offset angle (+-15 or 30 degrees usually) if offset then - radial=radial+self.holdingoffset + radial = radial + self.holdingoffset end end -- Adjust for negative values. - if radial<0 then - radial=radial+360 + if radial < 0 then + radial = radial + 360 end -- Inverse? if inverse then -- Inverse radial - radial=radial-180 + radial = radial - 180 -- Adjust for negative values. - if radial<0 then - radial=radial+360 + if radial < 0 then + radial = radial + 360 end end @@ -109246,19 +115751,19 @@ end -- @param #number hdg1 Heading one. -- @param #number hdg2 Heading two. -- @return #number Difference between the two headings in degrees. -function AIRBOSS:_GetDeltaHeading(hdg1, hdg2) +function AIRBOSS:_GetDeltaHeading( hdg1, hdg2 ) - local V={} --DCS#Vec3 - V.x=math.cos(math.rad(hdg1)) - V.y=0 - V.z=math.sin(math.rad(hdg1)) + local V = {} -- DCS#Vec3 + V.x = math.cos( math.rad( hdg1 ) ) + V.y = 0 + V.z = math.sin( math.rad( hdg1 ) ) - local W={} --DCS#Vec3 - W.x=math.cos(math.rad(hdg2)) - W.y=0 - W.z=math.sin(math.rad(hdg2)) + local W = {} -- DCS#Vec3 + W.x = math.cos( math.rad( hdg2 ) ) + W.y = 0 + W.z = math.sin( math.rad( hdg2 ) ) - local alpha=UTILS.VecAngle(V,W) + local alpha = UTILS.VecAngle( V, W ) return alpha end @@ -109270,24 +115775,25 @@ end -- @param Wrapper.Unit#UNIT unit Player unit. -- @param #boolean runway (Optional) If true, return relative heading of unit wrt to angled runway of the carrier. -- @return #number Relative heading in degrees. An angle of 0 means, unit fly parallel to carrier. An angle of + or - 90 degrees means, unit flies perpendicular to carrier. -function AIRBOSS:_GetRelativeHeading(unit, runway) +function AIRBOSS:_GetRelativeHeading( unit, runway ) -- Direction vector of the carrier. - local vC=self.carrier:GetOrientationX() + local vC = self.carrier:GetOrientationX() -- Include runway angle. if runway then - vC=UTILS.Rotate2D(vC, -self.carrierparam.rwyangle) + vC = UTILS.Rotate2D( vC, -self.carrierparam.rwyangle ) end -- Direction vector of the unit. - local vP=unit:GetOrientationX() + local vP = unit:GetOrientationX() -- We only want the X-Z plane. Aircraft could fly parallel but ballistic and we dont want the "pitch" angle. - vC.y=0 ; vP.y=0 + vC.y = 0; + vP.y = 0 -- Get angle between the two orientation vectors in degrees. - local rhdg=UTILS.VecAngle(vC,vP) + local rhdg = UTILS.VecAngle( vC, vP ) -- Return heading in degrees. return rhdg @@ -109297,20 +115803,20 @@ end -- @param #AIRBOSS self -- @param Wrapper.Unit#UNIT unit Player unit. -- @return #number Relative velocity in m/s. -function AIRBOSS:_GetRelativeVelocity(unit) +function AIRBOSS:_GetRelativeVelocity( unit ) - local vC=self.carrier:GetVelocityVec3() - local vP=unit:GetVelocityVec3() + local vC = self.carrier:GetVelocityVec3() + local vP = unit:GetVelocityVec3() -- Only X-Z plane is necessary here. - vC.y=0 ; vP.y=0 + vC.y = 0; + vP.y = 0 - local v=UTILS.VecSubstract(vP, vC) + local v = UTILS.VecSubstract( vP, vC ) - return UTILS.VecNorm(v),v + return UTILS.VecNorm( v ), v end - --- Calculate distances between carrier and aircraft unit. -- @param #AIRBOSS self -- @param Wrapper.Unit#UNIT unit Aircraft unit. @@ -109318,42 +115824,41 @@ end -- @return #number Distance [m] perpendicular to the orientation of the carrier. -- @return #number Distance [m] to the carrier. -- @return #number Angle [Deg] from carrier to plane. Phi=0 if the plane is directly behind the carrier, phi=90 if the plane is starboard, phi=180 if the plane is in front of the carrier. -function AIRBOSS:_GetDistances(unit) +function AIRBOSS:_GetDistances( unit ) -- Vector to carrier - local a=self.carrier:GetVec3() + local a = self.carrier:GetVec3() -- Vector to player - local b=unit:GetVec3() + local b = unit:GetVec3() -- Vector from carrier to player. - local c={x=b.x-a.x, y=0, z=b.z-a.z} + local c = { x = b.x - a.x, y = 0, z = b.z - a.z } -- Orientation of carrier. - local x=self.carrier:GetOrientationX() + local x = self.carrier:GetOrientationX() -- Projection of player pos on x component. - local dx=UTILS.VecDot(x,c) + local dx = UTILS.VecDot( x, c ) -- Orientation of carrier. - local z=self.carrier:GetOrientationZ() + local z = self.carrier:GetOrientationZ() -- Projection of player pos on z component. - local dz=UTILS.VecDot(z,c) + local dz = UTILS.VecDot( z, c ) -- Polar coordinates. - local rho=math.sqrt(dx*dx+dz*dz) - + local rho = math.sqrt( dx * dx + dz * dz ) -- Not exactly sure any more what I wanted to calculate here. - local phi=math.deg(math.atan2(dz,dx)) + local phi = math.deg( math.atan2( dz, dx ) ) -- Correct for negative values. - if phi<0 then - phi=phi+360 + if phi < 0 then + phi = phi + 360 end - return dx,dz,rho,phi + return dx, dz, rho, phi end --- Check limits for reaching next step. @@ -109362,26 +115867,24 @@ end -- @param #number Z Z position of player unit. -- @param #AIRBOSS.Checkpoint check Checkpoint. -- @return #boolean If true, checkpoint condition for next step was reached. -function AIRBOSS:_CheckLimits(X, Z, check) +function AIRBOSS:_CheckLimits( X, Z, check ) -- Limits - local nextXmin=check.LimitXmin==nil or (check.LimitXmin and (check.LimitXmin<0 and X<=check.LimitXmin or check.LimitXmin>=0 and X>=check.LimitXmin)) - local nextXmax=check.LimitXmax==nil or (check.LimitXmax and (check.LimitXmax<0 and X>=check.LimitXmax or check.LimitXmax>=0 and X<=check.LimitXmax)) - local nextZmin=check.LimitZmin==nil or (check.LimitZmin and (check.LimitZmin<0 and Z<=check.LimitZmin or check.LimitZmin>=0 and Z>=check.LimitZmin)) - local nextZmax=check.LimitZmax==nil or (check.LimitZmax and (check.LimitZmax<0 and Z>=check.LimitZmax or check.LimitZmax>=0 and Z<=check.LimitZmax)) + local nextXmin = check.LimitXmin == nil or (check.LimitXmin and (check.LimitXmin < 0 and X <= check.LimitXmin or check.LimitXmin >= 0 and X >= check.LimitXmin)) + local nextXmax = check.LimitXmax == nil or (check.LimitXmax and (check.LimitXmax < 0 and X >= check.LimitXmax or check.LimitXmax >= 0 and X <= check.LimitXmax)) + local nextZmin = check.LimitZmin == nil or (check.LimitZmin and (check.LimitZmin < 0 and Z <= check.LimitZmin or check.LimitZmin >= 0 and Z >= check.LimitZmin)) + local nextZmax = check.LimitZmax == nil or (check.LimitZmax and (check.LimitZmax < 0 and Z >= check.LimitZmax or check.LimitZmax >= 0 and Z <= check.LimitZmax)) -- Proceed to next step if all conditions are fullfilled. - local next=nextXmin and nextXmax and nextZmin and nextZmax + local next = nextXmin and nextXmax and nextZmin and nextZmax -- Debug info. - local text=string.format("step=%s: next=%s: X=%d Xmin=%s Xmax=%s | Z=%d Zmin=%s Zmax=%s", - check.name, tostring(next), X, tostring(check.LimitXmin), tostring(check.LimitXmax), Z, tostring(check.LimitZmin), tostring(check.LimitZmax)) - self:T3(self.lid..text) + local text = string.format( "step=%s: next=%s: X=%d Xmin=%s Xmax=%s | Z=%d Zmin=%s Zmax=%s", check.name, tostring( next ), X, tostring( check.LimitXmin ), tostring( check.LimitXmax ), Z, tostring( check.LimitZmin ), tostring( check.LimitZmax ) ) + self:T3( self.lid .. text ) return next end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- LSO functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -109391,93 +115894,92 @@ end -- @param #AIRBOSS.PlayerData playerData Player data table. -- @param #number glideslopeError Error in degrees. -- @param #number lineupError Error in degrees. -function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) +function AIRBOSS:_LSOadvice( playerData, glideslopeError, lineupError ) -- Advice time. - local advice=0 + local advice = 0 -- Glideslope high/low calls. - if glideslopeError>self.gle.HIGH then --1.5 then + if glideslopeError > self.gle.HIGH then -- 1.5 then -- "You're high!" - self:RadioTransmission(self.LSORadio, self.LSOCall.HIGH, true, nil, nil, true) - advice=advice+self.LSOCall.HIGH.duration - elseif glideslopeError>self.gle.High then --0.8 then + self:RadioTransmission( self.LSORadio, self.LSOCall.HIGH, true, nil, nil, true ) + advice = advice + self.LSOCall.HIGH.duration + elseif glideslopeError > self.gle.High then -- 0.8 then -- "You're high." - self:RadioTransmission(self.LSORadio, self.LSOCall.HIGH, false, nil, nil, true) - advice=advice+self.LSOCall.HIGH.duration - elseif glideslopeErrorself.lue.RIGHT then --3 then + self:RadioTransmission( self.LSORadio, self.LSOCall.COMELEFT, false, nil, nil, true ) + advice = advice + self.LSOCall.COMELEFT.duration + elseif lineupError > self.lue.RIGHT then -- 3 then -- "Right for lineup!" - self:RadioTransmission(self.LSORadio, self.LSOCall.RIGHTFORLINEUP, true, nil, nil, true) - advice=advice+self.LSOCall.RIGHTFORLINEUP.duration - elseif lineupError>self.lue.Right then -- 1 then + self:RadioTransmission( self.LSORadio, self.LSOCall.RIGHTFORLINEUP, true, nil, nil, true ) + advice = advice + self.LSOCall.RIGHTFORLINEUP.duration + elseif lineupError > self.lue.Right then -- 1 then -- "Right for lineup." - self:RadioTransmission(self.LSORadio, self.LSOCall.RIGHTFORLINEUP, false, nil, nil, true) - advice=advice+self.LSOCall.RIGHTFORLINEUP.duration + self:RadioTransmission( self.LSORadio, self.LSOCall.RIGHTFORLINEUP, false, nil, nil, true ) + advice = advice + self.LSOCall.RIGHTFORLINEUP.duration else -- "Good lineup." end -- Get current AoA. - local AOA=playerData.unit:GetAoA() + local AOA = playerData.unit:GetAoA() -- Get aircraft AoA parameters. - local acaoa=self:_GetAircraftAoA(playerData) + local acaoa = self:_GetAircraftAoA( playerData ) -- Speed via AoA - not for the Harrier. - if playerData.actype~=AIRBOSS.AircraftCarrier.AV8B then - if AOA>acaoa.SLOW then + if playerData.actype ~= AIRBOSS.AircraftCarrier.AV8B then + if AOA > acaoa.SLOW then -- "Your're slow!" - self:RadioTransmission(self.LSORadio, self.LSOCall.SLOW, true, nil, nil, true) - advice=advice+self.LSOCall.SLOW.duration - --S=underline("SLO") - elseif AOA>acaoa.Slow then + self:RadioTransmission( self.LSORadio, self.LSOCall.SLOW, true, nil, nil, true ) + advice = advice + self.LSOCall.SLOW.duration + -- S=underline("SLO") + elseif AOA > acaoa.Slow then -- "Your're slow." - self:RadioTransmission(self.LSORadio, self.LSOCall.SLOW, false, nil, nil, true) - advice=advice+self.LSOCall.SLOW.duration - --S="SLO" - elseif AOA>acaoa.OnSpeedMax then + self:RadioTransmission( self.LSORadio, self.LSOCall.SLOW, false, nil, nil, true ) + advice = advice + self.LSOCall.SLOW.duration + -- S="SLO" + elseif AOA > acaoa.OnSpeedMax then -- No call. - --S=little("SLO") - elseif AOA 91 Seconds: SLOW V/STOL (Early hover stop selection) +-- * < 55 seconds: Fast V/STOL +-- * < 75 seconds: OK V/STOL +-- * > 76 Seconds: SLOW V/STOL (Early hover stop selection) +-- +-- If you manage to be between 60.0 and 65.0 seconds in the AV-8B, you will even get and okay underline "\_OK\_" -- -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. -- @return #string LSO grade for time in groove, i.e. \_OK\_, OK, (OK), --. -function AIRBOSS:_EvalGrooveTime(playerData) +function AIRBOSS:_EvalGrooveTime( playerData ) -- Time in groove. - local t=playerData.Tgroove + local t = playerData.Tgroove - local grade="" - if t<9 then - grade="_NESA_" - elseif t<15 then - grade="NESA" - elseif t<19 then - grade="OK Groove" - elseif t<=24 then - grade="(LIG)" - -- Time in groove for AV-8B - elseif playerData.actype==AIRBOSS.AircraftCarrier.AV8B and t<55 then -- VSTOL Late Hover stop selection too fast to Abeam LDG Spot AV-8B. - grade="FAST V/STOL Groove" - elseif playerData.actype==AIRBOSS.AircraftCarrier.AV8B and t<90 then -- VSTOL Operations with AV-8B. - grade="OK V/STOL Groove" - elseif playerData.actype==AIRBOSS.AircraftCarrier.AV8B and t>=91 then -- VSTOL Early Hover stop selection slow to Abeam LDG Spot AV-8B. - grade="SLOW V/STOL Groove" + local grade = "" + if t < 9 then + grade = "_NESA_" + elseif t < 15 then + grade = "NESA" + elseif t < 19 then + grade = "OK Groove" + elseif t <= 24 then + grade = "(LIG)" + -- Time in groove for AV-8B + elseif playerData.actype == AIRBOSS.AircraftCarrier.AV8B and t < 55 then -- VSTOL Late Hover stop selection too fast to Abeam LDG Spot AV-8B. + grade = "FAST V/STOL Groove" + elseif playerData.actype == AIRBOSS.AircraftCarrier.AV8B and t < 75 then -- VSTOL Operations with AV-8B. + grade = "OK V/STOL Groove" + elseif playerData.actype == AIRBOSS.AircraftCarrier.AV8B and t >= 76 then -- VSTOL Early Hover stop selection slow to Abeam LDG Spot AV-8B. + grade = "SLOW V/STOL Groove" else - grade="LIG" + grade = "LIG" end -- The unicorn! - if t>=16.4 and t<=16.6 then - grade="_OK_" + if t >= 16.4 and t <= 16.6 then + grade = "_OK_" end -- V/STOL Unicorn! - if playerData.actype==AIRBOSS.AircraftCarrier.AV8B and (t>=65.0 and t<=75.0) then - grade="_OK_ V/STOL" + if playerData.actype == AIRBOSS.AircraftCarrier.AV8B and (t >= 60.0 and t <= 65.0) then + grade = "_OK_ V/STOL" end return grade @@ -109544,81 +116050,87 @@ end -- @return #string LSO grade, i.g. _OK_, OK, (OK), --, etc. -- @return #number Points. -- @return #string LSO analysis of flight path. -function AIRBOSS:_LSOgrade(playerData) +function AIRBOSS:_LSOgrade( playerData ) --- Count deviations. - local function count(base, pattern) - return select(2, string.gsub(base, pattern, "")) + local function count( base, pattern ) + return select( 2, string.gsub( base, pattern, "" ) ) end -- Analyse flight data and convert to LSO text. - local GXX,nXX=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.XX) - local GIM,nIM=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.IM) - local GIC,nIC=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.IC) - local GAR,nAR=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.AR) + local GXX, nXX = self:_Flightdata2Text( playerData, AIRBOSS.GroovePos.XX ) + local GIM, nIM = self:_Flightdata2Text( playerData, AIRBOSS.GroovePos.IM ) + local GIC, nIC = self:_Flightdata2Text( playerData, AIRBOSS.GroovePos.IC ) + local GAR, nAR = self:_Flightdata2Text( playerData, AIRBOSS.GroovePos.AR ) -- Put everything together. - local G=GXX.." "..GIM.." ".." "..GIC.." "..GAR + local G = GXX .. " " .. GIM .. " " .. " " .. GIC .. " " .. GAR - -- Count number of minor, normal and major deviations. TODO - work on Harrier counts due slower approach speed. + -- Count number of minor, normal and major deviations. local N=nXX+nIM+nIC+nAR + local Nv=nXX+nIM local nL=count(G, '_')/2 local nS=count(G, '%(') local nN=N-nS-nL - - -- Groove time 15-18.99 sec for a unicorn. Or 65-70 for V/STOL unicorn. + local nNv=Nv-nS-nL + + -- Groove time 15-18.99 sec for a unicorn. Or 60-65 for V/STOL unicorn. local Tgroove=playerData.Tgroove local TgrooveUnicorn=Tgroove and (Tgroove>=15.0 and Tgroove<=18.99) or false - local TgrooveVstolUnicorn=Tgroove and (Tgroove>=65.0 and Tgroove<=70.0)and playerData.actype==AIRBOSS.AircraftCarrier.AV8B or false + local TgrooveVstolUnicorn=Tgroove and (Tgroove>=60.0 and Tgroove<=65.0)and playerData.actype==AIRBOSS.AircraftCarrier.AV8B or false local grade local points - if N==0 and (TgrooveUnicorn or TgrooveVstolUnicorn ) then + if N == 0 and (TgrooveUnicorn or TgrooveVstolUnicorn) then -- No deviations, should be REALLY RARE! - grade="_OK_" - points=5.0 - G="Unicorn" + grade = "_OK_" + points = 5.0 + G = "Unicorn" else - -- Add AV-8B Harrier devation allowances due to lower groundspeed and 3x conventional groove time, this allows to maintain LSO tolerances while respecting the deviations are not unsafe. (WIP requires feedback) + -- Add AV-8B Harrier devation allowances due to lower groundspeed and 3x conventional groove time, this allows to maintain LSO tolerances while respecting the deviations are not unsafe.--Pene testing -- Large devaitions still result in a No Grade, A Unicorn still requires a clean pass with no deviation. - if nL>3 and playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + if nL > 1 and playerData.actype==AIRBOSS.AircraftCarrier.AV8B then -- Larger deviations ==> "No grade" 2.0 points. grade="--" points=2.0 - elseif nN>2 and playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + elseif nNv >= 1 and playerData.actype==AIRBOSS.AircraftCarrier.AV8B then -- Only average deviations ==> "Fair Pass" Pass with average deviations and corrections. grade="(OK)" points=3.0 - elseif nL>0 then + elseif nNv < 1 and playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + -- Only minor average deviations ==> "OK" Pass with minor deviations and corrections. (test nNv<=1 and) + grade="OK" + points=4.0 + elseif nL > 0 then -- Larger deviations ==> "No grade" 2.0 points. grade="--" points=2.0 - elseif nN>0 then + elseif nN> 0 then -- No larger but average deviations ==> "Fair Pass" Pass with average deviations and corrections. grade="(OK)" points=3.0 - else + else -- Only minor corrections grade="OK" points=4.0 - end + end -end + end -- Replace" )"( and "__" - G=G:gsub("%)%(", "") - G=G:gsub("__","") + G = G:gsub( "%)%(", "" ) + G = G:gsub( "__", "" ) -- Debug info - local text="LSO grade:\n" - text=text..G.."\n" - text=text.."Grade = "..grade.." points = "..points.."\n" - text=text.."# of total deviations = "..N.."\n" - text=text.."# of large deviations _ = "..nL.."\n" - text=text.."# of normal deviations = "..nN.."\n" - text=text.."# of small deviations ( = "..nS.."\n" - self:T2(self.lid..text) + local text = "LSO grade:\n" + text = text .. G .. "\n" + text = text .. "Grade = " .. grade .. " points = " .. points .. "\n" + text = text .. "# of total deviations = " .. N .. "\n" + text = text .. "# of large deviations _ = " .. nL .. "\n" + text = text .. "# of normal deviations = " .. nN .. "\n" + text = text .. "# of small deviations ( = " .. nS .. "\n" + self:T2( self.lid .. text ) -- Special cases. if playerData.wop then @@ -109628,55 +116140,65 @@ end if playerData.lig then -- Long In the Groove (LIG). -- According to Stingers this is a CUT pass and gives 1.0 points. - grade="WO" - points=1.0 - G="LIG" + grade = "WO" + points = 1.0 + G = "LIG" else -- Other pattern WO - grade="WOP" - points=2.0 - G="n/a" + grade = "WOP" + points = 2.0 + G = "n/a" end elseif playerData.wofd then ----------------------- -- Foul Deck Waveoff -- ----------------------- if playerData.landed then - --AIRBOSS wants to talk to you! - grade="CUT" - points=0.0 + -- AIRBOSS wants to talk to you! + grade = "CUT" + points = 0.0 else - grade="WOFD" - points=-1.0 + grade = "WOFD" + points = -1.0 end - G="n/a" + G = "n/a" elseif playerData.owo then ----------------- -- Own Waveoff -- ----------------- - grade="OWO" - points=2.0 - if N==0 then - G="n/a" + grade = "OWO" + points = 2.0 + if N == 0 then + G = "n/a" end elseif playerData.waveoff then ------------- -- Waveoff -- ------------- if playerData.landed then - --AIRBOSS wants to talk to you! - grade="CUT" - points=0.0 + -- AIRBOSS wants to talk to you! + grade = "CUT" + points = 0.0 else - grade="WO" - points=1.0 + grade = "WO" + points = 1.0 end elseif playerData.boltered then -- Bolter - grade="-- (BOLTER)" - points=2.5 - end + grade = "-- (BOLTER)" + points = 2.5 + elseif not playerData.hover and playerData.actype == AIRBOSS.AircraftCarrier.AV8B then + ------------------------------- + -- AV-8B not cleared to land -- -- Landing clearence is carrier from LC to Landing + ------------------------------- + if playerData.landed then + -- AIRBOSS wants your balls! + grade = "CUT" + points = 0.0 + end + + end return grade, points, G end @@ -109687,175 +116209,171 @@ end -- @param #AIRBOSS.GrooveData fdata Flight data in the groove. -- @return #string LSO grade or empty string if flight data table is nil. -- @return #number Number of deviations from perfect flight path. -function AIRBOSS:_Flightdata2Text(playerData, groovestep) +function AIRBOSS:_Flightdata2Text( playerData, groovestep ) - local function little(text) - return string.format("(%s)",text) + local function little( text ) + return string.format( "(%s)", text ) end - local function underline(text) - return string.format("_%s_", text) + local function underline( text ) + return string.format( "_%s_", text ) end -- Groove Data. - local fdata=playerData.groove[groovestep] --#AIRBOSS.GrooveData + local fdata = playerData.groove[groovestep] -- #AIRBOSS.GrooveData -- No flight data ==> return empty string. - if fdata==nil then - self:T3(self.lid.."Flight data is nil.") + if fdata == nil then + self:T3( self.lid .. "Flight data is nil." ) return "", 0 end -- Flight data. - local step=fdata.Step - local AOA=fdata.AoA - local GSE=fdata.GSE - local LUE=fdata.LUE - local ROL=fdata.Roll + local step = fdata.Step + local AOA = fdata.AoA + local GSE = fdata.GSE + local LUE = fdata.LUE + local ROL = fdata.Roll -- Aircraft specific AoA values. - local acaoa=self:_GetAircraftAoA(playerData) + local acaoa = self:_GetAircraftAoA( playerData ) - --Angled Approach. - local P=nil - if step==AIRBOSS.PatternStep.GROOVE_XX and ROL<=4.0 and playerData.case<3 then - if LUE>self.lue.RIGHT then - P=underline("AA") - elseif - LUE>self.lue.RightMed then - P="AA " - elseif - LUE>self.lue.Right then - P=little("AA") - end + -- Angled Approach. + local P = nil + if step == AIRBOSS.PatternStep.GROOVE_XX and ROL <= 4.0 and playerData.case < 3 then + if LUE > self.lue.RIGHT then + P = underline( "AA" ) + elseif LUE > self.lue.RightMed then + P = "AA " + elseif LUE > self.lue.Right then + P = little( "AA" ) + end end - --Overshoot Start. - local O=nil - if step==AIRBOSS.PatternStep.GROOVE_XX then - if LUEacaoa.SLOW then - S=underline("SLO") - elseif AOA>acaoa.Slow then - S="SLO" - elseif AOA>acaoa.OnSpeedMax then - S=little("SLO") - elseif AOA acaoa.SLOW then + S = underline( "SLO" ) + elseif AOA > acaoa.Slow then + S = "SLO" + elseif AOA > acaoa.OnSpeedMax then + S = little( "SLO" ) + elseif AOA < acaoa.FAST then + S = underline( "F" ) + elseif AOA < acaoa.Fast then + S = "F" + elseif AOA < acaoa.OnSpeedMin then + S = little( "F" ) end -- Glideslope/altitude. Good [-0.3, 0.4] asymmetric! - local A=nil - if GSE>self.gle.HIGH then - A=underline("H") - elseif GSE>self.gle.High then - A="H" - elseif GSE>self.gle._max then - A=little("H") - elseif GSE self.gle.HIGH then + A = underline( "H" ) + elseif GSE > self.gle.High then + A = "H" + elseif GSE > self.gle._max then + A = little( "H" ) + elseif GSE < self.gle.LOW then + A = underline( "LO" ) + elseif GSE < self.gle.Low then + A = "LO" + elseif GSE < self.gle._min then + A = little( "LO" ) end -- Line up. XX Step replaced by Overshoot start (OS). Good [-0.5, 0.5] - local D=nil - if LUE>self.lue.RIGHT then - D=underline("LUL") - elseif LUE>self.lue.Right then - D="LUL" - elseif LUE>self.lue._max then - D=little("LUL") - elseif playerData.case<3 then - if LUE self.lue.RIGHT then + D = underline( "LUL" ) + elseif LUE > self.lue.Right then + D = "LUL" + elseif LUE > self.lue._max then + D = little( "LUL" ) + elseif playerData.case < 3 then + if LUE < self.lue.LEFT and step ~= AIRBOSS.PatternStep.GROOVE_XX then + D = underline( "LUR" ) + elseif LUE < self.lue.Left and step ~= AIRBOSS.PatternStep.GROOVE_XX then + D = "LUR" + elseif LUE < self.lue._min and step ~= AIRBOSS.PatternStep.GROOVE_XX then + D = little( "LUR" ) + end + elseif playerData.case == 3 then + if LUE < self.lue.LEFT then + D = underline( "LUR" ) + elseif LUE < self.lue.Left then + D = "LUR" + elseif LUE < self.lue._min then + D = little( "LUR" ) + end end -- Compile. - local G="" - local n=0 + local G = "" + local n = 0 -- Fly trough. if fdata.FlyThrough then - G=G..fdata.FlyThrough + G = G .. fdata.FlyThrough end -- Angled Approach - doesn't affect score, advisory only. if P then - G=G..P - n=n + G = G .. P + n = n end -- Speed. if S then - G=G..S - n=n+1 + G = G .. S + n = n + 1 end -- Glide slope. if A then - G=G..A - n=n+1 + G = G .. A + n = n + 1 end -- Line up. if D then - G=G..D - n=n+1 + G = G .. D + n = n + 1 end - --Drift in Lineup + -- Drift in Lineup if fdata.Drift then - G=G..fdata.Drift - n=n -- Drift doesn't affect score, advisory only. + G = G .. fdata.Drift + n = n -- Drift doesn't affect score, advisory only. end -- Overshoot. if O then - G=G..O - n=n+1 + G = G .. O + n = n + 1 end -- Add current step. - local step=self:_GS(step) - step=step:gsub("XX","X") - if G~="" then - G=G..step + local step = self:_GS( step ) + step = step:gsub( "XX", "X" ) + if G ~= "" then + G = G .. step end -- Debug info. - local text=string.format("LSO Grade at %s:\n", step) - text=text..string.format("AOA=%.1f\n",AOA) - text=text..string.format("GSE=%.1f\n",GSE) - text=text..string.format("LUE=%.1f\n",LUE) - text=text..string.format("ROL=%.1f\n",ROL) - text=text..G - self:T3(self.lid..text) + local text = string.format( "LSO Grade at %s:\n", step ) + text = text .. string.format( "AOA=%.1f\n", AOA ) + text = text .. string.format( "GSE=%.1f\n", GSE ) + text = text .. string.format( "LUE=%.1f\n", LUE ) + text = text .. string.format( "ROL=%.1f\n", ROL ) + text = text .. G + self:T3( self.lid .. text ) - return G,n + return G, n end --- Get short name of the grove step. @@ -109863,69 +116381,69 @@ end -- @param #string step Player step. -- @param #number n Use -1 for previous or +1 for next. Default 0. -- @return #string Shortcut name "X", "RB", "IM", "AR", "IW". -function AIRBOSS:_GS(step, n) +function AIRBOSS:_GS( step, n ) local gp - n=n or 0 + n = n or 0 - if step==AIRBOSS.PatternStep.FINAL then - gp=AIRBOSS.GroovePos.X0 --"X0" -- Entering the groove. - if n==-1 then - gp=AIRBOSS.GroovePos.X0 -- There is no previous step. - elseif n==1 then - gp=AIRBOSS.GroovePos.XX + if step == AIRBOSS.PatternStep.FINAL then + gp = AIRBOSS.GroovePos.X0 -- "X0" -- Entering the groove. + if n == -1 then + gp = AIRBOSS.GroovePos.X0 -- There is no previous step. + elseif n == 1 then + gp = AIRBOSS.GroovePos.XX end - elseif step==AIRBOSS.PatternStep.GROOVE_XX then - gp=AIRBOSS.GroovePos.XX --"XX" -- Starting the groove. - if n==-1 then - gp=AIRBOSS.GroovePos.X0 - elseif n==1 then - gp=AIRBOSS.GroovePos.IM + elseif step == AIRBOSS.PatternStep.GROOVE_XX then + gp = AIRBOSS.GroovePos.XX -- "XX" -- Starting the groove. + if n == -1 then + gp = AIRBOSS.GroovePos.X0 + elseif n == 1 then + gp = AIRBOSS.GroovePos.IM end - elseif step==AIRBOSS.PatternStep.GROOVE_IM then - gp=AIRBOSS.GroovePos.IM --"IM" -- In the middle. - if n==-1 then - gp=AIRBOSS.GroovePos.XX - elseif n==1 then - gp=AIRBOSS.GroovePos.IC + elseif step == AIRBOSS.PatternStep.GROOVE_IM then + gp = AIRBOSS.GroovePos.IM -- "IM" -- In the middle. + if n == -1 then + gp = AIRBOSS.GroovePos.XX + elseif n == 1 then + gp = AIRBOSS.GroovePos.IC end - elseif step==AIRBOSS.PatternStep.GROOVE_IC then - gp=AIRBOSS.GroovePos.IC --"IC" -- In close. - if n==-1 then - gp=AIRBOSS.GroovePos.IM - elseif n==1 then - gp=AIRBOSS.GroovePos.AR + elseif step == AIRBOSS.PatternStep.GROOVE_IC then + gp = AIRBOSS.GroovePos.IC -- "IC" -- In close. + if n == -1 then + gp = AIRBOSS.GroovePos.IM + elseif n == 1 then + gp = AIRBOSS.GroovePos.AR end - elseif step==AIRBOSS.PatternStep.GROOVE_AR then - gp=AIRBOSS.GroovePos.AR --"AR" -- At the ramp. - if n==-1 then - gp=AIRBOSS.GroovePos.IC - elseif n==1 then - if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then - gp=AIRBOSS.GroovePos.AL + elseif step == AIRBOSS.PatternStep.GROOVE_AR then + gp = AIRBOSS.GroovePos.AR -- "AR" -- At the ramp. + if n == -1 then + gp = AIRBOSS.GroovePos.IC + elseif n == 1 then + if self.carriertype == AIRBOSS.CarrierType.HERMES or self.carriertype == AIRBOSS.CarrierType.TARAWA or self.carriertype == AIRBOSS.CarrierType.AMERICA or self.carriertype == AIRBOSS.CarrierType.JCARLOS or self.carriertype == AIRBOSS.CarrierType.CANBERRA then + gp = AIRBOSS.GroovePos.AL else - gp=AIRBOSS.GroovePos.IW + gp = AIRBOSS.GroovePos.IW end end - elseif step==AIRBOSS.PatternStep.GROOVE_AL then - gp=AIRBOSS.GroovePos.AL --"AL" -- Abeam landing spot. - if n==-1 then - gp=AIRBOSS.GroovePos.AR - elseif n==1 then - gp=AIRBOSS.GroovePos.LC + elseif step == AIRBOSS.PatternStep.GROOVE_AL then + gp = AIRBOSS.GroovePos.AL -- "AL" -- Abeam landing spot. + if n == -1 then + gp = AIRBOSS.GroovePos.AR + elseif n == 1 then + gp = AIRBOSS.GroovePos.LC end - elseif step==AIRBOSS.PatternStep.GROOVE_LC then - gp=AIRBOSS.GroovePos.LC --"LC" -- Level crossing. - if n==-1 then - gp=AIRBOSS.GroovePos.AL - elseif n==1 then - gp=AIRBOSS.GroovePos.LC + elseif step == AIRBOSS.PatternStep.GROOVE_LC then + gp = AIRBOSS.GroovePos.LC -- "LC" -- Level crossing. + if n == -1 then + gp = AIRBOSS.GroovePos.AL + elseif n == 1 then + gp = AIRBOSS.GroovePos.LC end - elseif step==AIRBOSS.PatternStep.GROOVE_IW then - gp=AIRBOSS.GroovePos.IW --"IW" -- In the wires. - if n==-1 then - gp=AIRBOSS.GroovePos.AR - elseif n==1 then - gp=AIRBOSS.GroovePos.IW -- There is no next step. + elseif step == AIRBOSS.PatternStep.GROOVE_IW then + gp = AIRBOSS.GroovePos.IW -- "IW" -- In the wires. + if n == -1 then + gp = AIRBOSS.GroovePos.AR + elseif n == 1 then + gp = AIRBOSS.GroovePos.IW -- There is no next step. end end return gp @@ -109937,21 +116455,21 @@ end -- @param #number Z Z distance player to carrier. -- @param #AIRBOSS.Checkpoint pos Position data limits. -- @return #boolean If true, approach should be aborted. -function AIRBOSS:_CheckAbort(X, Z, pos) +function AIRBOSS:_CheckAbort( X, Z, pos ) - local abort=false - if pos.Xmin and Xpos.Xmax then - self:T(string.format("Xmax: X=%d > %d=Xmax", X, pos.Xmax)) - abort=true - elseif pos.Zmin and Zpos.Zmax then - self:T(string.format("Zmax: Z=%d > %d=Zmax", Z, pos.Zmax)) - abort=true + local abort = false + if pos.Xmin and X < pos.Xmin then + self:T( string.format( "Xmin: X=%d < %d=Xmin", X, pos.Xmin ) ) + abort = true + elseif pos.Xmax and X > pos.Xmax then + self:T( string.format( "Xmax: X=%d > %d=Xmax", X, pos.Xmax ) ) + abort = true + elseif pos.Zmin and Z < pos.Zmin then + self:T( string.format( "Zmin: Z=%d < %d=Zmin", Z, pos.Zmin ) ) + abort = true + elseif pos.Zmax and Z > pos.Zmax then + self:T( string.format( "Zmax: Z=%d > %d=Zmax", Z, pos.Zmax ) ) + abort = true end return abort @@ -109962,58 +116480,58 @@ end -- @param #number X X distance player to carrier. -- @param #number Z Z distance player to carrier. -- @param #AIRBOSS.Checkpoint posData Checkpoint data. -function AIRBOSS:_TooFarOutText(X, Z, posData) +function AIRBOSS:_TooFarOutText( X, Z, posData ) -- Intro. - local text="you are too " + local text = "you are too " -- X text. - local xtext=nil - if posData.Xmin and XposData.Xmax then - if posData.Xmax>=0 then - xtext="far ahead of " + elseif posData.Xmax and X > posData.Xmax then + if posData.Xmax >= 0 then + xtext = "far ahead of " else - xtext="close to " + xtext = "close to " end end -- Z text. - local ztext=nil - if posData.Zmin and ZposData.Zmax then - if posData.Zmax>=0 then - ztext="far starboard of " + elseif posData.Zmax and Z > posData.Zmax then + if posData.Zmax >= 0 then + ztext = "far starboard of " else - ztext="too close to " + ztext = "too close to " end end -- Combine X-Z text. if xtext and ztext then - text=text..xtext.." and "..ztext + text = text .. xtext .. " and " .. ztext elseif xtext then - text=text..xtext + text = text .. xtext elseif ztext then - text=text..ztext + text = text .. ztext end -- Complete the sentence - text=text.."the carrier." + text = text .. "the carrier." -- If no case could be identified. - if xtext==nil and ztext==nil then - text="you are too far from where you should be!" + if xtext == nil and ztext == nil then + text = "you are too far from where you should be!" end return text @@ -110026,33 +116544,33 @@ end -- @param #number Z Z distance player to carrier. -- @param #AIRBOSS.Checkpoint posData Checkpoint data. -- @param #boolean patternwo (Optional) Pattern wave off. -function AIRBOSS:_AbortPattern(playerData, X, Z, posData, patternwo) +function AIRBOSS:_AbortPattern( playerData, X, Z, posData, patternwo ) -- Text where we are wrong. - local text=self:_TooFarOutText(X, Z, posData) + local text = self:_TooFarOutText( X, Z, posData ) -- Debug. - local dtext=string.format("Abort: X=%d Xmin=%s, Xmax=%s | Z=%d Zmin=%s Zmax=%s", X, tostring(posData.Xmin), tostring(posData.Xmax), Z, tostring(posData.Zmin), tostring(posData.Zmax)) - self:T(self.lid..dtext) + local dtext = string.format( "Abort: X=%d Xmin=%s, Xmax=%s | Z=%d Zmin=%s Zmax=%s", X, tostring( posData.Xmin ), tostring( posData.Xmax ), Z, tostring( posData.Zmin ), tostring( posData.Zmax ) ) + self:T( self.lid .. dtext ) -- Message to player. - self:MessageToPlayer(playerData, text, "LSO") + self:MessageToPlayer( playerData, text, "LSO" ) if patternwo then -- Pattern wave off! - playerData.wop=true + playerData.wop = true -- Add to debrief. - self:_AddToDebrief(playerData, string.format("Pattern wave off: %s", text)) + self:_AddToDebrief( playerData, string.format( "Pattern wave off: %s", text ) ) -- Depart and re-enter radio message. -- TODO: Radio should depend on player step. - self:RadioTransmission(self.LSORadio, self.LSOCall.DEPARTANDREENTER, false, 3, nil, nil, true) + self:RadioTransmission( self.LSORadio, self.LSOCall.DEPARTANDREENTER, false, 3, nil, nil, true ) -- Next step debrief. - playerData.step=AIRBOSS.PatternStep.DEBRIEF - playerData.warning=nil + playerData.step = AIRBOSS.PatternStep.DEBRIEF + playerData.warning = nil end end @@ -110062,7 +116580,7 @@ end -- @param #AIRBOSS.PlayerData playerData Player data table. -- @param #number delay Delay before playing sound messages. Default 0 sec. -- @param #boolean soundoff If true, don't play and sound hint. -function AIRBOSS:_PlayerHint(playerData, delay, soundoff) +function AIRBOSS:_PlayerHint( playerData, delay, soundoff ) -- No hint for the pros. if not playerData.showhints then @@ -110070,195 +116588,193 @@ function AIRBOSS:_PlayerHint(playerData, delay, soundoff) end -- Get optimal altitude, distance and speed. - local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) + local alt, aoa, dist, speed = self:_GetAircraftParameters( playerData ) -- Get altitude hint. - local hintAlt,debriefAlt,callAlt=self:_AltitudeCheck(playerData, alt) + local hintAlt, debriefAlt, callAlt = self:_AltitudeCheck( playerData, alt ) -- Get speed hint. - local hintSpeed,debriefSpeed,callSpeed=self:_SpeedCheck(playerData, speed) + local hintSpeed, debriefSpeed, callSpeed = self:_SpeedCheck( playerData, speed ) -- Get AoA hint. - local hintAoA,debriefAoA,callAoA=self:_AoACheck(playerData, aoa) + local hintAoA, debriefAoA, callAoA = self:_AoACheck( playerData, aoa ) -- Get distance to the boat hint. - local hintDist,debriefDist,callDist=self:_DistanceCheck(playerData, dist) + local hintDist, debriefDist, callDist = self:_DistanceCheck( playerData, dist ) -- Message to player. - local hint="" - if hintAlt and hintAlt~="" then - hint=hint.."\n"..hintAlt + local hint = "" + if hintAlt and hintAlt ~= "" then + hint = hint .. "\n" .. hintAlt end - if hintSpeed and hintSpeed~="" then - hint=hint.."\n"..hintSpeed + if hintSpeed and hintSpeed ~= "" then + hint = hint .. "\n" .. hintSpeed end - if hintAoA and hintAoA~="" then - hint=hint.."\n"..hintAoA + if hintAoA and hintAoA ~= "" then + hint = hint .. "\n" .. hintAoA end - if hintDist and hintDist~="" then - hint=hint.."\n"..hintDist + if hintDist and hintDist ~= "" then + hint = hint .. "\n" .. hintDist end -- Debriefing text. - local debrief="" - if debriefAlt and debriefAlt~="" then - debrief=debrief.."\n- "..debriefAlt + local debrief = "" + if debriefAlt and debriefAlt ~= "" then + debrief = debrief .. "\n- " .. debriefAlt end - if debriefSpeed and debriefSpeed~="" then - debrief=debrief.."\n- "..debriefSpeed + if debriefSpeed and debriefSpeed ~= "" then + debrief = debrief .. "\n- " .. debriefSpeed end - if debriefAoA and debriefAoA~="" then - debrief=debrief.."\n- "..debriefAoA + if debriefAoA and debriefAoA ~= "" then + debrief = debrief .. "\n- " .. debriefAoA end - if debriefDist and debriefDist~="" then - debrief=debrief.."\n- "..debriefDist + if debriefDist and debriefDist ~= "" then + debrief = debrief .. "\n- " .. debriefDist end -- Add step to debriefing. - if debrief~="" then - self:_AddToDebrief(playerData, debrief) + if debrief ~= "" then + self:_AddToDebrief( playerData, debrief ) end -- Voice hint. - delay=delay or 0 + delay = delay or 0 if not soundoff then if callAlt then - self:Sound2Player(playerData, self.LSORadio, callAlt, false, delay) - delay=delay+callAlt.duration+0.5 + self:Sound2Player( playerData, self.LSORadio, callAlt, false, delay ) + delay = delay + callAlt.duration + 0.5 end if callSpeed then - self:Sound2Player(playerData, self.LSORadio, callSpeed, false, delay) - delay=delay+callSpeed.duration+0.5 + self:Sound2Player( playerData, self.LSORadio, callSpeed, false, delay ) + delay = delay + callSpeed.duration + 0.5 end if callAoA then - self:Sound2Player(playerData, self.LSORadio, callAoA, false, delay) - delay=delay+callAoA.duration+0.5 + self:Sound2Player( playerData, self.LSORadio, callAoA, false, delay ) + delay = delay + callAoA.duration + 0.5 end if callDist then - self:Sound2Player(playerData, self.LSORadio, callDist, false, delay) - delay=delay+callDist.duration+0.5 + self:Sound2Player( playerData, self.LSORadio, callDist, false, delay ) + delay = delay + callDist.duration + 0.5 end end -- ARC IN info. - if playerData.step==AIRBOSS.PatternStep.ARCIN then + if playerData.step == AIRBOSS.PatternStep.ARCIN then -- Hint turn and set TACAN. - if playerData.difficulty==AIRBOSS.Difficulty.EASY then + if playerData.difficulty == AIRBOSS.Difficulty.EASY then -- Get inverse magnetic radial without offset ==> FB for Case II or BRC for Case III. - local radial=self:GetRadial(playerData.case, true, false, true) - local turn="right" - if self.holdingoffset<0 then - turn="left" + local radial = self:GetRadial( playerData.case, true, false, true ) + local turn = "right" + if self.holdingoffset < 0 then + turn = "left" end - hint=hint..string.format("\nTurn %s and select TACAN %03d°.", turn, radial) + hint = hint .. string.format( "\nTurn %s and select TACAN %03d°.", turn, radial ) end end -- DIRTUP additonal info. - if playerData.step==AIRBOSS.PatternStep.DIRTYUP then - if playerData.difficulty==AIRBOSS.Difficulty.EASY then - if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then - hint=hint.."\nFAF! Checks completed. Nozzles 50°." + if playerData.step == AIRBOSS.PatternStep.DIRTYUP then + if playerData.difficulty == AIRBOSS.Difficulty.EASY then + if playerData.actype == AIRBOSS.AircraftCarrier.AV8B then + hint = hint .. "\nFAF! Checks completed. Nozzles 50°." else - --TODO: Tomcat? - hint=hint.."\nDirty up! Hook, gear and flaps down." + -- TODO: Tomcat? + hint = hint .. "\nDirty up! Hook, gear and flaps down." end end end -- BULLSEYE additonal info. - if playerData.step==AIRBOSS.PatternStep.BULLSEYE then + if playerData.step == AIRBOSS.PatternStep.BULLSEYE then -- Hint follow the needles. - if playerData.difficulty==AIRBOSS.Difficulty.EASY then - if playerData.actype==AIRBOSS.AircraftCarrier.HORNET then - hint=hint..string.format("\nIntercept glideslope and follow the needles.") + if playerData.difficulty == AIRBOSS.Difficulty.EASY then + if playerData.actype == AIRBOSS.AircraftCarrier.HORNET then + hint = hint .. string.format( "\nIntercept glideslope and follow the needles." ) else - hint=hint..string.format("\nIntercept glideslope.") + hint = hint .. string.format( "\nIntercept glideslope." ) end end end -- Message to player. - if hint~="" then - local text=string.format("%s%s", playerData.step, hint) - self:MessageToPlayer(playerData, hint, "AIRBOSS", "") + if hint ~= "" then + local text = string.format( "%s%s", playerData.step, hint ) + self:MessageToPlayer( playerData, hint, "AIRBOSS", "" ) end end - --- Display hint for flight students about the (next) step. Message is displayed after one second. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. -- @param #string step Step for which hint is given. -function AIRBOSS:_StepHint(playerData, step) +function AIRBOSS:_StepHint( playerData, step ) -- Set step. - step=step or playerData.step + step = step or playerData.step -- Message is only for "Flight Students". - if playerData.difficulty==AIRBOSS.Difficulty.EASY and playerData.showhints then + if playerData.difficulty == AIRBOSS.Difficulty.EASY and playerData.showhints then -- Get optimal parameters at step. - local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData, step) + local alt, aoa, dist, speed = self:_GetAircraftParameters( playerData, step ) -- Hint: - local hint="" + local hint = "" -- Altitude. if alt then - hint=hint..string.format("\nAltitude %d ft", UTILS.MetersToFeet(alt)) + hint = hint .. string.format( "\nAltitude %d ft", UTILS.MetersToFeet( alt ) ) end -- AoA. if aoa then - hint=hint..string.format("\nAoA %.1f", self:_AoADeg2Units(playerData, aoa)) + hint = hint .. string.format( "\nAoA %.1f", self:_AoADeg2Units( playerData, aoa ) ) end -- Speed. if speed then - hint=hint..string.format("\nSpeed %d knots", UTILS.MpsToKnots(speed)) + hint = hint .. string.format( "\nSpeed %d knots", UTILS.MpsToKnots( speed ) ) end -- Distance to the boat. if dist then - hint=hint..string.format("\nDistance to the boat %.1f NM", UTILS.MetersToNM(dist)) + hint = hint .. string.format( "\nDistance to the boat %.1f NM", UTILS.MetersToNM( dist ) ) end -- Late break. - if step==AIRBOSS.PatternStep.LATEBREAK then - if playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then - hint=hint.."\nWing Sweep 20°, Gear DOWN < 280 KIAS." + if step == AIRBOSS.PatternStep.LATEBREAK then + if playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B then + hint = hint .. "\nWing Sweep 20°, Gear DOWN < 280 KIAS." end end -- Abeam. - if step==AIRBOSS.PatternStep.ABEAM then - if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then - hint=hint.."\nNozzles 50°-60°. Antiskid OFF. Lights OFF." - elseif playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then - hint=hint.."\nSlats/Flaps EXTENDED < 225 KIAS. DLC SELECTED. Auto Throttle IF DESIRED." + if step == AIRBOSS.PatternStep.ABEAM then + if playerData.actype == AIRBOSS.AircraftCarrier.AV8B then + hint = hint .. "\nNozzles 50°-60°. Antiskid OFF. Lights OFF." + elseif playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B then + hint = hint .. "\nSlats/Flaps EXTENDED < 225 KIAS. DLC SELECTED. Auto Throttle IF DESIRED." else - hint=hint.."\nDirty up! Gear DOWN, flaps DOWN. Check hook down." + hint = hint .. "\nDirty up! Gear DOWN, flaps DOWN. Check hook down." end end -- Check if there was actually anything to tell. - if hint~="" then + if hint ~= "" then -- Compile text if any. - local text=string.format("Optimal setup at next step %s:%s", step, hint) + local text = string.format( "Optimal setup at next step %s:%s", step, hint ) -- Send hint to player. - self:MessageToPlayer(playerData, text, "AIRBOSS", "", nil, false, 1) + self:MessageToPlayer( playerData, text, "AIRBOSS", "", nil, false, 1 ) end end end - --- Evaluate player's altitude at checkpoint. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. @@ -110266,57 +116782,57 @@ end -- @return #string Feedback text. -- @return #string Debriefing text. -- @return #AIRBOSS.RadioCall Radio call. -function AIRBOSS:_AltitudeCheck(playerData, altopt) +function AIRBOSS:_AltitudeCheck( playerData, altopt ) - if altopt==nil then + if altopt == nil then return nil, nil end -- Player altitude. - local altitude=playerData.unit:GetAltitude() + local altitude = playerData.unit:GetAltitude() -- Get relative score. - local lowscore, badscore=self:_GetGoodBadScore(playerData) + local lowscore, badscore = self:_GetGoodBadScore( playerData ) -- Altitude error +-X% - local _error=(altitude-altopt)/altopt*100 + local _error = (altitude - altopt) / altopt * 100 -- Radio call for flight students. - local radiocall=nil --#AIRBOSS.RadioCall + local radiocall = nil -- #AIRBOSS.RadioCall - local hint="" - if _error>badscore then - --hint=string.format("You're high.") - radiocall=self:_NewRadioCall(self.LSOCall.HIGH, "Paddles", "") - elseif _error>lowscore then - --hint= string.format("You're slightly high.") - radiocall=self:_NewRadioCall(self.LSOCall.HIGH, "Paddles", "") - elseif _error<-badscore then - --hint=string.format("You're low. ") - radiocall=self:_NewRadioCall(self.LSOCall.LOW, "Paddles", "") - elseif _error<-lowscore then - --hint=string.format("You're slightly low.") - radiocall=self:_NewRadioCall(self.LSOCall.LOW, "Paddles", "") + local hint = "" + if _error > badscore then + -- hint=string.format("You're high.") + radiocall = self:_NewRadioCall( self.LSOCall.HIGH, "Paddles", "" ) + elseif _error > lowscore then + -- hint= string.format("You're slightly high.") + radiocall = self:_NewRadioCall( self.LSOCall.HIGH, "Paddles", "" ) + elseif _error < -badscore then + -- hint=string.format("You're low. ") + radiocall = self:_NewRadioCall( self.LSOCall.LOW, "Paddles", "" ) + elseif _error < -lowscore then + -- hint=string.format("You're slightly low.") + radiocall = self:_NewRadioCall( self.LSOCall.LOW, "Paddles", "" ) else - hint=string.format("Good altitude. ") + hint = string.format( "Good altitude. " ) end -- Extend or decrease depending on skill. - if playerData.difficulty==AIRBOSS.Difficulty.EASY then + if playerData.difficulty == AIRBOSS.Difficulty.EASY then -- Also inform students about the optimal altitude. - hint=hint..string.format("Optimal altitude is %d ft.", UTILS.MetersToFeet(altopt)) - elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + hint = hint .. string.format( "Optimal altitude is %d ft.", UTILS.MetersToFeet( altopt ) ) + elseif playerData.difficulty == AIRBOSS.Difficulty.NORMAL then -- We keep it short normally. - hint="" - elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then + hint = "" + elseif playerData.difficulty == AIRBOSS.Difficulty.HARD then -- No hint at all for the pros. - hint="" + hint = "" end -- Debrief text. - local debrief=string.format("Altitude %d ft = %d%% deviation from %d ft.", UTILS.MetersToFeet(altitude), _error, UTILS.MetersToFeet(altopt)) + local debrief = string.format( "Altitude %d ft = %d%% deviation from %d ft.", UTILS.MetersToFeet( altitude ), _error, UTILS.MetersToFeet( altopt ) ) - return hint, debrief,radiocall + return hint, debrief, radiocall end --- Score for correct AoA. @@ -110326,65 +116842,65 @@ end -- @return #string Feedback message text or easy and normal difficulty level or nil for hard. -- @return #string Debriefing text. -- @return #AIRBOSS.RadioCall Radio call. -function AIRBOSS:_AoACheck(playerData, optaoa) +function AIRBOSS:_AoACheck( playerData, optaoa ) - if optaoa==nil then + if optaoa == nil then return nil, nil end -- Get relative score. - local lowscore, badscore = self:_GetGoodBadScore(playerData) + local lowscore, badscore = self:_GetGoodBadScore( playerData ) -- Player AoA - local aoa=playerData.unit:GetAoA() + local aoa = playerData.unit:GetAoA() -- Altitude error +-X% - local _error=(aoa-optaoa)/optaoa*100 + local _error = (aoa - optaoa) / optaoa * 100 -- Get aircraft AoA parameters. - local aircraftaoa=self:_GetAircraftAoA(playerData) + local aircraftaoa = self:_GetAircraftAoA( playerData ) - -- Radio call for flight students. - local radiocall=nil --#AIRBOSS.RadioCall + -- Radio call for flight students. + local radiocall = nil -- #AIRBOSS.RadioCall -- Rate aoa. - local hint="" - if aoa>=aircraftaoa.SLOW then - --hint="Your're slow!" - radiocall=self:_NewRadioCall(self.LSOCall.SLOW, "Paddles", "") - elseif aoa>=aircraftaoa.Slow then - --hint="Your're slow." - radiocall=self:_NewRadioCall(self.LSOCall.SLOW, "Paddles", "") - elseif aoa>=aircraftaoa.OnSpeedMax then - hint="Your're a little slow. " - elseif aoa>=aircraftaoa.OnSpeedMin then - hint="You're on speed. " - elseif aoa>=aircraftaoa.Fast then - hint="You're a little fast. " - elseif aoa>=aircraftaoa.FAST then - --hint="Your're fast." - radiocall=self:_NewRadioCall(self.LSOCall.FAST, "Paddles", "") + local hint = "" + if aoa >= aircraftaoa.SLOW then + -- hint="Your're slow!" + radiocall = self:_NewRadioCall( self.LSOCall.SLOW, "Paddles", "" ) + elseif aoa >= aircraftaoa.Slow then + -- hint="Your're slow." + radiocall = self:_NewRadioCall( self.LSOCall.SLOW, "Paddles", "" ) + elseif aoa >= aircraftaoa.OnSpeedMax then + hint = "Your're a little slow. " + elseif aoa >= aircraftaoa.OnSpeedMin then + hint = "You're on speed. " + elseif aoa >= aircraftaoa.Fast then + hint = "You're a little fast. " + elseif aoa >= aircraftaoa.FAST then + -- hint="Your're fast." + radiocall = self:_NewRadioCall( self.LSOCall.FAST, "Paddles", "" ) else - --hint="You're fast!" - radiocall=self:_NewRadioCall(self.LSOCall.FAST, "Paddles", "") + -- hint="You're fast!" + radiocall = self:_NewRadioCall( self.LSOCall.FAST, "Paddles", "" ) end -- Extend or decrease depending on skill. - if playerData.difficulty==AIRBOSS.Difficulty.EASY then + if playerData.difficulty == AIRBOSS.Difficulty.EASY then -- Also inform students about optimal value. - hint=hint..string.format("Optimal AoA is %.1f.", self:_AoADeg2Units(playerData, optaoa)) - elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + hint = hint .. string.format( "Optimal AoA is %.1f.", self:_AoADeg2Units( playerData, optaoa ) ) + elseif playerData.difficulty == AIRBOSS.Difficulty.NORMAL then -- We keep is short normally. - hint="" - elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then + hint = "" + elseif playerData.difficulty == AIRBOSS.Difficulty.HARD then -- No hint at all for the pros. - hint="" + hint = "" end -- Debriefing text. - local debrief=string.format("AoA %.1f = %d%% deviation from %.1f.", self:_AoADeg2Units(playerData, aoa), _error, self:_AoADeg2Units(playerData, optaoa)) + local debrief = string.format( "AoA %.1f = %d%% deviation from %.1f.", self:_AoADeg2Units( playerData, aoa ), _error, self:_AoADeg2Units( playerData, optaoa ) ) - return hint, debrief,radiocall + return hint, debrief, radiocall end --- Evaluate player's speed. @@ -110394,54 +116910,54 @@ end -- @return #string Feedback text. -- @return #string Debriefing text. -- @return #AIRBOSS.RadioCall Radio call. -function AIRBOSS:_SpeedCheck(playerData, speedopt) +function AIRBOSS:_SpeedCheck( playerData, speedopt ) - if speedopt==nil then + if speedopt == nil then return nil, nil end -- Player altitude. - local speed=playerData.unit:GetVelocityMPS() + local speed = playerData.unit:GetVelocityMPS() -- Get relative score. - local lowscore, badscore=self:_GetGoodBadScore(playerData) + local lowscore, badscore = self:_GetGoodBadScore( playerData ) -- Altitude error +-X% - local _error=(speed-speedopt)/speedopt*100 + local _error = (speed - speedopt) / speedopt * 100 - -- Radio call for flight students. - local radiocall=nil --#AIRBOSS.RadioCall + -- Radio call for flight students. + local radiocall = nil -- #AIRBOSS.RadioCall - local hint="" - if _error>badscore then - --hint=string.format("You're fast.") - radiocall=self:_NewRadioCall(self.LSOCall.FAST, "AIRBOSS", "") - elseif _error>lowscore then - --hint= string.format("You're slightly fast.") - radiocall=self:_NewRadioCall(self.LSOCall.FAST, "AIRBOSS", "") - elseif _error<-badscore then - --hint=string.format("You're slow.") - radiocall=self:_NewRadioCall(self.LSOCall.SLOW, "AIRBOSS", "") - elseif _error<-lowscore then - --hint=string.format("You're slightly slow.") - radiocall=self:_NewRadioCall(self.LSOCall.SLOW, "AIRBOSS", "") + local hint = "" + if _error > badscore then + -- hint=string.format("You're fast.") + radiocall = self:_NewRadioCall( self.LSOCall.FAST, "AIRBOSS", "" ) + elseif _error > lowscore then + -- hint= string.format("You're slightly fast.") + radiocall = self:_NewRadioCall( self.LSOCall.FAST, "AIRBOSS", "" ) + elseif _error < -badscore then + -- hint=string.format("You're slow.") + radiocall = self:_NewRadioCall( self.LSOCall.SLOW, "AIRBOSS", "" ) + elseif _error < -lowscore then + -- hint=string.format("You're slightly slow.") + radiocall = self:_NewRadioCall( self.LSOCall.SLOW, "AIRBOSS", "" ) else - hint=string.format("Good speed. ") + hint = string.format( "Good speed. " ) end -- Extend or decrease depending on skill. - if playerData.difficulty==AIRBOSS.Difficulty.EASY then - hint=hint..string.format("Optimal speed is %d knots.", UTILS.MpsToKnots(speedopt)) - elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + if playerData.difficulty == AIRBOSS.Difficulty.EASY then + hint = hint .. string.format( "Optimal speed is %d knots.", UTILS.MpsToKnots( speedopt ) ) + elseif playerData.difficulty == AIRBOSS.Difficulty.NORMAL then -- We keep is short normally. - hint="" - elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then + hint = "" + elseif playerData.difficulty == AIRBOSS.Difficulty.HARD then -- No hint at all for pros. - hint="" + hint = "" end -- Debrief text. - local debrief=string.format("Speed %d knots = %d%% deviation from %d knots.", UTILS.MpsToKnots(speed), _error, UTILS.MpsToKnots(speedopt)) + local debrief = string.format( "Speed %d knots = %d%% deviation from %d knots.", UTILS.MpsToKnots( speed ), _error, UTILS.MpsToKnots( speedopt ) ) return hint, debrief, radiocall end @@ -110453,48 +116969,48 @@ end -- @return #string Feedback message text. -- @return #string Debriefing text. -- @return #AIRBOSS.RadioCall Distance radio call. Not implemented yet. -function AIRBOSS:_DistanceCheck(playerData, optdist) +function AIRBOSS:_DistanceCheck( playerData, optdist ) - if optdist==nil then + if optdist == nil then return nil, nil end -- Distance to carrier. - local distance=playerData.unit:GetCoordinate():Get2DDistance(self:GetCoordinate()) + local distance = playerData.unit:GetCoordinate():Get2DDistance( self:GetCoordinate() ) -- Get relative score. - local lowscore, badscore = self:_GetGoodBadScore(playerData) + local lowscore, badscore = self:_GetGoodBadScore( playerData ) -- Altitude error +-X% - local _error=(distance-optdist)/optdist*100 + local _error = (distance - optdist) / optdist * 100 local hint - if _error>badscore then - hint=string.format("You're too far from the boat!") - elseif _error>lowscore then - hint=string.format("You're slightly too far from the boat.") - elseif _error<-badscore then - hint=string.format( "You're too close to the boat!") - elseif _error<-lowscore then - hint=string.format("You're slightly too far from the boat.") + if _error > badscore then + hint = string.format( "You're too far from the boat!" ) + elseif _error > lowscore then + hint = string.format( "You're slightly too far from the boat." ) + elseif _error < -badscore then + hint = string.format( "You're too close to the boat!" ) + elseif _error < -lowscore then + hint = string.format( "You're slightly too far from the boat." ) else - hint=string.format("Good distance to the boat.") + hint = string.format( "Good distance to the boat." ) end -- Extend or decrease depending on skill. - if playerData.difficulty==AIRBOSS.Difficulty.EASY then + if playerData.difficulty == AIRBOSS.Difficulty.EASY then -- Also inform students about optimal value. - hint=hint..string.format(" Optimal distance is %.1f NM.", UTILS.MetersToNM(optdist)) - elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + hint = hint .. string.format( " Optimal distance is %.1f NM.", UTILS.MetersToNM( optdist ) ) + elseif playerData.difficulty == AIRBOSS.Difficulty.NORMAL then -- We keep it short normally. - hint="" - elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then + hint = "" + elseif playerData.difficulty == AIRBOSS.Difficulty.HARD then -- No hint at all for the pros. - hint="" + hint = "" end -- Debriefing text. - local debrief=string.format("Distance %.1f NM = %d%% deviation from %.1f NM.",UTILS.MetersToNM(distance), _error, UTILS.MetersToNM(optdist)) + local debrief = string.format( "Distance %.1f NM = %d%% deviation from %.1f NM.", UTILS.MetersToNM( distance ), _error, UTILS.MetersToNM( optdist ) ) return hint, debrief, nil end @@ -110508,122 +117024,120 @@ end -- @param #AIRBOSS.PlayerData playerData Player data. -- @param #string hint Debrief text of this step. -- @param #string step (Optional) Current step in the pattern. Default from playerData. -function AIRBOSS:_AddToDebrief(playerData, hint, step) - step=step or playerData.step - table.insert(playerData.debrief, {step=step, hint=hint}) +function AIRBOSS:_AddToDebrief( playerData, hint, step ) + step = step or playerData.step + table.insert( playerData.debrief, { step = step, hint = hint } ) end --- Debrief player and set next step. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. -function AIRBOSS:_Debrief(playerData) - self:F(self.lid..string.format("Debriefing of player %s.", playerData.name)) +function AIRBOSS:_Debrief( playerData ) + self:F( self.lid .. string.format( "Debriefing of player %s.", playerData.name ) ) -- Delete scheduler ID. - playerData.debriefschedulerID=nil + playerData.debriefschedulerID = nil -- Switch attitude monitor off if on. - playerData.attitudemonitor=false + playerData.attitudemonitor = false -- LSO grade, points, and flight data analyis. - local grade, points, analysis=self:_LSOgrade(playerData) + local grade, points, analysis = self:_LSOgrade( playerData ) -- Insert points to table of all points until player landed. - if points and points>=0 then - table.insert(playerData.points, points) + if points and points >= 0 then + table.insert( playerData.points, points ) end -- Player has landed and is not airborne any more. - local Points=0 + local Points = 0 if playerData.landed and not playerData.unit:InAir() then -- Average over all points received so far. - for _,_points in pairs(playerData.points) do - Points=Points+_points + for _, _points in pairs( playerData.points ) do + Points = Points + _points end -- This is the final points. - Points=Points/#playerData.points + Points = Points / #playerData.points -- Reset points array. - playerData.points={} + playerData.points = {} else -- Player boltered or was waved off ==> We display the normal points. - Points=points + Points = points end -- My LSO grade. - local mygrade={} --#AIRBOSS.LSOgrade - mygrade.grade=grade - mygrade.points=points - mygrade.details=analysis - mygrade.wire=playerData.wire - mygrade.Tgroove=playerData.Tgroove + local mygrade = {} -- #AIRBOSS.LSOgrade + mygrade.grade = grade + mygrade.points = points + mygrade.details = analysis + mygrade.wire = playerData.wire + mygrade.Tgroove = playerData.Tgroove if playerData.landed and not playerData.unit:InAir() then - mygrade.finalscore=Points + mygrade.finalscore = Points end - mygrade.case=playerData.case - local windondeck=self:GetWindOnDeck() - mygrade.wind=tostring(UTILS.Round(UTILS.MpsToKnots(windondeck), 1)) - mygrade.modex=playerData.onboard - mygrade.airframe=playerData.actype - mygrade.carriertype=self.carriertype - mygrade.carriername=self.alias - mygrade.theatre=self.theatre - mygrade.mitime=UTILS.SecondsToClock(timer.getAbsTime()) - mygrade.midate=UTILS.GetDCSMissionDate() - mygrade.osdate="n/a" + mygrade.case = playerData.case + local windondeck = self:GetWindOnDeck() + mygrade.wind = tostring( UTILS.Round( UTILS.MpsToKnots( windondeck ), 1 ) ) + mygrade.modex = playerData.onboard + mygrade.airframe = playerData.actype + mygrade.carriertype = self.carriertype + mygrade.carriername = self.alias + mygrade.theatre = self.theatre + mygrade.mitime = UTILS.SecondsToClock( timer.getAbsTime() ) + mygrade.midate = UTILS.GetDCSMissionDate() + mygrade.osdate = "n/a" if os then - mygrade.osdate=os.date() --os.date("%d.%m.%Y") + mygrade.osdate = os.date() -- os.date("%d.%m.%Y") end -- Save trap sheet. if playerData.trapon and self.trapsheet then - self:_SaveTrapSheet(playerData, mygrade) + self:_SaveTrapSheet( playerData, mygrade ) end -- Add LSO grade to player grades table. - table.insert(self.playerscores[playerData.name], mygrade) + table.insert( self.playerscores[playerData.name], mygrade ) -- Trigger grading event. - self:LSOGrade(playerData, mygrade) + self:LSOGrade( playerData, mygrade ) -- LSO grade: (OK) 3.0 PT - LURIM - local text=string.format("%s %.1f PT - %s", grade, Points, analysis) - if Points==-1 then - text=string.format("%s n/a PT - Foul deck", grade, Points, analysis) + local text = string.format( "%s %.1f PT - %s", grade, Points, analysis ) + if Points == -1 then + text = string.format( "%s n/a PT - Foul deck", grade, Points, analysis ) end -- Wire and Groove time only if not pattern WO. if not (playerData.wop or playerData.wofd) then -- Wire trapped. Not if pattern WI. - if playerData.wire and playerData.wire<=4 then - text=text..string.format(" %d-wire", playerData.wire) + if playerData.wire and playerData.wire <= 4 then + text = text .. string.format( " %d-wire", playerData.wire ) end -- Time in the groove. Only Case I/II and not pattern WO. - if playerData.Tgroove and playerData.Tgroove<=360 and playerData.case<3 then - text=text..string.format("\nTime in the groove %.1f seconds: %s", playerData.Tgroove, self:_EvalGrooveTime(playerData)) + if playerData.Tgroove and playerData.Tgroove <= 360 and playerData.case < 3 then + text = text .. string.format( "\nTime in the groove %.1f seconds: %s", playerData.Tgroove, self:_EvalGrooveTime( playerData ) ) end end -- Copy debriefing text. - playerData.lastdebrief=UTILS.DeepCopy(playerData.debrief) + playerData.lastdebrief = UTILS.DeepCopy( playerData.debrief ) -- Info text. - if playerData.difficulty==AIRBOSS.Difficulty.EASY then - text=text..string.format("\nYour detailed debriefing can be found via the F10 radio menu.") + if playerData.difficulty == AIRBOSS.Difficulty.EASY then + text = text .. string.format( "\nYour detailed debriefing can be found via the F10 radio menu." ) end -- Message. - self:MessageToPlayer(playerData, text, "LSO", "", 30, true) - + self:MessageToPlayer( playerData, text, "LSO", "", 30, true ) -- Set step to undefined and check if other cases apply. - playerData.step=AIRBOSS.PatternStep.UNDEFINED - + playerData.step = AIRBOSS.PatternStep.UNDEFINED -- Check what happened? if playerData.wop then @@ -110644,41 +117158,41 @@ function AIRBOSS:_Debrief(playerData) -- Heading and distance tip. local heading, distance - if playerData.case==1 or playerData.case==2 then + if playerData.case == 1 or playerData.case == 2 then -- Next step: Initial again. - playerData.step=AIRBOSS.PatternStep.INITIAL + playerData.step = AIRBOSS.PatternStep.INITIAL -- Create a point 3.0 NM astern for re-entry. - local initial=self:GetCoordinate():Translate(UTILS.NMToMeters(3.5), self:GetRadial(2, false, false, false)) + local initial = self:GetCoordinate():Translate( UTILS.NMToMeters( 3.5 ), self:GetRadial( 2, false, false, false ) ) -- Get heading and distance to initial zone ~3 NM astern. - heading=playerData.unit:GetCoordinate():HeadingTo(initial) - distance=playerData.unit:GetCoordinate():Get2DDistance(initial) + heading = playerData.unit:GetCoordinate():HeadingTo( initial ) + distance = playerData.unit:GetCoordinate():Get2DDistance( initial ) - elseif playerData.case==3 then + elseif playerData.case == 3 then -- Next step? Bullseye for now. -- TODO: Could be DIRTY UP or PLATFORM or even back to MARSHAL STACK? - playerData.step=AIRBOSS.PatternStep.BULLSEYE + playerData.step = AIRBOSS.PatternStep.BULLSEYE -- Get heading and distance to bullseye zone ~3 NM astern. - local zone=self:_GetZoneBullseye(playerData.case) + local zone = self:_GetZoneBullseye( playerData.case ) - heading=playerData.unit:GetCoordinate():HeadingTo(zone:GetCoordinate()) - distance=playerData.unit:GetCoordinate():Get2DDistance(zone:GetCoordinate()) + heading = playerData.unit:GetCoordinate():HeadingTo( zone:GetCoordinate() ) + distance = playerData.unit:GetCoordinate():Get2DDistance( zone:GetCoordinate() ) end -- Re-enter message. - local text=string.format("fly heading %03d° for %d NM to re-enter the pattern.", heading, UTILS.MetersToNM(distance)) - self:MessageToPlayer(playerData, text, "LSO", nil, nil, false, 5) + local text = string.format( "fly heading %03d° for %d NM to re-enter the pattern.", heading, UTILS.MetersToNM( distance ) ) + self:MessageToPlayer( playerData, text, "LSO", nil, nil, false, 5 ) else -- Unit does not seem to be alive! -- TODO: What now? - self:E(self.lid..string.format("ERROR: Player unit not alive!")) + self:E( self.lid .. string.format( "ERROR: Player unit not alive!" ) ) end @@ -110691,16 +117205,16 @@ function AIRBOSS:_Debrief(playerData) if playerData.unit:InAir() then -- Bolter pattern. Then Abeam or bullseye. - playerData.step=AIRBOSS.PatternStep.BOLTER + playerData.step = AIRBOSS.PatternStep.BOLTER else -- Welcome aboard! - self:Sound2Player(playerData, self.LSORadio, self.LSOCall.WELCOMEABOARD) + self:Sound2Player( playerData, self.LSORadio, self.LSOCall.WELCOMEABOARD ) -- Airboss talkto! - local text=string.format("deck was fouled but you landed anyway. Airboss wants to talk to you!") - self:MessageToPlayer(playerData, text, "LSO", nil, nil, false, 3) + local text = string.format( "deck was fouled but you landed anyway. Airboss wants to talk to you!" ) + self:MessageToPlayer( playerData, text, "LSO", nil, nil, false, 3 ) end @@ -110713,18 +117227,17 @@ function AIRBOSS:_Debrief(playerData) if playerData.unit:InAir() then -- Bolter pattern. Then Abeam or bullseye. - playerData.step=AIRBOSS.PatternStep.BOLTER + playerData.step = AIRBOSS.PatternStep.BOLTER else -- Welcome aboard! -- NOTE: This should not happen as owo is only triggered if player flew past the carrier. - self:E(self.lid.."ERROR: player landed when OWO was issues. This should not happen. Please report!") - self:Sound2Player(playerData, self.LSORadio, self.LSOCall.WELCOMEABOARD) + self:E( self.lid .. "ERROR: player landed when OWO was issues. This should not happen. Please report!" ) + self:Sound2Player( playerData, self.LSORadio, self.LSOCall.WELCOMEABOARD ) end - elseif playerData.waveoff then -------------- @@ -110734,16 +117247,16 @@ function AIRBOSS:_Debrief(playerData) if playerData.unit:InAir() then -- Bolter pattern. Then Abeam or bullseye. - playerData.step=AIRBOSS.PatternStep.BOLTER + playerData.step = AIRBOSS.PatternStep.BOLTER else -- Welcome aboard! - self:Sound2Player(playerData, self.LSORadio, self.LSOCall.WELCOMEABOARD) + self:Sound2Player( playerData, self.LSORadio, self.LSOCall.WELCOMEABOARD ) -- Airboss talkto! - local text=string.format("you were waved off but landed anyway. Airboss wants to talk to you!") - self:MessageToPlayer(playerData, text, "LSO", nil, nil, false, 3) + local text = string.format( "you were waved off but landed anyway. Airboss wants to talk to you!" ) + self:MessageToPlayer( playerData, text, "LSO", nil, nil, false, 3 ) end @@ -110756,7 +117269,7 @@ function AIRBOSS:_Debrief(playerData) if playerData.unit:InAir() then -- Bolter pattern. Then Abeam or bullseye. - playerData.step=AIRBOSS.PatternStep.BOLTER + playerData.step = AIRBOSS.PatternStep.BOLTER end @@ -110766,47 +117279,47 @@ function AIRBOSS:_Debrief(playerData) -- Landed -- ------------ - if not playerData.unit:InAir() then + if not playerData.unit:InAir() then -- Welcome aboard! - self:Sound2Player(playerData, self.LSORadio, self.LSOCall.WELCOMEABOARD) + self:Sound2Player( playerData, self.LSORadio, self.LSOCall.WELCOMEABOARD ) end else -- Message to player. - self:MessageToPlayer(playerData, "Undefined state after landing! Please report.", "ERROR", nil, 20) + self:MessageToPlayer( playerData, "Undefined state after landing! Please report.", "ERROR", nil, 20 ) -- Next step. - playerData.step=AIRBOSS.PatternStep.UNDEFINED + playerData.step = AIRBOSS.PatternStep.UNDEFINED end -- Player landed and is not in air anymore. if playerData.landed and not playerData.unit:InAir() then -- Set recovered flag. - self:_RecoveredElement(playerData.unit) + self:_RecoveredElement( playerData.unit ) -- Check if all elements - self:_CheckSectionRecovered(playerData) + self:_CheckSectionRecovered( playerData ) end -- Increase number of passes. - playerData.passes=playerData.passes+1 + playerData.passes = playerData.passes + 1 -- Next step hint for students if any. - self:_StepHint(playerData) + self:_StepHint( playerData ) -- Reinitialize player data for new approach. - self:_InitPlayer(playerData, playerData.step) + self:_InitPlayer( playerData, playerData.step ) -- Debug message. - MESSAGE:New(string.format("Player step %s.", playerData.step), 5, "DEBUG"):ToAllIf(self.Debug) + MESSAGE:New( string.format( "Player step %s.", playerData.step ), 5, "DEBUG" ):ToAllIf( self.Debug ) -- Auto save player results. if self.autosave and mygrade.finalscore then - self:Save(self.autosavepath, self.autosavefile) + self:Save( self.autosavepath, self.autosavefile ) end end @@ -110820,80 +117333,79 @@ end -- @param Core.Point#COORDINATE coordfrom Coordinate from which the collision is check. -- @return #boolean If true, surface type ahead is not deep water. -- @return #number Max free distance in meters. -function AIRBOSS:_CheckCollisionCoord(coordto, coordfrom) +function AIRBOSS:_CheckCollisionCoord( coordto, coordfrom ) -- Increment in meters. - local dx=100 + local dx = 100 -- From coordinate. Default 500 in front of the carrier. - local d=0 + local d = 0 if coordfrom then - d=0 + d = 0 else - d=250 - coordfrom=self:GetCoordinate():Translate(d, self:GetHeading()) + d = 250 + coordfrom = self:GetCoordinate():Translate( d, self:GetHeading() ) end -- Distance between the two coordinates. - local dmax=coordfrom:Get2DDistance(coordto) + local dmax = coordfrom:Get2DDistance( coordto ) -- Direction. - local direction=coordfrom:HeadingTo(coordto) + local direction = coordfrom:HeadingTo( coordto ) -- Scan path between the two coordinates. - local clear=true - while d<=dmax do + local clear = true + while d <= dmax do -- Check point. - local cp=coordfrom:Translate(d, direction) + local cp = coordfrom:Translate( d, direction ) -- Check if surface type is water. if not cp:IsSurfaceTypeWater() then -- Debug mark points. if self.Debug then - local st=cp:GetSurfaceType() - cp:MarkToAll(string.format("Collision check surface type %d", st)) + local st = cp:GetSurfaceType() + cp:MarkToAll( string.format( "Collision check surface type %d", st ) ) end -- Collision WARNING! - clear=false + clear = false break end -- Increase distance. - d=d+dx + d = d + dx end - local text="" + local text = "" if clear then - text=string.format("Path into direction %03d° is clear for the next %.1f NM.", direction, UTILS.MetersToNM(d)) + text = string.format( "Path into direction %03d° is clear for the next %.1f NM.", direction, UTILS.MetersToNM( d ) ) else - text=string.format("Detected obstacle at distance %.1f NM into direction %03d°.", UTILS.MetersToNM(d), direction) + text = string.format( "Detected obstacle at distance %.1f NM into direction %03d°.", UTILS.MetersToNM( d ), direction ) end - self:T2(self.lid..text) + self:T2( self.lid .. text ) return not clear, d end - --- Check Collision. -- @param #AIRBOSS self -- @param Core.Point#COORDINATE fromcoord Coordinate from which the path to the next WP is calculated. Default current carrier position. -- @return #boolean If true, surface type ahead is not deep water. -function AIRBOSS:_CheckFreePathToNextWP(fromcoord) +function AIRBOSS:_CheckFreePathToNextWP( fromcoord ) -- Position. - fromcoord=fromcoord or self:GetCoordinate():Translate(250, self:GetHeading()) + fromcoord = fromcoord or self:GetCoordinate():Translate( 250, self:GetHeading() ) -- Next wp = current+1 (or last) - local Nnextwp=math.min(self.currentwp+1, #self.waypoints) + local Nnextwp = math.min( self.currentwp + 1, #self.waypoints ) -- Next waypoint. - local nextwp=self.waypoints[Nnextwp] --Core.Point#COORDINATE + local nextwp = self.waypoints[Nnextwp] -- Core.Point#COORDINATE -- Check for collision. - local collision=self:_CheckCollisionCoord(nextwp, fromcoord) + local collision = self:_CheckCollisionCoord( nextwp, fromcoord ) return collision end @@ -110903,59 +117415,57 @@ end function AIRBOSS:_Pathfinder() -- Heading and current coordiante. - local hdg=self:GetHeading() - local cv=self:GetCoordinate() + local hdg = self:GetHeading() + local cv = self:GetCoordinate() -- Possible directions. - local directions={-20, 20, -30, 30, -40, 40, -50, 50, -60, 60, -70, 70, -80, 80, -90, 90, -100, 100} + local directions = { -20, 20, -30, 30, -40, 40, -50, 50, -60, 60, -70, 70, -80, 80, -90, 90, -100, 100 } -- Starboard turns up to 90 degrees. - for _,_direction in pairs(directions) do + for _, _direction in pairs( directions ) do -- New direction. - local direction=hdg+_direction + local direction = hdg + _direction -- Check for collisions in the next 20 NM of the current direction. - local _, dfree=self:_CheckCollisionCoord(cv:Translate(UTILS.NMToMeters(20), direction), cv) + local _, dfree = self:_CheckCollisionCoord( cv:Translate( UTILS.NMToMeters( 20 ), direction ), cv ) -- Loop over distances and find the first one which gives a clear path to the next waypoint. - local distance=500 - while distance<=dfree do + local distance = 500 + while distance <= dfree do -- Coordinate from which we calculate the path. - local fromcoord=cv:Translate(distance, direction) + local fromcoord = cv:Translate( distance, direction ) -- Check for collision between point and next waypoint. - local collision=self:_CheckFreePathToNextWP(fromcoord) + local collision = self:_CheckFreePathToNextWP( fromcoord ) -- Debug info. - self:T2(self.lid..string.format("Pathfinder d=%.1f m, direction=%03d°, collision=%s", distance, direction, tostring(collision))) + self:T2( self.lid .. string.format( "Pathfinder d=%.1f m, direction=%03d°, collision=%s", distance, direction, tostring( collision ) ) ) -- If path is clear, we start a little detour. if not collision then - self:CarrierDetour(fromcoord) + self:CarrierDetour( fromcoord ) return end - distance=distance+500 + distance = distance + 500 end end end - --- Carrier resumes the route at its next waypoint. ---@param #AIRBOSS self ---@param Core.Point#COORDINATE gotocoord (Optional) First goto this coordinate before resuming route. ---@return #AIRBOSS self -function AIRBOSS:CarrierResumeRoute(gotocoord) +-- @param #AIRBOSS self +-- @param Core.Point#COORDINATE gotocoord (Optional) First goto this coordinate before resuming route. +-- @return #AIRBOSS self +function AIRBOSS:CarrierResumeRoute( gotocoord ) -- Make carrier resume its route. - AIRBOSS._ResumeRoute(self.carrier:GetGroup(), self, gotocoord) + AIRBOSS._ResumeRoute( self.carrier:GetGroup(), self, gotocoord ) return self end - --- Let the carrier make a detour to a given point. When it reaches the point, it will resume its normal route. -- @param #AIRBOSS self -- @param Core.Point#COORDINATE coord Coordinate of the detour. @@ -110964,70 +117474,70 @@ end -- @param #number uspeed Speed in knots after U-turn. Default is same as before. -- @param Core.Point#COORDINATE tcoord Additional coordinate to make turn smoother. -- @return #AIRBOSS self -function AIRBOSS:CarrierDetour(coord, speed, uturn, uspeed, tcoord) +function AIRBOSS:CarrierDetour( coord, speed, uturn, uspeed, tcoord ) -- Current coordinate of the carrier. - local pos0=self:GetCoordinate() + local pos0 = self:GetCoordinate() -- Current speed in knots. - local vel0=self.carrier:GetVelocityKNOTS() + local vel0 = self.carrier:GetVelocityKNOTS() -- Default. If speed is not given we take the current speed but at least 5 knots. - speed=speed or math.max(vel0, 5) + speed = speed or math.max( vel0, 5 ) -- Speed in km/h. At least 2 knots. - local speedkmh=math.max(UTILS.KnotsToKmph(speed), UTILS.KnotsToKmph(2)) + local speedkmh = math.max( UTILS.KnotsToKmph( speed ), UTILS.KnotsToKmph( 2 ) ) -- Turn speed in km/h. At least 10 knots. - local cspeedkmh=math.max(self.carrier:GetVelocityKMH(), UTILS.KnotsToKmph(10)) + local cspeedkmh = math.max( self.carrier:GetVelocityKMH(), UTILS.KnotsToKmph( 10 ) ) -- U-turn speed in km/h. - local uspeedkmh=UTILS.KnotsToKmph(uspeed or speed) + local uspeedkmh = UTILS.KnotsToKmph( uspeed or speed ) -- Waypoint table. - local wp={} + local wp = {} -- Waypoint at current position. - table.insert(wp, pos0:WaypointGround(cspeedkmh)) + table.insert( wp, pos0:WaypointGround( cspeedkmh ) ) -- Waypooint to help the turn. if tcoord then - table.insert(wp, tcoord:WaypointGround(cspeedkmh)) + table.insert( wp, tcoord:WaypointGround( cspeedkmh ) ) end -- Detour waypoint. - table.insert(wp, coord:WaypointGround(speedkmh)) + table.insert( wp, coord:WaypointGround( speedkmh ) ) -- U-turn waypoint. If enabled, go back to where you came from. if uturn then - table.insert(wp, pos0:WaypointGround(uspeedkmh)) + table.insert( wp, pos0:WaypointGround( uspeedkmh ) ) end -- Get carrier group. - local group=self.carrier:GetGroup() + local group = self.carrier:GetGroup() -- Passing waypoint taskfunction - local TaskResumeRoute=group:TaskFunction("AIRBOSS._ResumeRoute", self) + local TaskResumeRoute = group:TaskFunction( "AIRBOSS._ResumeRoute", self ) -- Set task to restart route at the last point. - group:SetTaskWaypoint(wp[#wp], TaskResumeRoute) + group:SetTaskWaypoint( wp[#wp], TaskResumeRoute ) -- Debug mark. if self.Debug then if tcoord then - tcoord:MarkToAll(string.format("Detour Turn Help WP. Speed %.1f knots", UTILS.KmphToKnots(cspeedkmh))) + tcoord:MarkToAll( string.format( "Detour Turn Help WP. Speed %.1f knots", UTILS.KmphToKnots( cspeedkmh ) ) ) end - coord:MarkToAll(string.format("Detour Waypoint. Speed %.1f knots", UTILS.KmphToKnots(speedkmh))) + coord:MarkToAll( string.format( "Detour Waypoint. Speed %.1f knots", UTILS.KmphToKnots( speedkmh ) ) ) if uturn then - pos0:MarkToAll(string.format("Detour U-turn WP. Speed %.1f knots", UTILS.KmphToKnots(uspeedkmh))) + pos0:MarkToAll( string.format( "Detour U-turn WP. Speed %.1f knots", UTILS.KmphToKnots( uspeedkmh ) ) ) end end -- Detour switch true. - self.detour=true + self.detour = true -- Route carrier into the wind. - self.carrier:Route(wp) + self.carrier:Route( wp ) end --- Let the carrier turn into the wind. @@ -111036,109 +117546,108 @@ end -- @param #number vdeck Speed on deck m/s. Carrier will -- @param #boolean uturn Make U-turn and go back to initial after downwind leg. -- @return #AIRBOSS self -function AIRBOSS:CarrierTurnIntoWind(time, vdeck, uturn) +function AIRBOSS:CarrierTurnIntoWind( time, vdeck, uturn ) -- Wind speed. - local _,vwind=self:GetWind() + local _, vwind = self:GetWind() -- Speed of carrier in m/s but at least 2 knots. - local vtot=math.max(vdeck-vwind, UTILS.KnotsToMps(2)) + local vtot = math.max( vdeck - vwind, UTILS.KnotsToMps( 2 ) ) -- Distance to travel - local dist=vtot*time + local dist = vtot * time -- Speed in knots - local speedknots=UTILS.MpsToKnots(vtot) - local distNM=UTILS.MetersToNM(dist) + local speedknots = UTILS.MpsToKnots( vtot ) + local distNM = UTILS.MetersToNM( dist ) -- Debug output - self:I(self.lid..string.format("Carrier steaming into the wind (%.1f kts). Distance=%.1f NM, Speed=%.1f knots, Time=%d sec.", UTILS.MpsToKnots(vwind), distNM, speedknots, time)) + self:I( self.lid .. string.format( "Carrier steaming into the wind (%.1f kts). Distance=%.1f NM, Speed=%.1f knots, Time=%d sec.", UTILS.MpsToKnots( vwind ), distNM, speedknots, time ) ) -- Get heading into the wind accounting for angled runway. - local hiw=self:GetHeadingIntoWind() + local hiw = self:GetHeadingIntoWind() -- Current heading. - local hdg=self:GetHeading() + local hdg = self:GetHeading() -- Heading difference. - local deltaH=self:_GetDeltaHeading(hdg, hiw) + local deltaH = self:_GetDeltaHeading( hdg, hiw ) - local Cv=self:GetCoordinate() + local Cv = self:GetCoordinate() - local Ctiw=nil --Core.Point#COORDINATE - local Csoo=nil --Core.Point#COORDINATE + local Ctiw = nil -- Core.Point#COORDINATE + local Csoo = nil -- Core.Point#COORDINATE -- Define path depending on turn angle. - if deltaH<45 then + if deltaH < 45 then -- Small turn. -- Point in the right direction to help turning. - Csoo=Cv:Translate(750, hdg):Translate(750, hiw) + Csoo = Cv:Translate( 750, hdg ):Translate( 750, hiw ) -- Heading into wind from Csoo. - local hsw=self:GetHeadingIntoWind(false, Csoo) + local hsw = self:GetHeadingIntoWind( false, Csoo ) -- Into the wind coord. - Ctiw=Csoo:Translate(dist, hsw) + Ctiw = Csoo:Translate( dist, hsw ) - elseif deltaH<90 then + elseif deltaH < 90 then -- Medium turn. - -- Point in the right direction to help turning. - Csoo=Cv:Translate(900, hdg):Translate(900, hiw) + -- Point in the right direction to help turning. + Csoo = Cv:Translate( 900, hdg ):Translate( 900, hiw ) -- Heading into wind from Csoo. - local hsw=self:GetHeadingIntoWind(false, Csoo) + local hsw = self:GetHeadingIntoWind( false, Csoo ) -- Into the wind coord. - Ctiw=Csoo:Translate(dist, hsw) + Ctiw = Csoo:Translate( dist, hsw ) - elseif deltaH<135 then + elseif deltaH < 135 then -- Large turn backwards. -- Point in the right direction to help turning. - Csoo=Cv:Translate(1100, hdg-90):Translate(1000, hiw) + Csoo = Cv:Translate( 1100, hdg - 90 ):Translate( 1000, hiw ) -- Heading into wind from Csoo. - local hsw=self:GetHeadingIntoWind(false, Csoo) + local hsw = self:GetHeadingIntoWind( false, Csoo ) -- Into the wind coord. - Ctiw=Csoo:Translate(dist, hsw) + Ctiw = Csoo:Translate( dist, hsw ) else -- Huge turn backwards. -- Point in the right direction to help turning. - Csoo=Cv:Translate(1200, hdg-90):Translate(1000, hiw) + Csoo = Cv:Translate( 1200, hdg - 90 ):Translate( 1000, hiw ) -- Heading into wind from Csoo. - local hsw=self:GetHeadingIntoWind(false, Csoo) + local hsw = self:GetHeadingIntoWind( false, Csoo ) -- Into the wind coord. - Ctiw=Csoo:Translate(dist, hsw) + Ctiw = Csoo:Translate( dist, hsw ) end - -- Return to coordinate if collision is detected. - self.Creturnto=self:GetCoordinate() + self.Creturnto = self:GetCoordinate() -- Next waypoint. - local nextwp=self:_GetNextWaypoint() + local nextwp = self:_GetNextWaypoint() -- For downwind, we take the velocity at the next WP. - local vdownwind=UTILS.MpsToKnots(nextwp:GetVelocity()) + local vdownwind = UTILS.MpsToKnots( nextwp:GetVelocity() ) -- Make sure we move at all in case the speed at the waypoint is zero. - if vdownwind<1 then - vdownwind=10 + if vdownwind < 1 then + vdownwind = 10 end -- Let the carrier make a detour from its route but return to its current position. - self:CarrierDetour(Ctiw, speedknots, uturn, vdownwind, Csoo) + self:CarrierDetour( Ctiw, speedknots, uturn, vdownwind, Csoo ) -- Set switch that we are currently turning into the wind. - self.turnintowind=true + self.turnintowind = true return self end @@ -111150,50 +117659,49 @@ end function AIRBOSS:_GetNextWaypoint() -- Next waypoint. - local Nextwp=nil - if self.currentwp==#self.waypoints then - Nextwp=1 + local Nextwp = nil + if self.currentwp == #self.waypoints then + Nextwp = 1 else - Nextwp=self.currentwp+1 + Nextwp = self.currentwp + 1 end -- Debug output - local text=string.format("Current WP=%d/%d, next WP=%d", self.currentwp, #self.waypoints, Nextwp) - self:T2(self.lid..text) + local text = string.format( "Current WP=%d/%d, next WP=%d", self.currentwp, #self.waypoints, Nextwp ) + self:T2( self.lid .. text ) -- Next waypoint. - local nextwp=self.waypoints[Nextwp] --Core.Point#COORDINATE + local nextwp = self.waypoints[Nextwp] -- Core.Point#COORDINATE - return nextwp,Nextwp + return nextwp, Nextwp end - --- Initialize Mission Editor waypoints. -- @param #AIRBOSS self -- @return #AIRBOSS self function AIRBOSS:_InitWaypoints() -- Waypoints of group as defined in the ME. - local Waypoints=self.carrier:GetGroup():GetTemplateRoutePoints() + local Waypoints = self.carrier:GetGroup():GetTemplateRoutePoints() -- Init array. - self.waypoints={} + self.waypoints = {} -- Set waypoint table. - for i,point in ipairs(Waypoints) do + for i, point in ipairs( Waypoints ) do -- Coordinate of the waypoint - local coord=COORDINATE:New(point.x, point.alt, point.y) + local coord = COORDINATE:New( point.x, point.alt, point.y ) -- Set velocity of the coordinate. - coord:SetVelocity(point.speed) + coord:SetVelocity( point.speed ) -- Add to table. - table.insert(self.waypoints, coord) + table.insert( self.waypoints, coord ) -- Debug info. if self.Debug then - coord:MarkToAll(string.format("Carrier Waypoint %d, Speed=%.1f knots", i, UTILS.MpsToKnots(point.speed))) + coord:MarkToAll( string.format( "Carrier Waypoint %d, Speed=%.1f knots", i, UTILS.MpsToKnots( point.speed ) ) ) end end @@ -111205,86 +117713,83 @@ end -- @param #AIRBOSS self -- @param #number n Next waypoint number. -- @return #AIRBOSS self -function AIRBOSS:_PatrolRoute(n) +function AIRBOSS:_PatrolRoute( n ) -- Get next waypoint coordinate and number. - local nextWP, N=self:_GetNextWaypoint() + local nextWP, N = self:_GetNextWaypoint() -- Default resume is to next waypoint. - n=n or N + n = n or N -- Get carrier group. - local CarrierGroup=self.carrier:GetGroup() + local CarrierGroup = self.carrier:GetGroup() -- Waypoints table. - local Waypoints={} + local Waypoints = {} -- Create a waypoint from the current coordinate. - local wp=self:GetCoordinate():WaypointGround(CarrierGroup:GetVelocityKMH()) + local wp = self:GetCoordinate():WaypointGround( CarrierGroup:GetVelocityKMH() ) -- Add current position as first waypoint. - table.insert(Waypoints, wp) + table.insert( Waypoints, wp ) -- Loop over waypoints. - for i=n,#self.waypoints do - local coord=self.waypoints[i] --Core.Point#COORDINATE + for i = n, #self.waypoints do + local coord = self.waypoints[i] -- Core.Point#COORDINATE -- Create a waypoint from the coordinate. - local wp=coord:WaypointGround(UTILS.MpsToKmph(coord.Velocity)) + local wp = coord:WaypointGround( UTILS.MpsToKmph( coord.Velocity ) ) -- Passing waypoint taskfunction - local TaskPassingWP=CarrierGroup:TaskFunction("AIRBOSS._PassingWaypoint", self, i, #self.waypoints) + local TaskPassingWP = CarrierGroup:TaskFunction( "AIRBOSS._PassingWaypoint", self, i, #self.waypoints ) -- Call task function when carrier arrives at waypoint. - CarrierGroup:SetTaskWaypoint(wp, TaskPassingWP) + CarrierGroup:SetTaskWaypoint( wp, TaskPassingWP ) -- Add waypoint to table. - table.insert(Waypoints, wp) + table.insert( Waypoints, wp ) end -- Route carrier group. - CarrierGroup:Route(Waypoints) + CarrierGroup:Route( Waypoints ) return self end - - - --- Estimated the carrier position at some point in the future given the current waypoints and speeds. -- @param #AIRBOSS self -- @return DCS#time ETA abs. time in seconds. function AIRBOSS:_GetETAatNextWP() -- Current waypoint - local cwp=self.currentwp + local cwp = self.currentwp -- Current abs. time. - local tnow=timer.getAbsTime() + local tnow = timer.getAbsTime() -- Current position. - local p=self:GetCoordinate() + local p = self:GetCoordinate() -- Current velocity [m/s]. - local v=self.carrier:GetVelocityMPS() + local v = self.carrier:GetVelocityMPS() -- Next waypoint. - local nextWP=self:_GetNextWaypoint() + local nextWP = self:_GetNextWaypoint() -- Distance to next waypoint. - local s=p:Get2DDistance(nextWP) + local s = p:Get2DDistance( nextWP ) -- Distance to next waypoint. - --local s=0 - --if #self.waypoints>cwp then + -- local s=0 + -- if #self.waypoints>cwp then -- s=p:Get2DDistance(self.waypoints[cwp+1]) - --end + -- end -- v=s/t <==> t=s/v - local t=s/v + local t = s / v -- ETA - local eta=t+tnow + local eta = t + tnow return eta end @@ -111294,31 +117799,32 @@ end function AIRBOSS:_CheckCarrierTurning() -- Current orientation of carrier. - local vNew=self.carrier:GetOrientationX() + local vNew = self.carrier:GetOrientationX() -- Last orientation from 30 seconds ago. - local vLast=self.Corientlast + local vLast = self.Corientlast -- We only need the X-Z plane. - vNew.y=0 ; vLast.y=0 + vNew.y = 0; + vLast.y = 0 -- Angle between current heading and last time we checked ~30 seconds ago. - local deltaLast=math.deg(math.acos(UTILS.VecDot(vNew,vLast)/UTILS.VecNorm(vNew)/UTILS.VecNorm(vLast))) + local deltaLast = math.deg( math.acos( UTILS.VecDot( vNew, vLast ) / UTILS.VecNorm( vNew ) / UTILS.VecNorm( vLast ) ) ) -- Last orientation becomes new orientation - self.Corientlast=vNew + self.Corientlast = vNew -- Carrier is turning when its heading changed by at least one degree since last check. - local turning=math.abs(deltaLast)>=1 + local turning = math.abs( deltaLast ) >= 1 -- Check if turning stopped. (Carrier was turning but is not any more.) if self.turning and not turning then -- Get final bearing. - local FB=self:GetFinalBearing(true) + local FB = self:GetFinalBearing( true ) -- Marshal radio call: "99, new final bearing XYZ degrees." - self:_MarshalCallNewFinalBearing(FB) + self:_MarshalCallNewFinalBearing( FB ) end @@ -111329,24 +117835,24 @@ function AIRBOSS:_CheckCarrierTurning() local hdg if self.turnintowind then -- We are now steaming into the wind. - hdg=self:GetHeadingIntoWind(false) + hdg = self:GetHeadingIntoWind( false ) else -- We turn towards the next waypoint. - hdg=self:GetCoordinate():HeadingTo(self:_GetNextWaypoint()) + hdg = self:GetCoordinate():HeadingTo( self:_GetNextWaypoint() ) end -- Magnetic! - hdg=hdg-self.magvar - if hdg<0 then - hdg=360+hdg + hdg = hdg - self.magvar + if hdg < 0 then + hdg = 360 + hdg end -- Radio call: "99, Carrier starting turn to heading XYZ degrees". - self:_MarshalCallCarrierTurnTo(hdg) + self:_MarshalCallCarrierTurnTo( hdg ) end -- Update turning. - self.turning=turning + self.turning = turning end --- Check if heading or position of carrier have changed significantly. @@ -111358,23 +117864,23 @@ function AIRBOSS:_CheckPatternUpdate() ---------------------------------------- -- Min 10 min between pattern updates. - local dTPupdate=10*60 + local dTPupdate = 10 * 60 -- Update if carrier moves by more than 2.5 NM. - local Dupdate=UTILS.NMToMeters(2.5) + local Dupdate = UTILS.NMToMeters( 2.5 ) -- Update if carrier turned by more than 5°. - local Hupdate=5 + local Hupdate = 5 ----------------------- -- Time Update Check -- ----------------------- -- Time since last pattern update - local dt=timer.getTime()-self.Tpupdate + local dt = timer.getTime() - self.Tpupdate -- Check whether at least 10 min between updates and not turning currently. - if dt=Hupdate then - self:T(self.lid..string.format("Carrier heading changed by %d°.", deltaHeading)) - Hchange=true + local Hchange = false + if math.abs( deltaHeading ) >= Hupdate then + self:T( self.lid .. string.format( "Carrier heading changed by %d°.", deltaHeading ) ) + Hchange = true end --------------------------- @@ -111406,16 +117913,16 @@ function AIRBOSS:_CheckPatternUpdate() --------------------------- -- Get current position and orientation of carrier. - local pos=self:GetCoordinate() + local pos = self:GetCoordinate() -- Get distance to saved position. - local dist=pos:Get2DDistance(self.Cposition) + local dist = pos:Get2DDistance( self.Cposition ) -- Check if carrier moved more than ~10 km. - local Dchange=false - if dist>=Dupdate then - self:T(self.lid..string.format("Carrier position changed by %.1f NM.", UTILS.MetersToNM(dist))) - Dchange=true + local Dchange = false + if dist >= Dupdate then + self:T( self.lid .. string.format( "Carrier position changed by %.1f NM.", UTILS.MetersToNM( dist ) ) ) + Dchange = true end ---------------------------- @@ -111426,228 +117933,227 @@ function AIRBOSS:_CheckPatternUpdate() if Hchange or Dchange then -- Loop over all marshal flights - for _,_flight in pairs(self.Qmarshal) do - local flight=_flight --#AIRBOSS.FlightGroup + for _, _flight in pairs( self.Qmarshal ) do + local flight = _flight -- #AIRBOSS.FlightGroup -- Update marshal pattern of AI keeping the same stack. if flight.ai then - self:_MarshalAI(flight, flight.flag) + self:_MarshalAI( flight, flight.flag ) end end -- Reset parameters for next update check. - self.Corientation=vNew - self.Cposition=pos - self.Tpupdate=timer.getTime() + self.Corientation = vNew + self.Cposition = pos + self.Tpupdate = timer.getTime() end end --- Function called when a group is passing a waypoint. ---@param Wrapper.Group#GROUP group Group that passed the waypoint ---@param #AIRBOSS airboss Airboss object. ---@param #number i Waypoint number that has been reached. ---@param #number final Final waypoint number. -function AIRBOSS._PassingWaypoint(group, airboss, i, final) +-- @param Wrapper.Group#GROUP group Group that passed the waypoint +-- @param #AIRBOSS airboss Airboss object. +-- @param #number i Waypoint number that has been reached. +-- @param #number final Final waypoint number. +function AIRBOSS._PassingWaypoint( group, airboss, i, final ) -- Debug message. - local text=string.format("Group %s passing waypoint %d of %d.", group:GetName(), i, final) + local text = string.format( "Group %s passing waypoint %d of %d.", group:GetName(), i, final ) -- Debug smoke and marker. if airboss.Debug and false then - local pos=group:GetCoordinate() + local pos = group:GetCoordinate() pos:SmokeRed() - local MarkerID=pos:MarkToAll(string.format("Group %s reached waypoint %d", group:GetName(), i)) + local MarkerID = pos:MarkToAll( string.format( "Group %s reached waypoint %d", group:GetName(), i ) ) end -- Debug message. - MESSAGE:New(text,10):ToAllIf(airboss.Debug) - airboss:T(airboss.lid..text) + MESSAGE:New( text, 10 ):ToAllIf( airboss.Debug ) + airboss:T( airboss.lid .. text ) -- Set current waypoint. - airboss.currentwp=i + airboss.currentwp = i -- Passing Waypoint event. - airboss:PassingWaypoint(i) + airboss:PassingWaypoint( i ) -- Reactivate beacons. - --airboss:_ActivateBeacons() + -- airboss:_ActivateBeacons() -- If final waypoint reached, do route all over again. - if i==final and final>1 and airboss.adinfinitum then + if i == final and final > 1 and airboss.adinfinitum then airboss:_PatrolRoute() end end --- Carrier Strike Group resumes the route of the waypoints defined in the mission editor. ---@param Wrapper.Group#GROUP group Carrier Strike Group that passed the waypoint. ---@param #AIRBOSS airboss Airboss object. ---@param Core.Point#COORDINATE gotocoord Go to coordinate before route is resumed. -function AIRBOSS._ResumeRoute(group, airboss, gotocoord) +-- @param Wrapper.Group#GROUP group Carrier Strike Group that passed the waypoint. +-- @param #AIRBOSS airboss Airboss object. +-- @param Core.Point#COORDINATE gotocoord Go to coordinate before route is resumed. +function AIRBOSS._ResumeRoute( group, airboss, gotocoord ) -- Get next waypoint - local nextwp,Nextwp=airboss:_GetNextWaypoint() + local nextwp, Nextwp = airboss:_GetNextWaypoint() -- Speed set at waypoint. - local speedkmh=nextwp.Velocity*3.6 + local speedkmh = nextwp.Velocity * 3.6 -- If speed at waypoint is zero, we set it to 10 knots. - if speedkmh<1 then - speedkmh=UTILS.KnotsToKmph(10) + if speedkmh < 1 then + speedkmh = UTILS.KnotsToKmph( 10 ) end -- Waypoints array. - local waypoints={} + local waypoints = {} -- Current position. - local c0=group:GetCoordinate() + local c0 = group:GetCoordinate() -- Current positon as first waypoint. - local wp0=c0:WaypointGround(speedkmh) - table.insert(waypoints, wp0) + local wp0 = c0:WaypointGround( speedkmh ) + table.insert( waypoints, wp0 ) -- First goto this coordinate. if gotocoord then - --gotocoord:MarkToAll(string.format("Goto waypoint speed=%.1f km/h", speedkmh)) + -- gotocoord:MarkToAll(string.format("Goto waypoint speed=%.1f km/h", speedkmh)) - local headingto=c0:HeadingTo(gotocoord) + local headingto = c0:HeadingTo( gotocoord ) - local hdg1=airboss:GetHeading() - local hdg2=c0:HeadingTo(gotocoord) - local delta=airboss:_GetDeltaHeading(hdg1, hdg2) - - --env.info(string.format("FF hdg1=%d, hdg2=%d, delta=%d", hdg1, hdg2, delta)) + local hdg1 = airboss:GetHeading() + local hdg2 = c0:HeadingTo( gotocoord ) + local delta = airboss:_GetDeltaHeading( hdg1, hdg2 ) + -- env.info(string.format("FF hdg1=%d, hdg2=%d, delta=%d", hdg1, hdg2, delta)) -- Add additional turn points - if delta>90 then + if delta > 90 then -- Turn radius 3 NM. - local turnradius=UTILS.NMToMeters(3) + local turnradius = UTILS.NMToMeters( 3 ) - local gotocoordh=c0:Translate(turnradius, hdg1+45) - --gotocoordh:MarkToAll(string.format("Goto help waypoint 1 speed=%.1f km/h", speedkmh)) + local gotocoordh = c0:Translate( turnradius, hdg1 + 45 ) + -- gotocoordh:MarkToAll(string.format("Goto help waypoint 1 speed=%.1f km/h", speedkmh)) - local wp=gotocoordh:WaypointGround(speedkmh) - table.insert(waypoints, wp) + local wp = gotocoordh:WaypointGround( speedkmh ) + table.insert( waypoints, wp ) - gotocoordh=c0:Translate(turnradius, hdg1+90) - --gotocoordh:MarkToAll(string.format("Goto help waypoint 2 speed=%.1f km/h", speedkmh)) + gotocoordh = c0:Translate( turnradius, hdg1 + 90 ) + -- gotocoordh:MarkToAll(string.format("Goto help waypoint 2 speed=%.1f km/h", speedkmh)) - wp=gotocoordh:WaypointGround(speedkmh) - table.insert(waypoints, wp) + wp = gotocoordh:WaypointGround( speedkmh ) + table.insert( waypoints, wp ) end - local wp1=gotocoord:WaypointGround(speedkmh) - table.insert(waypoints, wp1) + local wp1 = gotocoord:WaypointGround( speedkmh ) + table.insert( waypoints, wp1 ) end -- Debug message. - local text=string.format("Carrier is resuming route. Next waypoint %d, Speed=%.1f knots.", Nextwp, UTILS.KmphToKnots(speedkmh)) + local text = string.format( "Carrier is resuming route. Next waypoint %d, Speed=%.1f knots.", Nextwp, UTILS.KmphToKnots( speedkmh ) ) -- Debug message. - MESSAGE:New(text,10):ToAllIf(airboss.Debug) - airboss:I(airboss.lid..text) + MESSAGE:New( text, 10 ):ToAllIf( airboss.Debug ) + airboss:I( airboss.lid .. text ) -- Loop over all remaining waypoints. - for i=Nextwp, #airboss.waypoints do + for i = Nextwp, #airboss.waypoints do -- Coordinate of the next WP. - local coord=airboss.waypoints[i] --Core.Point#COORDINATE + local coord = airboss.waypoints[i] -- Core.Point#COORDINATE -- Speed in km/h of that WP. Velocity is in m/s. - local speed=coord.Velocity*3.6 + local speed = coord.Velocity * 3.6 -- If speed is zero we set it to 10 knots. - if speed<1 then - speed=UTILS.KnotsToKmph(10) + if speed < 1 then + speed = UTILS.KnotsToKmph( 10 ) end - --coord:MarkToAll(string.format("Resume route WP %d, speed=%.1f km/h", i, speed)) + -- coord:MarkToAll(string.format("Resume route WP %d, speed=%.1f km/h", i, speed)) -- Create waypoint. - local wp=coord:WaypointGround(speed) + local wp = coord:WaypointGround( speed ) -- Passing waypoint task function. - local TaskPassingWP=group:TaskFunction("AIRBOSS._PassingWaypoint", airboss, i, #airboss.waypoints) + local TaskPassingWP = group:TaskFunction( "AIRBOSS._PassingWaypoint", airboss, i, #airboss.waypoints ) -- Call task function when carrier arrives at waypoint. - group:SetTaskWaypoint(wp, TaskPassingWP) + group:SetTaskWaypoint( wp, TaskPassingWP ) -- Add waypoints to table. - table.insert(waypoints, wp) + table.insert( waypoints, wp ) end -- Set turn into wind switch false. - airboss.turnintowind=false - airboss.detour=false + airboss.turnintowind = false + airboss.detour = false -- Route group. - group:Route(waypoints) + group:Route( waypoints ) end --- Function called when a group has reached the holding zone. ---@param Wrapper.Group#GROUP group Group that reached the holding zone. ---@param #AIRBOSS airboss Airboss object. ---@param #AIRBOSS.FlightGroup flight Flight group that has reached the holding zone. -function AIRBOSS._ReachedHoldingZone(group, airboss, flight) +-- @param Wrapper.Group#GROUP group Group that reached the holding zone. +-- @param #AIRBOSS airboss Airboss object. +-- @param #AIRBOSS.FlightGroup flight Flight group that has reached the holding zone. +function AIRBOSS._ReachedHoldingZone( group, airboss, flight ) -- Debug message. - local text=string.format("Flight %s reached holding zone.", group:GetName()) - MESSAGE:New(text,10):ToAllIf(airboss.Debug) - airboss:T(airboss.lid..text) + local text = string.format( "Flight %s reached holding zone.", group:GetName() ) + MESSAGE:New( text, 10 ):ToAllIf( airboss.Debug ) + airboss:T( airboss.lid .. text ) -- Debug mark. if airboss.Debug then - group:GetCoordinate():MarkToAll(text) + group:GetCoordinate():MarkToAll( text ) end -- Set holding flag true and set timestamp for marshal time check. if flight then - flight.holding=true - flight.time=timer.getAbsTime() + flight.holding = true + flight.time = timer.getAbsTime() end end --- Function called when a group should be send to the Marshal stack. If stack is full, it is send to wait. ---@param Wrapper.Group#GROUP group Group that reached the holding zone. ---@param #AIRBOSS airboss Airboss object. ---@param #AIRBOSS.FlightGroup flight Flight group that has reached the holding zone. -function AIRBOSS._TaskFunctionMarshalAI(group, airboss, flight) +-- @param Wrapper.Group#GROUP group Group that reached the holding zone. +-- @param #AIRBOSS airboss Airboss object. +-- @param #AIRBOSS.FlightGroup flight Flight group that has reached the holding zone. +function AIRBOSS._TaskFunctionMarshalAI( group, airboss, flight ) -- Debug message. - local text=string.format("Flight %s is send to marshal.", group:GetName()) - MESSAGE:New(text,10):ToAllIf(airboss.Debug) - airboss:T(airboss.lid..text) + local text = string.format( "Flight %s is send to marshal.", group:GetName() ) + MESSAGE:New( text, 10 ):ToAllIf( airboss.Debug ) + airboss:T( airboss.lid .. text ) -- Get the next free stack for current recovery case. - local stack=airboss:_GetFreeStack(flight.ai) + local stack = airboss:_GetFreeStack( flight.ai ) if stack then -- Send AI to marshal stack. - airboss:_MarshalAI(flight, stack) + airboss:_MarshalAI( flight, stack ) else -- Send AI to orbit outside 10 NM zone and wait until the next Marshal stack is available. - if not airboss:_InQueue(airboss.Qwaiting, flight.group) then - airboss:_WaitAI(flight) + if not airboss:_InQueue( airboss.Qwaiting, flight.group ) then + airboss:_WaitAI( flight ) end end -- If it came from refueling. - if flight.refueling==true then - airboss:I(airboss.lid..string.format("Flight group %s finished refueling task.", flight.groupname)) + if flight.refueling == true then + airboss:I( airboss.lid .. string.format( "Flight group %s finished refueling task.", flight.groupname ) ) end -- Not refueling any more in case it was. - flight.refueling=false + flight.refueling = false end @@ -111659,23 +118165,23 @@ end -- @param #AIRBOSS self -- @param #string actype Aircraft type name. -- @return #string Aircraft nickname. E.g. "Hornet" for the F/A-18C or "Tomcat" For the F-14A. -function AIRBOSS:_GetACNickname(actype) +function AIRBOSS:_GetACNickname( actype ) - local nickname="unknown" - if actype==AIRBOSS.AircraftCarrier.A4EC then - nickname="Skyhawk" - elseif actype==AIRBOSS.AircraftCarrier.T45C then - nickname="Goshawk" - elseif actype==AIRBOSS.AircraftCarrier.AV8B then - nickname="Harrier" - elseif actype==AIRBOSS.AircraftCarrier.E2D then - nickname="Hawkeye" - elseif actype==AIRBOSS.AircraftCarrier.F14A_AI or actype==AIRBOSS.AircraftCarrier.F14A or actype==AIRBOSS.AircraftCarrier.F14B then - nickname="Tomcat" - elseif actype==AIRBOSS.AircraftCarrier.FA18C or actype==AIRBOSS.AircraftCarrier.HORNET then - nickname="Hornet" - elseif actype==AIRBOSS.AircraftCarrier.S3B or actype==AIRBOSS.AircraftCarrier.S3BTANKER then - nickname="Viking" + local nickname = "unknown" + if actype == AIRBOSS.AircraftCarrier.A4EC then + nickname = "Skyhawk" + elseif actype == AIRBOSS.AircraftCarrier.T45C then + nickname = "Goshawk" + elseif actype == AIRBOSS.AircraftCarrier.AV8B then + nickname = "Harrier" + elseif actype == AIRBOSS.AircraftCarrier.E2D then + nickname = "Hawkeye" + elseif actype == AIRBOSS.AircraftCarrier.F14A_AI or actype == AIRBOSS.AircraftCarrier.F14A or actype == AIRBOSS.AircraftCarrier.F14B then + nickname = "Tomcat" + elseif actype == AIRBOSS.AircraftCarrier.FA18C or actype == AIRBOSS.AircraftCarrier.HORNET then + nickname = "Hornet" + elseif actype == AIRBOSS.AircraftCarrier.S3B or actype == AIRBOSS.AircraftCarrier.S3BTANKER then + nickname = "Viking" end return nickname @@ -111685,8 +118191,8 @@ end -- @param #AIRBOSS self -- @param Wrapper.Group#GROUP group Aircraft group. -- @return #string Onboard number as string. -function AIRBOSS:_GetOnboardNumberPlayer(group) - return self:_GetOnboardNumbers(group, true) +function AIRBOSS:_GetOnboardNumberPlayer( group ) + return self:_GetOnboardNumbers( group, true ) end --- Get onboard numbers of all units in a group. @@ -111694,60 +118200,59 @@ end -- @param Wrapper.Group#GROUP group Aircraft group. -- @param #boolean playeronly If true, return the onboard number for player or client skill units. -- @return #table Table of onboard numbers. -function AIRBOSS:_GetOnboardNumbers(group, playeronly) - --self:F({groupname=group:GetName}) +function AIRBOSS:_GetOnboardNumbers( group, playeronly ) + -- self:F({groupname=group:GetName}) -- Get group name. - local groupname=group:GetName() + local groupname = group:GetName() -- Debug text. - local text=string.format("Onboard numbers of group %s:", groupname) + local text = string.format( "Onboard numbers of group %s:", groupname ) -- Units of template group. - local units=group:GetTemplate().units + local units = group:GetTemplate().units -- Get numbers. - local numbers={} - for _,unit in pairs(units) do + local numbers = {} + for _, unit in pairs( units ) do -- Onboard number and unit name. - local n=tostring(unit.onboard_num) - local name=unit.name - local skill=unit.skill or "Unknown" + local n = tostring( unit.onboard_num ) + local name = unit.name + local skill = unit.skill or "Unknown" -- Debug text. - text=text..string.format("\n- unit %s: onboard #=%s skill=%s", name, n, tostring(skill)) + text = text .. string.format( "\n- unit %s: onboard #=%s skill=%s", name, n, tostring( skill ) ) - if playeronly and skill=="Client" or skill=="Player" then + if playeronly and skill == "Client" or skill == "Player" then -- There can be only one player in the group, so we skip everything else. return n end -- Table entry. - numbers[name]=n + numbers[name] = n end -- Debug info. - self:T2(self.lid..text) + self:T2( self.lid .. text ) return numbers end - --- Get Tower frequency of carrier. -- @param #AIRBOSS self function AIRBOSS:_GetTowerFrequency() -- Tower frequency in MHz - self.TowerFreq=0 + self.TowerFreq = 0 -- Get Template of Strike Group - local striketemplate=self.carrier:GetGroup():GetTemplate() + local striketemplate = self.carrier:GetGroup():GetTemplate() -- Find the carrier unit. - for _,unit in pairs(striketemplate.units) do - if self.carrier:GetName()==unit.name then - self.TowerFreq=unit.frequency/1000000 + for _, unit in pairs( striketemplate.units ) do + if self.carrier:GetName() == unit.name then + self.TowerFreq = unit.frequency / 1000000 return end end @@ -111763,19 +118268,19 @@ end -- @param #AIRBOSS.PlayerData playerData Player data table. -- @return #number Error margin for still being okay. -- @return #number Error margin for really sucking. -function AIRBOSS:_GetGoodBadScore(playerData) +function AIRBOSS:_GetGoodBadScore( playerData ) local lowscore local badscore - if playerData.difficulty==AIRBOSS.Difficulty.EASY then - lowscore=10 - badscore=20 - elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then - lowscore=5 - badscore=10 - elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then - lowscore=2.5 - badscore=5 + if playerData.difficulty == AIRBOSS.Difficulty.EASY then + lowscore = 10 + badscore = 20 + elseif playerData.difficulty == AIRBOSS.Difficulty.NORMAL then + lowscore = 5 + badscore = 10 + elseif playerData.difficulty == AIRBOSS.Difficulty.HARD then + lowscore = 2.5 + badscore = 5 end return lowscore, badscore @@ -111785,14 +118290,14 @@ end -- @param #AIRBOSS self -- @param Wrapper.Unit#UNIT unit Aircraft unit. (Will also work with groups as given parameter.) -- @return #boolean If true, aircraft can land on a carrier. -function AIRBOSS:_IsCarrierAircraft(unit) +function AIRBOSS:_IsCarrierAircraft( unit ) -- Get aircraft type name - local aircrafttype=unit:GetTypeName() + local aircrafttype = unit:GetTypeName() -- Special case for Harrier which can only land on Tarawa, LHA and LHD. - if aircrafttype==AIRBOSS.AircraftCarrier.AV8B then - if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then + if aircrafttype == AIRBOSS.AircraftCarrier.AV8B then + if self.carriertype == AIRBOSS.CarrierType.HERMES or self.carriertype == AIRBOSS.CarrierType.TARAWA or self.carriertype == AIRBOSS.CarrierType.AMERICA or self.carriertype == AIRBOSS.CarrierType.JCARLOS or self.carriertype == AIRBOSS.CarrierType.CANBERRA then return true else return false @@ -111800,17 +118305,17 @@ function AIRBOSS:_IsCarrierAircraft(unit) end -- Also only Harriers can land on the Tarawa, LHA and LHD. - if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then - if aircrafttype~=AIRBOSS.AircraftCarrier.AV8B then + if self.carriertype == AIRBOSS.CarrierType.HERMES or self.carriertype == AIRBOSS.CarrierType.TARAWA or self.carriertype == AIRBOSS.CarrierType.AMERICA or self.carriertype == AIRBOSS.CarrierType.JCARLOS or self.carriertype == AIRBOSS.CarrierType.CANBERRA then + if aircrafttype ~= AIRBOSS.AircraftCarrier.AV8B then return false end end -- Loop over all other known carrier capable aircraft. - for _,actype in pairs(AIRBOSS.AircraftCarrier) do + for _, actype in pairs( AIRBOSS.AircraftCarrier ) do -- Check if this is a carrier capable aircraft type. - if actype==aircrafttype then + if actype == aircrafttype then return true end end @@ -111823,10 +118328,10 @@ end -- @param #AIRBOSS self -- @param Wrapper.Unit#UNIT unit Aircraft unit. -- @return #boolean If true, human player inside the unit. -function AIRBOSS:_IsHumanUnit(unit) +function AIRBOSS:_IsHumanUnit( unit ) -- Get player unit or nil if no player unit. - local playerunit=self:_GetPlayerUnitAndName(unit:GetName()) + local playerunit = self:_GetPlayerUnitAndName( unit:GetName() ) if playerunit then return true @@ -111839,15 +118344,15 @@ end -- @param #AIRBOSS self -- @param Wrapper.Group#GROUP group Aircraft group. -- @return #boolean If true, human player inside group. -function AIRBOSS:_IsHuman(group) +function AIRBOSS:_IsHuman( group ) -- Get all units of the group. - local units=group:GetUnits() + local units = group:GetUnits() -- Loop over all units. - for _,_unit in pairs(units) do + for _, _unit in pairs( units ) do -- Check if unit is human. - local human=self:_IsHumanUnit(_unit) + local human = self:_IsHumanUnit( _unit ) if human then return true end @@ -111860,31 +118365,31 @@ end -- @param #AIRBOSS self -- @param Wrapper.Unit#UNIT unit The unit for which the mass is determined. -- @return #number Fuel state in pounds. -function AIRBOSS:_GetFuelState(unit) +function AIRBOSS:_GetFuelState( unit ) -- Get relative fuel [0,1]. - local fuel=unit:GetFuel() + local fuel = unit:GetFuel() -- Get max weight of fuel in kg. - local maxfuel=self:_GetUnitMasses(unit) + local maxfuel = self:_GetUnitMasses( unit ) -- Fuel state, i.e. what let's - local fuelstate=fuel*maxfuel + local fuelstate = fuel * maxfuel -- Debug info. - self:T2(self.lid..string.format("Unit %s fuel state = %.1f kg = %.1f lbs", unit:GetName(), fuelstate, UTILS.kg2lbs(fuelstate))) + self:T2( self.lid .. string.format( "Unit %s fuel state = %.1f kg = %.1f lbs", unit:GetName(), fuelstate, UTILS.kg2lbs( fuelstate ) ) ) - return UTILS.kg2lbs(fuelstate) + return UTILS.kg2lbs( fuelstate ) end --- Convert altitude from meters to angels (thousands of feet). -- @param #AIRBOSS self -- @param alt Alitude in meters. -- @return #number Altitude in Anglels = thousands of feet using math.floor(). -function AIRBOSS:_GetAngels(alt) +function AIRBOSS:_GetAngels( alt ) if alt then - local angels=UTILS.Round(UTILS.MetersToFeet(alt)/1000, 0) + local angels = UTILS.Round( UTILS.MetersToFeet( alt ) / 1000, 0 ) return angels else return 0 @@ -111899,25 +118404,25 @@ end -- @return #number Empty weight of unit in kg. -- @return #number Max weight of unit in kg. -- @return #number Max cargo weight in kg. -function AIRBOSS:_GetUnitMasses(unit) +function AIRBOSS:_GetUnitMasses( unit ) -- Get DCS descriptors table. - local Desc=unit:GetDesc() + local Desc = unit:GetDesc() -- Mass of fuel in kg. - local massfuel=Desc.fuelMassMax or 0 + local massfuel = Desc.fuelMassMax or 0 -- Mass of empty unit in km. - local massempty=Desc.massEmpty or 0 + local massempty = Desc.massEmpty or 0 -- Max weight of unit in kg. - local massmax=Desc.massMax or 0 + local massmax = Desc.massMax or 0 -- Rest is cargo. - local masscargo=massmax-massfuel-massempty + local masscargo = massmax - massfuel - massempty -- Debug info. - self:T2(self.lid..string.format("Unit %s mass fuel=%.1f kg, empty=%.1f kg, max=%.1f kg, cargo=%.1f kg", unit:GetName(), massfuel, massempty, massmax, masscargo)) + self:T2( self.lid .. string.format( "Unit %s mass fuel=%.1f kg, empty=%.1f kg, max=%.1f kg, cargo=%.1f kg", unit:GetName(), massfuel, massempty, massmax, masscargo ) ) return massfuel, massempty, massmax, masscargo end @@ -111926,10 +118431,10 @@ end -- @param #AIRBOSS self -- @param Wrapper.Unit#UNIT unit Unit in question. -- @return #AIRBOSS.PlayerData Player data or nil if not player with this name or unit exists. -function AIRBOSS:_GetPlayerDataUnit(unit) +function AIRBOSS:_GetPlayerDataUnit( unit ) if unit:IsAlive() then - local unitname=unit:GetName() - local playerunit,playername=self:_GetPlayerUnitAndName(unitname) + local unitname = unit:GetName() + local playerunit, playername = self:_GetPlayerUnitAndName( unitname ) if playerunit and playername then return self.players[playername] end @@ -111937,15 +118442,14 @@ function AIRBOSS:_GetPlayerDataUnit(unit) return nil end - --- Get player data from group object. -- @param #AIRBOSS self -- @param Wrapper.Group#GROUP group Group in question. -- @return #AIRBOSS.PlayerData Player data or nil if not player with this name or unit exists. -function AIRBOSS:_GetPlayerDataGroup(group) - local units=group:GetUnits() - for _,unit in pairs(units) do - local playerdata=self:_GetPlayerDataUnit(unit) +function AIRBOSS:_GetPlayerDataGroup( group ) + local units = group:GetUnits() + for _, unit in pairs( units ) do + local playerdata = self:_GetPlayerDataUnit( unit ) if playerdata then return playerdata end @@ -111958,20 +118462,20 @@ end -- @param #string _unitName Name of the player unit. -- @return Wrapper.Unit#UNIT Unit of player or nil. -- @return #string Name of player or nil. -function AIRBOSS:_GetPlayerUnit(_unitName) +function AIRBOSS:_GetPlayerUnit( _unitName ) - for _,_player in pairs(self.players) do + for _, _player in pairs( self.players ) do - local player=_player --#AIRBOSS.PlayerData + local player = _player -- #AIRBOSS.PlayerData - if player.unit and player.unit:GetName()==_unitName then - self:T(self.lid..string.format("Found player=%s unit=%s in players table.", tostring(player.name), tostring(_unitName))) + if player.unit and player.unit:GetName() == _unitName then + self:T( self.lid .. string.format( "Found player=%s unit=%s in players table.", tostring( player.name ), tostring( _unitName ) ) ) return player.unit, player.name end end - return nil,nil + return nil, nil end --- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. @@ -111979,13 +118483,13 @@ end -- @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 AIRBOSS:_GetPlayerUnitAndName(_unitName) - self:F2(_unitName) +function AIRBOSS:_GetPlayerUnitAndName( _unitName ) + self:F2( _unitName ) if _unitName ~= nil then -- First, let's look up all current players. - local u,pn=self:_GetPlayerUnit(_unitName) + local u, pn = self:_GetPlayerUnit( _unitName ) -- Return if u and pn then @@ -111993,22 +118497,22 @@ function AIRBOSS:_GetPlayerUnitAndName(_unitName) end -- Get DCS unit from its name. - local DCSunit=Unit.getByName(_unitName) + local DCSunit = Unit.getByName( _unitName ) if DCSunit then -- Get player name if any. - local playername=DCSunit:getPlayerName() + local playername = DCSunit:getPlayerName() -- Unit object. - local unit=UNIT:Find(DCSunit) + local unit = UNIT:Find( DCSunit ) -- Debug. - self:T2({DCSunit=DCSunit, unit=unit, playername=playername}) + self:T2( { DCSunit = DCSunit, unit = unit, playername = playername } ) -- 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))) + self:T( self.lid .. string.format( "Found DCS unit %s with player %s.", tostring( _unitName ), tostring( playername ) ) ) return unit, playername end @@ -112017,7 +118521,7 @@ function AIRBOSS:_GetPlayerUnitAndName(_unitName) end -- Return nil if we could not find a player. - return nil,nil + return nil, nil end --- Get carrier coalition. @@ -112050,7 +118554,7 @@ end function AIRBOSS:_GetStaticWeather() -- Weather data from mission file. - local weather=env.mission.weather + local weather = env.mission.weather -- Clouds --[[ @@ -112062,19 +118566,19 @@ function AIRBOSS:_GetStaticWeather() ["iprecptns"] = 1, }, -- end of ["clouds"] ]] - local clouds=weather.clouds + local clouds = weather.clouds -- Visibilty distance in meters. - local visibility=weather.visibility.distance + local visibility = weather.visibility.distance -- Dust --[[ ["enable_dust"] = false, ["dust_density"] = 0, ]] - local dust=nil - if weather.enable_dust==true then - dust=weather.dust_density + local dust = nil + if weather.enable_dust == true then + dust = weather.dust_density end -- Fog @@ -112086,12 +118590,11 @@ function AIRBOSS:_GetStaticWeather() ["visibility"] = 25, }, -- end of ["fog"] ]] - local fog=nil - if weather.enable_fog==true then - fog=weather.fog + local fog = nil + if weather.enable_fog == true then + fog = weather.fog end - return clouds, visibility, fog, dust end @@ -112102,9 +118605,9 @@ end --- Function called by DCS timer. Unused. -- @param #table param Parameters. -- @param #number time Time. -function AIRBOSS._CheckRadioQueueT(param, time) - AIRBOSS._CheckRadioQueue(param.airboss, param.radioqueue, param.name) - return time+0.05 +function AIRBOSS._CheckRadioQueueT( param, time ) + AIRBOSS._CheckRadioQueue( param.airboss, param.radioqueue, param.name ) + return time + 0.05 end --- Radio queue item. @@ -112121,83 +118624,83 @@ end -- @param #AIRBOSS self -- @param #table radioqueue The radio queue. -- @param #string name Name of the queue. -function AIRBOSS:_CheckRadioQueue(radioqueue, name) +function AIRBOSS:_CheckRadioQueue( radioqueue, name ) - --env.info(string.format("FF %s #radioqueue %d", name, #radioqueue)) + -- env.info(string.format("FF %s #radioqueue %d", name, #radioqueue)) -- Check if queue is empty. - if #radioqueue==0 then + if #radioqueue == 0 then - if name=="LSO" then - self:T(self.lid..string.format("Stopping LSO radio queue.")) - self.radiotimer:Stop(self.RQLid) - self.RQLid=nil - elseif name=="MARSHAL" then - self:T(self.lid..string.format("Stopping Marshal radio queue.")) - self.radiotimer:Stop(self.RQMid) - self.RQMid=nil - end + if name == "LSO" then + self:T( self.lid .. string.format( "Stopping LSO radio queue." ) ) + self.radiotimer:Stop( self.RQLid ) + self.RQLid = nil + elseif name == "MARSHAL" then + self:T( self.lid .. string.format( "Stopping Marshal radio queue." ) ) + self.radiotimer:Stop( self.RQMid ) + self.RQMid = nil + end return end -- Get current abs time. - local _time=timer.getAbsTime() + local _time = timer.getAbsTime() - local playing=false - local next=nil --#AIRBOSS.Radioitem - local _remove=nil - for i,_transmission in ipairs(radioqueue) do - local transmission=_transmission --#AIRBOSS.Radioitem + local playing = false + local next = nil -- #AIRBOSS.Radioitem + local _remove = nil + for i, _transmission in ipairs( radioqueue ) do + local transmission = _transmission -- #AIRBOSS.Radioitem -- Check if transmission time has passed. - if _time>=transmission.Tplay then + if _time >= transmission.Tplay then -- Check if transmission is currently playing. if transmission.isplaying then -- Check if transmission is finished. - if _time>=transmission.Tstarted+transmission.call.duration then + if _time >= transmission.Tstarted + transmission.call.duration then -- Transmission over. - transmission.isplaying=false - _remove=i + transmission.isplaying = false + _remove = i - if transmission.radio.alias=="LSO" then - self.TQLSO=_time - elseif transmission.radio.alias=="MARSHAL" then - self.TQMarshal=_time + if transmission.radio.alias == "LSO" then + self.TQLSO = _time + elseif transmission.radio.alias == "MARSHAL" then + self.TQMarshal = _time end else -- still playing -- Transmission is still playing. - playing=true + playing = true end else -- not playing yet - local Tlast=nil + local Tlast = nil if transmission.interval then - if transmission.radio.alias=="LSO" then - Tlast=self.TQLSO - elseif transmission.radio.alias=="MARSHAL" then - Tlast=self.TQMarshal + if transmission.radio.alias == "LSO" then + Tlast = self.TQLSO + elseif transmission.radio.alias == "MARSHAL" then + Tlast = self.TQMarshal end end - if transmission.interval==nil then + if transmission.interval == nil then -- Not playing ==> this will be next. - if next==nil then - next=transmission + if next == nil then + next = transmission end else - if _time-Tlast>=transmission.interval then - next=transmission + if _time - Tlast >= transmission.interval then + next = transmission else end @@ -112212,21 +118715,21 @@ function AIRBOSS:_CheckRadioQueue(radioqueue, name) else - -- Transmission not due yet. + -- Transmission not due yet. end end -- Found a new transmission. - if next~=nil and not playing then - self:Broadcast(next.radio, next.call, next.loud) - next.isplaying=true - next.Tstarted=_time + if next ~= nil and not playing then + self:Broadcast( next.radio, next.call, next.loud ) + next.isplaying = true + next.Tstarted = _time end -- Remove completed calls from queue. if _remove then - table.remove(radioqueue, _remove) + table.remove( radioqueue, _remove ) end return @@ -112241,76 +118744,75 @@ end -- @param #number interval Interval in seconds after the last sound has been played. -- @param #boolean click If true, play radio click at the end. -- @param #boolean pilotcall If true, it's a pilot call. -function AIRBOSS:RadioTransmission(radio, call, loud, delay, interval, click, pilotcall) - self:F2({radio=radio, call=call, loud=loud, delay=delay, interval=interval, click=click}) +function AIRBOSS:RadioTransmission( radio, call, loud, delay, interval, click, pilotcall ) + self:F2( { radio = radio, call = call, loud = loud, delay = delay, interval = interval, click = click } ) -- Nil check. - if radio==nil or call==nil then + if radio == nil or call == nil then return end -- Create a new radio transmission item. - local transmission={} --#AIRBOSS.Radioitem + local transmission = {} -- #AIRBOSS.Radioitem - transmission.radio=radio - transmission.call=call - transmission.Tplay=timer.getAbsTime()+(delay or 0) - transmission.interval=interval - transmission.isplaying=false - transmission.Tstarted=nil - transmission.loud=loud and call.loud + transmission.radio = radio + transmission.call = call + transmission.Tplay = timer.getAbsTime() + (delay or 0) + transmission.interval = interval + transmission.isplaying = false + transmission.Tstarted = nil + transmission.loud = loud and call.loud -- Player onboard number if sender has one. - if self:_IsOnboard(call.modexsender) then - self:_Number2Radio(radio, call.modexsender, delay, 0.3, pilotcall) + if self:_IsOnboard( call.modexsender ) then + self:_Number2Radio( radio, call.modexsender, delay, 0.3, pilotcall ) end -- Play onboard number if receiver has one. - if self:_IsOnboard(call.modexreceiver) then - self:_Number2Radio(radio, call.modexreceiver, delay, 0.3, pilotcall) + if self:_IsOnboard( call.modexreceiver ) then + self:_Number2Radio( radio, call.modexreceiver, delay, 0.3, pilotcall ) end -- Add transmission to the right queue. - local caller="" - if radio.alias=="LSO" then + local caller = "" + if radio.alias == "LSO" then - table.insert(self.RQLSO, transmission) + table.insert( self.RQLSO, transmission ) - caller="LSOCall" + caller = "LSOCall" - -- Schedule radio queue checks. - if not self.RQLid then - self:T(self.lid..string.format("Starting LSO radio queue.")) - self.RQLid=self.radiotimer:Schedule(nil, AIRBOSS._CheckRadioQueue, {self, self.RQLSO, "LSO"}, 0.02, 0.05) - end + -- Schedule radio queue checks. + if not self.RQLid then + self:T( self.lid .. string.format( "Starting LSO radio queue." ) ) + self.RQLid = self.radiotimer:Schedule( nil, AIRBOSS._CheckRadioQueue, { self, self.RQLSO, "LSO" }, 0.02, 0.05 ) + end - elseif radio.alias=="MARSHAL" then + elseif radio.alias == "MARSHAL" then - table.insert(self.RQMarshal, transmission) + table.insert( self.RQMarshal, transmission ) - caller="MarshalCall" + caller = "MarshalCall" - if not self.RQMid then - self:T(self.lid..string.format("Starting Marhal radio queue.")) - self.RQMid=self.radiotimer:Schedule(nil, AIRBOSS._CheckRadioQueue, {self, self.RQMarshal, "MARSHAL"}, 0.02, 0.05) - end + if not self.RQMid then + self:T( self.lid .. string.format( "Starting Marhal radio queue." ) ) + self.RQMid = self.radiotimer:Schedule( nil, AIRBOSS._CheckRadioQueue, { self, self.RQMarshal, "MARSHAL" }, 0.02, 0.05 ) + end end -- Append radio click sound at the end of the transmission. if click then - self:RadioTransmission(radio, self[caller].CLICK, false, delay) + self:RadioTransmission( radio, self[caller].CLICK, false, delay ) end end - --- Check if a call needs a subtitle because the complete voice overs are not available. -- @param #AIRBOSS self -- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. -- @return #boolean If true, call needs a subtitle. -function AIRBOSS:_NeedsSubtitle(call) +function AIRBOSS:_NeedsSubtitle( call ) -- Currently we play the noise file. - if call.file==self.MarshalCall.NOISE.file or call.file==self.LSOCall.NOISE.file then + if call.file == self.MarshalCall.NOISE.file or call.file == self.LSOCall.NOISE.file then return true else return false @@ -112322,8 +118824,8 @@ end -- @param #AIRBOSS.Radio radio Radio sending transmission. -- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. -- @param #boolean loud Play loud version of file. -function AIRBOSS:Broadcast(radio, call, loud) - self:F(call) +function AIRBOSS:Broadcast( radio, call, loud ) + self:F( call ) -- Check which sound output method to use. if not self.usersoundradio then @@ -112333,72 +118835,74 @@ function AIRBOSS:Broadcast(radio, call, loud) ---------------------------- -- Get unit sending the transmission. - local sender=self:_GetRadioSender(radio) + local sender = self:_GetRadioSender( radio ) -- Construct file name and subtitle. - local filename=self:_RadioFilename(call, loud, radio.alias) + local filename = self:_RadioFilename( call, loud, radio.alias ) -- Create subtitle for transmission. - local subtitle=self:_RadioSubtitle(radio, call, loud) + local subtitle = self:_RadioSubtitle( radio, call, loud ) -- Debug. - self:T({filename=filename, subtitle=subtitle}) + self:T( { filename = filename, subtitle = subtitle } ) if sender then -- Broadcasting from aircraft. Only players tuned in to the right frequency will see the message. - self:T(self.lid..string.format("Broadcasting from aircraft %s", sender:GetName())) + self:T( self.lid .. string.format( "Broadcasting from aircraft %s", sender:GetName() ) ) -- Command to set the Frequency for the transmission. - local commandFrequency={ - id="SetFrequency", - params={ - frequency=radio.frequency*1000000, -- Frequency in Hz. - modulation=radio.modulation, - }} + local commandFrequency = { + id = "SetFrequency", + params = { + frequency = radio.frequency * 1000000, -- Frequency in Hz. + modulation = radio.modulation, + }, + } -- Command to tranmit the call. - local commandTransmit={ + local commandTransmit = { id = "TransmitMessage", params = { - file=filename, - duration=call.subduration or 5, - subtitle=subtitle, - loop=false, - }} + file = filename, + duration = call.subduration or 5, + subtitle = subtitle, + loop = false, + }, + } -- Set commend for frequency - sender:SetCommand(commandFrequency) + sender:SetCommand( commandFrequency ) -- Set command for radio transmission. - sender:SetCommand(commandTransmit) + sender:SetCommand( commandTransmit ) else -- Broadcasting from carrier. No subtitle possible. Need to send messages to players. - self:T(self.lid..string.format("Broadcasting from carrier via trigger.action.radioTransmission().")) + self:T( self.lid .. string.format( "Broadcasting from carrier via trigger.action.radioTransmission()." ) ) -- Transmit from carrier position. - local vec3=self.carrier:GetPositionVec3() + local vec3 = self.carrier:GetPositionVec3() -- Transmit via trigger. - trigger.action.radioTransmission(filename, vec3, radio.modulation, false, radio.frequency*1000000, 100) + trigger.action.radioTransmission( filename, vec3, radio.modulation, false, radio.frequency * 1000000, 100 ) -- Display subtitle of message to players. - for _,_player in pairs(self.players) do - local playerData=_player --#AIRBOSS.PlayerData + for _, _player in pairs( self.players ) do + local playerData = _player -- #AIRBOSS.PlayerData -- Message to all players in CCA that have subtites on. - if playerData.unit:IsInZone(self.zoneCCA) and playerData.actype~=AIRBOSS.AircraftCarrier.A4EC then + if playerData.unit:IsInZone( self.zoneCCA ) and playerData.actype ~= AIRBOSS.AircraftCarrier.A4EC then -- Only to players with subtitle on or if noise is played. - if playerData.subtitles or self:_NeedsSubtitle(call) then + if playerData.subtitles or self:_NeedsSubtitle( call ) then -- Messages to marshal to everyone. Messages on LSO radio only to those in the pattern. - if radio.alias=="MARSHAL" or (radio.alias=="LSO" and self:_InQueue(self.Qpattern, playerData.group)) then + if radio.alias == "MARSHAL" or (radio.alias == "LSO" and self:_InQueue( self.Qpattern, playerData.group )) then -- Message to player. - self:MessageToPlayer(playerData, subtitle, nil, "", call.subduration or 5) + self:MessageToPlayer( playerData, subtitle, nil, "", call.subduration or 5 ) end @@ -112414,17 +118918,17 @@ function AIRBOSS:Broadcast(radio, call, loud) ---------------- -- Workaround for the community A-4E-C as long as their radios are not functioning properly. - for _,_player in pairs(self.players) do - local playerData=_player --#AIRBOSS.PlayerData + for _, _player in pairs( self.players ) do + local playerData = _player -- #AIRBOSS.PlayerData -- Easy comms if globally activated but definitly for all player in the community A-4E. - if self.usersoundradio or playerData.actype==AIRBOSS.AircraftCarrier.A4EC then + if self.usersoundradio or playerData.actype == AIRBOSS.AircraftCarrier.A4EC then -- Messages to marshal to everyone. Messages on LSO radio only to those in the pattern. - if radio.alias=="MARSHAL" or (radio.alias=="LSO" and self:_InQueue(self.Qpattern, playerData.group)) then + if radio.alias == "MARSHAL" or (radio.alias == "LSO" and self:_InQueue( self.Qpattern, playerData.group )) then -- User sound to players (inside CCA). - self:Sound2Player(playerData, radio, call, loud) + self:Sound2Player( playerData, radio, call, loud ) end end @@ -112439,23 +118943,23 @@ end -- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. -- @param #boolean loud If true, play loud sound file version. -- @param #number delay Delay in seconds, before the message is broadcasted. -function AIRBOSS:Sound2Player(playerData, radio, call, loud, delay) +function AIRBOSS:Sound2Player( playerData, radio, call, loud, delay ) -- Only to players inside the CCA. - if playerData.unit:IsInZone(self.zoneCCA) and call then + if playerData.unit:IsInZone( self.zoneCCA ) and call then -- Construct file name. - local filename=self:_RadioFilename(call, loud, radio.alias) + local filename = self:_RadioFilename( call, loud, radio.alias ) -- Get Subtitle - local subtitle=self:_RadioSubtitle(radio, call, loud) + local subtitle = self:_RadioSubtitle( radio, call, loud ) -- Play sound file via usersound trigger. - USERSOUND:New(filename):ToGroup(playerData.group, delay) + USERSOUND:New( filename ):ToGroup( playerData.group, delay ) -- Only to players with subtitle on or if noise is played. - if playerData.subtitles or self:_NeedsSubtitle(call) then - self:MessageToPlayer(playerData, subtitle, nil, "", call.subduration, false, delay) + if playerData.subtitles or self:_NeedsSubtitle( call ) then + self:MessageToPlayer( playerData, subtitle, nil, "", call.subduration, false, delay ) end end @@ -112467,44 +118971,44 @@ end -- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. -- @param #boolean loud If true, append "!" else ".". -- @return #string Subtitle to be displayed. -function AIRBOSS:_RadioSubtitle(radio, call, loud) +function AIRBOSS:_RadioSubtitle( radio, call, loud ) -- No subtitle if call is nil, or subtitle is nil or subtitle is empty. - if call==nil or call.subtitle==nil or call.subtitle=="" then + if call == nil or call.subtitle == nil or call.subtitle == "" then return "" end -- Sender - local sender=call.sender or radio.alias + local sender = call.sender or radio.alias if call.modexsender then - sender=call.modexsender + sender = call.modexsender end -- Modex of receiver. - local receiver=call.modexreceiver or "" + local receiver = call.modexreceiver or "" -- Init subtitle. - local subtitle=string.format("%s: %s", sender, call.subtitle) - if receiver and receiver~="" then - subtitle=string.format("%s: %s, %s", sender, receiver, call.subtitle) + local subtitle = string.format( "%s: %s", sender, call.subtitle ) + if receiver and receiver ~= "" then + subtitle = string.format( "%s: %s, %s", sender, receiver, call.subtitle ) end -- Last character of the string. - local lastchar=string.sub(subtitle, -1) + local lastchar = string.sub( subtitle, -1 ) -- Append ! or . if loud then - if lastchar=="." or lastchar=="!" then - subtitle=string.sub(subtitle, 1,-1) + if lastchar == "." or lastchar == "!" then + subtitle = string.sub( subtitle, 1, -1 ) end - subtitle=subtitle.."!" + subtitle = subtitle .. "!" else - if lastchar=="!" then + if lastchar == "!" then -- This also okay. - elseif lastchar=="." then + elseif lastchar == "." then -- Nothing to do. else - subtitle=subtitle.."." + subtitle = subtitle .. "." end end @@ -112517,30 +119021,30 @@ end -- @param #boolean loud Use loud version of file if available. -- @param #string channel Radio channel alias "LSO" or "LSOCall", "MARSHAL" or "MarshalCall". -- @return #string The file name of the radio sound. -function AIRBOSS:_RadioFilename(call, loud, channel) +function AIRBOSS:_RadioFilename( call, loud, channel ) -- Construct file name and subtitle. - local prefix=call.file or "" - local suffix=call.suffix or "ogg" + local prefix = call.file or "" + local suffix = call.suffix or "ogg" -- Path to sound files. Default is in the ME - local path=self.soundfolder or "l10n/DEFAULT/" + local path = self.soundfolder or "l10n/DEFAULT/" -- Check for special LSO and Marshal sound folders. - if string.find(call.file, "LSO-") and channel and (channel=="LSO" or channel=="LSOCall") then - path=self.soundfolderLSO or path + if string.find( call.file, "LSO-" ) and channel and (channel == "LSO" or channel == "LSOCall") then + path = self.soundfolderLSO or path end - if string.find(call.file, "MARSHAL-") and channel and (channel=="MARSHAL" or channel=="MarshalCall") then - path=self.soundfolderMSH or path + if string.find( call.file, "MARSHAL-" ) and channel and (channel == "MARSHAL" or channel == "MarshalCall") then + path = self.soundfolderMSH or path end -- Loud version. if loud then - prefix=prefix.."_Loud" + prefix = prefix .. "_Loud" end -- File name inclusing path in miz file. - local filename=string.format("%s%s.%s", path, prefix, suffix) + local filename = string.format( "%s%s.%s", path, prefix, suffix ) return filename end @@ -112555,76 +119059,76 @@ end -- @param #number duration Display message duration. Default 10 seconds. -- @param #boolean clear If true, clear screen from previous messages. -- @param #number delay Delay in seconds, before the message is displayed. -function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration, clear, delay) +function AIRBOSS:MessageToPlayer( playerData, message, sender, receiver, duration, clear, delay ) - if playerData and message and message~="" then + if playerData and message and message ~= "" then -- Default duration. - duration=duration or self.Tmessage + duration = duration or self.Tmessage -- Format message. local text - if receiver and receiver=="" then + if receiver and receiver == "" then -- No (blank) receiver. - text=string.format("%s", message) + text = string.format( "%s", message ) else -- Default "receiver" is onboard number of player. - receiver=receiver or playerData.onboard - text=string.format("%s, %s", receiver, message) + receiver = receiver or playerData.onboard + text = string.format( "%s, %s", receiver, message ) end - self:T(self.lid..text) + self:T( self.lid .. text ) - if delay and delay>0 then + if delay and delay > 0 then -- Delayed call. - --SCHEDULER:New(nil, self.MessageToPlayer, {self, playerData, message, sender, receiver, duration, clear}, delay) - self:ScheduleOnce(delay, self.MessageToPlayer, self, playerData, message, sender, receiver, duration, clear) + -- SCHEDULER:New(nil, self.MessageToPlayer, {self, playerData, message, sender, receiver, duration, clear}, delay) + self:ScheduleOnce( delay, self.MessageToPlayer, self, playerData, message, sender, receiver, duration, clear ) else -- Wait until previous sound finished. - local wait=0 + local wait = 0 -- Onboard number to get the attention. - if receiver==playerData.onboard then + if receiver == playerData.onboard then -- Which voice over number to use. - if sender and (sender=="LSO" or sender=="MARSHAL" or sender=="AIRBOSS") then + if sender and (sender == "LSO" or sender == "MARSHAL" or sender == "AIRBOSS") then -- User sound of board number. - wait=wait+self:_Number2Sound(playerData, sender, receiver) + wait = wait + self:_Number2Sound( playerData, sender, receiver ) end end -- Negative. - if string.find(text:lower(), "negative") then - local filename=self:_RadioFilename(self.MarshalCall.NEGATIVE, false, "MARSHAL") - USERSOUND:New(filename):ToGroup(playerData.group, wait) - wait=wait+self.MarshalCall.NEGATIVE.duration + if string.find( text:lower(), "negative" ) then + local filename = self:_RadioFilename( self.MarshalCall.NEGATIVE, false, "MARSHAL" ) + USERSOUND:New( filename ):ToGroup( playerData.group, wait ) + wait = wait + self.MarshalCall.NEGATIVE.duration end -- Affirm. - if string.find(text:lower(), "affirm") then - local filename=self:_RadioFilename(self.MarshalCall.AFFIRMATIVE, false, "MARSHAL") - USERSOUND:New(filename):ToGroup(playerData.group, wait) - wait=wait+self.MarshalCall.AFFIRMATIVE.duration + if string.find( text:lower(), "affirm" ) then + local filename = self:_RadioFilename( self.MarshalCall.AFFIRMATIVE, false, "MARSHAL" ) + USERSOUND:New( filename ):ToGroup( playerData.group, wait ) + wait = wait + self.MarshalCall.AFFIRMATIVE.duration end -- Roger. - if string.find(text:lower(), "roger") then - local filename=self:_RadioFilename(self.MarshalCall.ROGER, false, "MARSHAL") - USERSOUND:New(filename):ToGroup(playerData.group, wait) - wait=wait+self.MarshalCall.ROGER.duration + if string.find( text:lower(), "roger" ) then + local filename = self:_RadioFilename( self.MarshalCall.ROGER, false, "MARSHAL" ) + USERSOUND:New( filename ):ToGroup( playerData.group, wait ) + wait = wait + self.MarshalCall.ROGER.duration end -- Play click sound to end message. - if wait>0 then - local filename=self:_RadioFilename(self.MarshalCall.CLICK) - USERSOUND:New(filename):ToGroup(playerData.group, wait) + if wait > 0 then + local filename = self:_RadioFilename( self.MarshalCall.CLICK ) + USERSOUND:New( filename ):ToGroup( playerData.group, wait ) end -- Text message to player client. if playerData.client then - MESSAGE:New(text, duration, sender, clear):ToClient(playerData.client) + MESSAGE:New( text, duration, sender, clear ):ToClient( playerData.client ) end end @@ -112632,7 +119136,6 @@ function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration end end - --- Send text message to all players in the pattern queue. -- Message format will be "SENDER: RECCEIVER, MESSAGE". -- @param #AIRBOSS self @@ -112642,13 +119145,13 @@ end -- @param #number duration Display message duration. Default 10 seconds. -- @param #boolean clear If true, clear screen from previous messages. -- @param #number delay Delay in seconds, before the message is displayed. -function AIRBOSS:MessageToPattern(message, sender, receiver, duration, clear, delay) +function AIRBOSS:MessageToPattern( message, sender, receiver, duration, clear, delay ) -- Create new (fake) radio call to show the subtitile. - local call=self:_NewRadioCall(self.LSOCall.NOISE, sender or "LSO", message, duration, receiver, sender) + local call = self:_NewRadioCall( self.LSOCall.NOISE, sender or "LSO", message, duration, receiver, sender ) -- Dummy radio transmission to display subtitle only to those who tuned in. - self:RadioTransmission(self.LSORadio, call, false, delay, nil, true) + self:RadioTransmission( self.LSORadio, call, false, delay, nil, true ) end @@ -112661,13 +119164,13 @@ end -- @param #number duration Display message duration. Default 10 seconds. -- @param #boolean clear If true, clear screen from previous messages. -- @param #number delay Delay in seconds, before the message is displayed. -function AIRBOSS:MessageToMarshal(message, sender, receiver, duration, clear, delay) +function AIRBOSS:MessageToMarshal( message, sender, receiver, duration, clear, delay ) -- Create new (fake) radio call to show the subtitile. - local call=self:_NewRadioCall(self.MarshalCall.NOISE, sender or "MARSHAL", message, duration, receiver, sender) + local call = self:_NewRadioCall( self.MarshalCall.NOISE, sender or "MARSHAL", message, duration, receiver, sender ) -- Dummy radio transmission to display subtitle only to those who tuned in. - self:RadioTransmission(self.MarshalRadio, call, false, delay, nil, true) + self:RadioTransmission( self.MarshalRadio, call, false, delay, nil, true ) end @@ -112679,28 +119182,28 @@ end -- @param #number subduration Time in seconds the subtitle is displayed. Default 10 seconds. -- @param #string modexreceiver Onboard number of the receiver or nil. -- @param #string modexsender Onboard number of the sender or nil. -function AIRBOSS:_NewRadioCall(call, sender, subtitle, subduration, modexreceiver, modexsender) +function AIRBOSS:_NewRadioCall( call, sender, subtitle, subduration, modexreceiver, modexsender ) -- Create a new call - local newcall=UTILS.DeepCopy(call) --#AIRBOSS.RadioCall + local newcall = UTILS.DeepCopy( call ) -- #AIRBOSS.RadioCall -- Sender for displaying the subtitle. - newcall.sender=sender + newcall.sender = sender -- Subtitle of the message. - newcall.subtitle=subtitle or call.subtitle + newcall.subtitle = subtitle or call.subtitle -- Duration of subtitle display. - newcall.subduration=subduration or self.Tmessage + newcall.subduration = subduration or self.Tmessage -- Tail number of the receiver. - if self:_IsOnboard(modexreceiver) then - newcall.modexreceiver=modexreceiver + if self:_IsOnboard( modexreceiver ) then + newcall.modexreceiver = modexreceiver end -- Tail number of the sender. - if self:_IsOnboard(modexsender) then - newcall.modexsender=modexsender + if self:_IsOnboard( modexsender ) then + newcall.modexsender = modexsender end return newcall @@ -112710,27 +119213,27 @@ end -- @param #AIRBOSS self -- @param #AIRBOSS.Radio radio Airboss radio data. -- @return Wrapper.Unit#UNIT Sending aircraft unit or nil if was not setup, is not an aircraft or is not alive. -function AIRBOSS:_GetRadioSender(radio) +function AIRBOSS:_GetRadioSender( radio ) -- Check if we have a sending aircraft. - local sender=nil --Wrapper.Unit#UNIT + local sender = nil -- Wrapper.Unit#UNIT -- Try the general default. if self.senderac then - sender=UNIT:FindByName(self.senderac) + sender = UNIT:FindByName( self.senderac ) end -- Try the specific marshal unit. - if radio.alias=="MARSHAL" then + if radio.alias == "MARSHAL" then if self.radiorelayMSH then - sender=UNIT:FindByName(self.radiorelayMSH) + sender = UNIT:FindByName( self.radiorelayMSH ) end end -- Try the specific LSO unit. - if radio.alias=="LSO" then + if radio.alias == "LSO" then if self.radiorelayLSO then - sender=UNIT:FindByName(self.radiorelayLSO) + sender = UNIT:FindByName( self.radiorelayLSO ) end end @@ -112746,25 +119249,25 @@ end -- @param #AIRBOSS self -- @param #string text Text to check. -- @return #boolean If true, text is an onboard number of a flight. -function AIRBOSS:_IsOnboard(text) +function AIRBOSS:_IsOnboard( text ) -- Nil check. - if text==nil then + if text == nil then return false end -- Message to all. - if text=="99" then + if text == "99" then return true end -- Loop over all flights. - for _,_flight in pairs(self.flights) do - local flight=_flight --#AIRBOSS.FlightGroup + for _, _flight in pairs( self.flights ) do + local flight = _flight -- #AIRBOSS.FlightGroup -- Loop over all onboard number of that flight. - for _,onboard in pairs(flight.onboardnumbers) do - if text==onboard then + for _, onboard in pairs( flight.onboardnumbers ) do + if text == onboard then return true end end @@ -112781,55 +119284,55 @@ end -- @param #string number Number string, e.g. "032" or "183". -- @param #number delay Delay before transmission in seconds. -- @return #number Duration of the call in seconds. -function AIRBOSS:_Number2Sound(playerData, sender, number, delay) +function AIRBOSS:_Number2Sound( playerData, sender, number, delay ) -- Default. - delay=delay or 0 + delay = delay or 0 --- Split string into characters. - local function _split(str) - local chars={} - for i=1,#str do - local c=str:sub(i,i) - table.insert(chars, c) + local function _split( str ) + local chars = {} + for i = 1, #str do + local c = str:sub( i, i ) + table.insert( chars, c ) end return chars end -- Sender local Sender - if sender=="LSO" then - Sender="LSOCall" - elseif sender=="MARSHAL" or sender=="AIRBOSS" then - Sender="MarshalCall" + if sender == "LSO" then + Sender = "LSOCall" + elseif sender == "MARSHAL" or sender == "AIRBOSS" then + Sender = "MarshalCall" else - self:E(self.lid..string.format("ERROR: Unknown radio sender %s!", tostring(sender))) + self:E( self.lid .. string.format( "ERROR: Unknown radio sender %s!", tostring( sender ) ) ) return end -- Split string into characters. - local numbers=_split(number) + local numbers = _split( number ) - local wait=0 - for i=1,#numbers do + local wait = 0 + for i = 1, #numbers do -- Current number - local n=numbers[i] + local n = numbers[i] -- Convert to N0, N1, ... - local N=string.format("N%s", n) + local N = string.format( "N%s", n ) -- Radio call. - local call=self[Sender][N] --#AIRBOSS.RadioCall + local call = self[Sender][N] -- #AIRBOSS.RadioCall -- Create file name. - local filename=self:_RadioFilename(call, false, Sender) + local filename = self:_RadioFilename( call, false, Sender ) -- Play sound. - USERSOUND:New(filename):ToGroup(playerData.group, delay+wait) + USERSOUND:New( filename ):ToGroup( playerData.group, delay + wait ) -- Wait until this call is over before playing the next. - wait=wait+call.duration + wait = wait + call.duration end return wait @@ -112844,120 +119347,200 @@ end -- @param #number interval Interval between the next call. -- @param #boolean pilotcall If true, use pilot sound files. -- @return #number Duration of the call in seconds. -function AIRBOSS:_Number2Radio(radio, number, delay, interval, pilotcall) +function AIRBOSS:_Number2Radio( radio, number, delay, interval, pilotcall ) --- Split string into characters. - local function _split(str) - local chars={} - for i=1,#str do - local c=str:sub(i,i) - table.insert(chars, c) + local function _split( str ) + local chars = {} + for i = 1, #str do + local c = str:sub( i, i ) + table.insert( chars, c ) end return chars end -- Sender. - local Sender="" - if radio.alias=="LSO" then - Sender="LSOCall" - elseif radio.alias=="MARSHAL" then - Sender="MarshalCall" + local Sender = "" + if radio.alias == "LSO" then + Sender = "LSOCall" + elseif radio.alias == "MARSHAL" then + Sender = "MarshalCall" else - self:E(self.lid..string.format("ERROR: Unknown radio alias %s!", tostring(radio.alias))) + self:E( self.lid .. string.format( "ERROR: Unknown radio alias %s!", tostring( radio.alias ) ) ) end if pilotcall then - Sender="PilotCall" + Sender = "PilotCall" end -- Split string into characters. - local numbers=_split(number) + local numbers = _split( number ) - local wait=0 - for i=1,#numbers do + local wait = 0 + for i = 1, #numbers do -- Current number - local n=numbers[i] + local n = numbers[i] -- Convert to N0, N1, ... - local N=string.format("N%s", n) + local N = string.format( "N%s", n ) -- Radio call. - local call=self[Sender][N] --#AIRBOSS.RadioCall + local call = self[Sender][N] -- #AIRBOSS.RadioCall - if interval and i==1 then + if interval and i == 1 then -- Transmit. - self:RadioTransmission(radio, call, false, delay, interval) + self:RadioTransmission( radio, call, false, delay, interval ) else - self:RadioTransmission(radio, call, false, delay) + self:RadioTransmission( radio, call, false, delay ) end -- Add up duration of the number. - wait=wait+call.duration + wait = wait + call.duration end -- Return the total duration of the call. return wait end +--- Aircraft request marshal (Inbound call both for players and AI). +-- @param #AIRBOSS self +-- @return Wrapper.Unit#UNIT Unit of player or nil. +-- @param #string modex Tail number. +function AIRBOSS:_MarshallInboundCall(unit, modex) + + -- Calculate + local vectorCarrier = self:GetCoordinate():GetDirectionVec3(unit:GetCoordinate()) + local bearing = UTILS.Round(unit:GetCoordinate():GetAngleDegrees( vectorCarrier ), 0) + local distance = UTILS.Round(UTILS.MetersToNM(unit:GetCoordinate():Get2DDistance(self:GetCoordinate())),0) + local angels = UTILS.Round(UTILS.MetersToFeet(unit:GetHeight()/1000),0) + local state = UTILS.Round(self:_GetFuelState(unit)/1000,1) + + -- Pilot: "Marshall, [modex], marking mom's [bearing] for [distance], angels [XX], state [X.X]" + local text=string.format("Marshal, %s, marking mom's %d for %d, angels %d, state %.1f", modex, bearing, distance, angels, state) + -- Debug message. + self:T(self.lid..text) + + -- Fuel state. + local FS=UTILS.Split(string.format("%.1f", state), ".") + + -- Create new call to display complete subtitle. + local inboundcall=self:_NewRadioCall(self.MarshalCall.CLICK, unit.UnitName:upper() , text, self.Tmessage, nil, unit.UnitName:upper()) + + -- CLICK! + self:RadioTransmission(self.MarshalRadio, inboundcall) + -- Marshal .. + self:RadioTransmission(self.MarshalRadio, self.PilotCall.MARSHAL, nil, nil, nil, nil, true) + -- Modex.. + self:_Number2Radio(self.MarshalRadio, modex, nil, nil, true) + -- Marking Mom's, + self:RadioTransmission(self.MarshalRadio, self.PilotCall.MARKINGMOMS, nil, nil, nil, nil, true) + -- Bearing .. + self:_Number2Radio(self.MarshalRadio, tostring(bearing), nil, nil, true) + -- For .. + self:RadioTransmission(self.MarshalRadio, self.PilotCall.FOR, nil, nil, nil, nil, true) + -- Distance .. + self:_Number2Radio(self.MarshalRadio, tostring(distance), nil, nil, true) + -- Angels .. + self:RadioTransmission(self.MarshalRadio, self.PilotCall.ANGELS, nil, nil, nil, nil, true) + -- Angels Number .. + self:_Number2Radio(self.MarshalRadio, tostring(angels), nil, nil, true) + -- State .. + self:RadioTransmission(self.MarshalRadio, self.PilotCall.STATE, nil, nil, nil, nil, true) + -- X.. + self:_Number2Radio(self.MarshalRadio, FS[1], nil, nil, true) + -- Point.. + self:RadioTransmission(self.MarshalRadio, self.PilotCall.POINT, nil, nil, nil, nil, true) + -- Y. + self:_Number2Radio(self.MarshalRadio, FS[2], nil, nil, true) + -- CLICK! + self:RadioTransmission(self.MarshalRadio, self.MarshalRadio.CLICK, nil, nil, nil, nil, true) + +end + +--- Aircraft commencing call (both for players and AI). +-- @param #AIRBOSS self +-- @return Wrapper.Unit#UNIT Unit of player or nil. +-- @param #string modex Tail number. +function AIRBOSS:_CommencingCall(unit, modex) + + -- Pilot: "[modex], commencing" + local text=string.format("%s, commencing", modex) + -- Debug message. + self:T(self.lid..text) + + -- Create new call to display complete subtitle. + local commencingCall=self:_NewRadioCall(self.MarshalCall.CLICK, unit.UnitName:upper() , text, self.Tmessage, nil, unit.UnitName:upper()) + + -- Click + self:RadioTransmission(self.MarshalRadio, commencingCall) + -- Modex.. + self:_Number2Radio(self.MarshalRadio, modex, nil, nil, true) + -- Commencing + self:RadioTransmission(self.MarshalRadio, self.PilotCall.COMMENCING, nil, nil, nil, nil, true) + -- CLICK! + self:RadioTransmission(self.MarshalRadio, self.MarshalRadio.CLICK, nil, nil, nil, nil, true) + +end --- AI aircraft calls the ball. -- @param #AIRBOSS self -- @param #string modex Tail number. -- @param #string nickname Aircraft nickname. -- @param #number fuelstate Aircraft fuel state in thouthands of pounds. -function AIRBOSS:_LSOCallAircraftBall(modex, nickname, fuelstate) +function AIRBOSS:_LSOCallAircraftBall( modex, nickname, fuelstate ) -- Pilot: "405, Hornet Ball, 3.2" - local text=string.format("%s Ball, %.1f.", nickname, fuelstate) + local text = string.format( "%s Ball, %.1f.", nickname, fuelstate ) -- Debug message. - self:I(self.lid..text) + self:I( self.lid .. text ) -- Nickname UPPERCASE. - local NICKNAME=nickname:upper() + local NICKNAME = nickname:upper() -- Fuel state. - local FS=UTILS.Split(string.format("%.1f", fuelstate), ".") + local FS = UTILS.Split( string.format( "%.1f", fuelstate ), "." ) -- Create new call to display complete subtitle. - local call=self:_NewRadioCall(self.PilotCall[NICKNAME], modex, text, self.Tmessage, nil, modex) + local call = self:_NewRadioCall( self.PilotCall[NICKNAME], modex, text, self.Tmessage, nil, modex ) -- Hornet .. - self:RadioTransmission(self.LSORadio, call, nil, nil, nil, nil, true) + self:RadioTransmission( self.LSORadio, call, nil, nil, nil, nil, true ) -- Ball, - self:RadioTransmission(self.LSORadio, self.PilotCall.BALL, nil, nil, nil, nil, true) + self:RadioTransmission( self.LSORadio, self.PilotCall.BALL, nil, nil, nil, nil, true ) -- X.. - self:_Number2Radio(self.LSORadio, FS[1], nil, nil, true) + self:_Number2Radio( self.LSORadio, FS[1], nil, nil, true ) -- Point.. - self:RadioTransmission(self.LSORadio, self.PilotCall.POINT, nil, nil, nil, nil, true) + self:RadioTransmission( self.LSORadio, self.PilotCall.POINT, nil, nil, nil, nil, true ) -- Y. - self:_Number2Radio(self.LSORadio, FS[2], nil, nil, true) + self:_Number2Radio( self.LSORadio, FS[2], nil, nil, true ) -- CLICK! - self:RadioTransmission(self.LSORadio, self.LSOCall.CLICK) + self:RadioTransmission( self.LSORadio, self.LSOCall.CLICK ) end --- AI is bingo and goes to the recovery tanker. -- @param #AIRBOSS self -- @param #string modex Tail number. -function AIRBOSS:_MarshalCallGasAtTanker(modex) +function AIRBOSS:_MarshalCallGasAtTanker( modex ) -- Subtitle. - local text=string.format("Bingo fuel! Going for gas at the recovery tanker.") + local text = string.format( "Bingo fuel! Going for gas at the recovery tanker." ) -- Debug message. - self:I(self.lid..text) + self:I( self.lid .. text ) + -- Create new call to display complete subtitle. - local call=self:_NewRadioCall(self.PilotCall.BINGOFUEL, modex, text, self.Tmessage, nil, modex) + local call = self:_NewRadioCall( self.PilotCall.BINGOFUEL, modex, text, self.Tmessage, nil, modex ) -- MODEX, bingo fuel! - self:RadioTransmission(self.MarshalRadio, call, nil, nil, nil, nil, true) + self:RadioTransmission( self.MarshalRadio, call, nil, nil, nil, nil, true ) -- Going for fuel at the recovery tanker. Click! - self:RadioTransmission(self.MarshalRadio, self.PilotCall.GASATTANKER, nil, nil, nil, true, true) + self:RadioTransmission( self.MarshalRadio, self.PilotCall.GASATTANKER, nil, nil, nil, true, true ) end @@ -112965,48 +119548,47 @@ end -- @param #AIRBOSS self -- @param #string modex Tail number. -- @param #string divertname Name of the divert field. -function AIRBOSS:_MarshalCallGasAtDivert(modex, divertname) +function AIRBOSS:_MarshalCallGasAtDivert( modex, divertname ) -- Subtitle. - local text=string.format("Bingo fuel! Going for gas at divert field %s.", divertname) + local text = string.format( "Bingo fuel! Going for gas at divert field %s.", divertname ) -- Debug message. - self:I(self.lid..text) + self:I( self.lid .. text ) -- Create new call to display complete subtitle. - local call=self:_NewRadioCall(self.PilotCall.BINGOFUEL, modex, text, self.Tmessage, nil, modex) + local call = self:_NewRadioCall( self.PilotCall.BINGOFUEL, modex, text, self.Tmessage, nil, modex ) -- MODEX, bingo fuel! - self:RadioTransmission(self.MarshalRadio, call, nil, nil, nil, nil, true) + self:RadioTransmission( self.MarshalRadio, call, nil, nil, nil, nil, true ) -- Going for fuel at the divert field. Click! - self:RadioTransmission(self.MarshalRadio, self.PilotCall.GASATDIVERT, nil, nil, nil, true, true) + self:RadioTransmission( self.MarshalRadio, self.PilotCall.GASATDIVERT, nil, nil, nil, true, true ) end - --- Inform everyone that recovery ops are stopped and deck is closed. -- @param #AIRBOSS self -- @param #number case Recovery case. -function AIRBOSS:_MarshalCallRecoveryStopped(case) +function AIRBOSS:_MarshalCallRecoveryStopped( case ) -- Subtitle. - local text=string.format("Case %d recovery ops are stopped. Deck is closed.", case) + local text = string.format( "Case %d recovery ops are stopped. Deck is closed.", case ) -- Debug message. - self:I(self.lid..text) + self:I( self.lid .. text ) -- Create new call to display complete subtitle. - local call=self:_NewRadioCall(self.MarshalCall.CASE, "AIRBOSS", text, self.Tmessage, "99") + local call = self:_NewRadioCall( self.MarshalCall.CASE, "AIRBOSS", text, self.Tmessage, "99" ) -- 99, Case.. - self:RadioTransmission(self.MarshalRadio, call) + self:RadioTransmission( self.MarshalRadio, call ) -- X. - self:_Number2Radio(self.MarshalRadio, tostring(case)) + self:_Number2Radio( self.MarshalRadio, tostring( case ) ) -- recovery ops are stopped. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.RECOVERYOPSSTOPPED, nil, nil, 0.2) + self:RadioTransmission( self.MarshalRadio, self.MarshalCall.RECOVERYOPSSTOPPED, nil, nil, 0.2 ) -- Deck is closed. Click! - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.DECKCLOSED, nil, nil, nil, true) + self:RadioTransmission( self.MarshalRadio, self.MarshalCall.DECKCLOSED, nil, nil, nil, true ) end @@ -113015,68 +119597,67 @@ end function AIRBOSS:_MarshalCallRecoveryPausedUntilFurtherNotice() -- Create new call. Subtitle already set. - local call=self:_NewRadioCall(self.MarshalCall.RECOVERYPAUSEDNOTICE, "AIRBOSS", nil, self.Tmessage, "99") + local call = self:_NewRadioCall( self.MarshalCall.RECOVERYPAUSEDNOTICE, "AIRBOSS", nil, self.Tmessage, "99" ) -- 99, aircraft recovery is paused until further notice. - self:RadioTransmission(self.MarshalRadio, call, nil, nil, nil, true) + self:RadioTransmission( self.MarshalRadio, call, nil, nil, nil, true ) end --- Inform everyone that recovery is paused and will resume at a certain time. -- @param #AIRBOSS self -- @param #string clock Time. -function AIRBOSS:_MarshalCallRecoveryPausedResumedAt(clock) +function AIRBOSS:_MarshalCallRecoveryPausedResumedAt( clock ) -- Get relevant part of clock. - local _clock=UTILS.Split(clock, "+") - local CT=UTILS.Split(_clock[1], ":") + local _clock = UTILS.Split( clock, "+" ) + local CT = UTILS.Split( _clock[1], ":" ) -- Subtitle. - local text=string.format("aircraft recovery is paused and will be resumed at %s.", clock) + local text = string.format( "aircraft recovery is paused and will be resumed at %s.", clock ) -- Debug message. - self:I(self.lid..text) + self:I( self.lid .. text ) -- Create new call with full subtitle. - local call=self:_NewRadioCall(self.MarshalCall.RECOVERYPAUSEDRESUMED, "AIRBOSS", text, self.Tmessage, "99") + local call = self:_NewRadioCall( self.MarshalCall.RECOVERYPAUSEDRESUMED, "AIRBOSS", text, self.Tmessage, "99" ) -- 99, aircraft recovery is paused and will resume at... - self:RadioTransmission(self.MarshalRadio, call) + self:RadioTransmission( self.MarshalRadio, call ) -- XY.. (hours) - self:_Number2Radio(self.MarshalRadio, CT[1]) + self:_Number2Radio( self.MarshalRadio, CT[1] ) -- XY (minutes).. - self:_Number2Radio(self.MarshalRadio, CT[2]) + self:_Number2Radio( self.MarshalRadio, CT[2] ) -- hours. Click! - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.HOURS, nil, nil, nil, true) + self:RadioTransmission( self.MarshalRadio, self.MarshalCall.HOURS, nil, nil, nil, true ) end - --- Inform flight that he is cleared for recovery. -- @param #AIRBOSS self -- @param #string modex Tail number. -- @param #number case Recovery case. -function AIRBOSS:_MarshalCallClearedForRecovery(modex, case) +function AIRBOSS:_MarshalCallClearedForRecovery( modex, case ) -- Subtitle. - local text=string.format("you're cleared for Case %d recovery.", case) + local text = string.format( "you're cleared for Case %d recovery.", case ) -- Debug message. - self:I(self.lid..text) + self:I( self.lid .. text ) -- Create new call with full subtitle. - local call=self:_NewRadioCall(self.MarshalCall.CLEAREDFORRECOVERY, "MARSHAL", text, self.Tmessage, modex) + local call = self:_NewRadioCall( self.MarshalCall.CLEAREDFORRECOVERY, "MARSHAL", text, self.Tmessage, modex ) -- Two second delay. - local delay=2 + local delay = 2 -- XYZ, you're cleared for case.. - self:RadioTransmission(self.MarshalRadio, call, nil, delay) + self:RadioTransmission( self.MarshalRadio, call, nil, delay ) -- X.. - self:_Number2Radio(self.MarshalRadio, tostring(case), delay) + self:_Number2Radio( self.MarshalRadio, tostring( case ), delay ) -- recovery. Click! - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.RECOVERY, nil, delay, nil, true) + self:RadioTransmission( self.MarshalRadio, self.MarshalCall.RECOVERY, nil, delay, nil, true ) end @@ -113085,56 +119666,56 @@ end function AIRBOSS:_MarshalCallResumeRecovery() -- Create new call with full subtitle. - local call=self:_NewRadioCall(self.MarshalCall.RESUMERECOVERY, "AIRBOSS", nil, self.Tmessage, "99") + local call = self:_NewRadioCall( self.MarshalCall.RESUMERECOVERY, "AIRBOSS", nil, self.Tmessage, "99" ) -- 99, aircraft recovery resumed. Click! - self:RadioTransmission(self.MarshalRadio, call, nil, nil, nil, true) + self:RadioTransmission( self.MarshalRadio, call, nil, nil, nil, true ) end --- Inform everyone about new final bearing. -- @param #AIRBOSS self -- @param #number FB Final Bearing in degrees. -function AIRBOSS:_MarshalCallNewFinalBearing(FB) +function AIRBOSS:_MarshalCallNewFinalBearing( FB ) -- Subtitle. - local text=string.format("new final bearing %03d°.", FB) + local text = string.format( "new final bearing %03d°.", FB ) -- Debug message. - self:I(self.lid..text) + self:I( self.lid .. text ) -- Create new call with full subtitle. - local call=self:_NewRadioCall(self.MarshalCall.NEWFB, "AIRBOSS", text, self.Tmessage, "99") + local call = self:_NewRadioCall( self.MarshalCall.NEWFB, "AIRBOSS", text, self.Tmessage, "99" ) -- 99, new final bearing.. - self:RadioTransmission(self.MarshalRadio, call) + self:RadioTransmission( self.MarshalRadio, call ) -- XYZ.. - self:_Number2Radio(self.MarshalRadio, string.format("%03d", FB), nil, 0.2) + self:_Number2Radio( self.MarshalRadio, string.format( "%03d", FB ), nil, 0.2 ) -- Degrees. Click! - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.DEGREES, nil, nil, nil, true) + self:RadioTransmission( self.MarshalRadio, self.MarshalCall.DEGREES, nil, nil, nil, true ) end --- Compile a radio call when Marshal tells a flight the holding alitude. -- @param #AIRBOSS self -- @param #number hdg Heading in degrees. -function AIRBOSS:_MarshalCallCarrierTurnTo(hdg) +function AIRBOSS:_MarshalCallCarrierTurnTo( hdg ) -- Subtitle. - local text=string.format("carrier is now starting turn to heading %03d°.", hdg) + local text = string.format( "carrier is now starting turn to heading %03d°.", hdg ) -- Debug message. - self:I(self.lid..text) + self:I( self.lid .. text ) -- Create new call with full subtitle. - local call=self:_NewRadioCall(self.MarshalCall.CARRIERTURNTOHEADING, "AIRBOSS", text, self.Tmessage, "99") + local call = self:_NewRadioCall( self.MarshalCall.CARRIERTURNTOHEADING, "AIRBOSS", text, self.Tmessage, "99" ) -- 99, turning to heading... - self:RadioTransmission(self.MarshalRadio, call) + self:RadioTransmission( self.MarshalRadio, call ) -- XYZ.. - self:_Number2Radio(self.MarshalRadio, string.format("%03d", hdg), nil, 0.2) + self:_Number2Radio( self.MarshalRadio, string.format( "%03d", hdg ), nil, 0.2 ) -- Degrees. Click! - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.DEGREES, nil, nil, nil, true) + self:RadioTransmission( self.MarshalRadio, self.MarshalCall.DEGREES, nil, nil, nil, true ) end @@ -113142,64 +119723,64 @@ end -- @param #AIRBOSS self -- @param #string modex Tail number. -- @param #number nwaiting Number of flights already waiting. -function AIRBOSS:_MarshalCallStackFull(modex, nwaiting) +function AIRBOSS:_MarshalCallStackFull( modex, nwaiting ) -- Subtitle. - local text=string.format("Marshal stack is currently full. Hold outside 10 NM zone and wait for further instructions. ") - if nwaiting==1 then - text=text..string.format("There is one flight ahead of you.") - elseif nwaiting>1 then - text=text..string.format("There are %d flights ahead of you.", nwaiting) + local text = string.format( "Marshal stack is currently full. Hold outside 10 NM zone and wait for further instructions. " ) + if nwaiting == 1 then + text = text .. string.format( "There is one flight ahead of you." ) + elseif nwaiting > 1 then + text = text .. string.format( "There are %d flights ahead of you.", nwaiting ) else - text=text..string.format("You are next in line.") + text = text .. string.format( "You are next in line." ) end -- Debug message. - self:I(self.lid..text) + self:I( self.lid .. text ) -- Create new call with full subtitle. - local call=self:_NewRadioCall(self.MarshalCall.STACKFULL, "AIRBOSS", text, self.Tmessage, modex) + local call = self:_NewRadioCall( self.MarshalCall.STACKFULL, "AIRBOSS", text, self.Tmessage, modex ) -- XYZ, Marshal stack is currently full. - self:RadioTransmission(self.MarshalRadio, call, nil, nil, nil, true) + self:RadioTransmission( self.MarshalRadio, call, nil, nil, nil, true ) end --- Compile a radio call when Marshal tells a flight the holding alitude. -- @param #AIRBOSS self -function AIRBOSS:_MarshalCallRecoveryStart(case) +function AIRBOSS:_MarshalCallRecoveryStart( case ) -- Marshal radial. - local radial=self:GetRadial(case, true, true, false) + local radial = self:GetRadial( case, true, true, false ) -- Debug output. - local text=string.format("Starting aircraft recovery Case %d ops.", case) - if case==1 then - text=text..string.format(" BRC %03d°.", self:GetBRC()) - elseif case==2 then - text=text..string.format(" Marshal radial %03d°. BRC %03d°.", radial, self:GetBRC()) - elseif case==3 then - text=text..string.format(" Marshal radial %03d°. Final heading %03d°.", radial, self:GetFinalBearing(false)) + local text = string.format( "Starting aircraft recovery Case %d ops.", case ) + if case == 1 then + text = text .. string.format( " BRC %03d°.", self:GetBRC() ) + elseif case == 2 then + text = text .. string.format( " Marshal radial %03d°. BRC %03d°.", radial, self:GetBRC() ) + elseif case == 3 then + text = text .. string.format( " Marshal radial %03d°. Final heading %03d°.", radial, self:GetFinalBearing( false ) ) end - self:T(self.lid..text) + self:T( self.lid .. text ) -- New call including the subtitle. - local call=self:_NewRadioCall(self.MarshalCall.STARTINGRECOVERY, "AIRBOSS", text, self.Tmessage, "99") + local call = self:_NewRadioCall( self.MarshalCall.STARTINGRECOVERY, "AIRBOSS", text, self.Tmessage, "99" ) -- 99, Starting aircraft recovery case.. - self:RadioTransmission(self.MarshalRadio, call) + self:RadioTransmission( self.MarshalRadio, call ) -- X.. - self:_Number2Radio(self.MarshalRadio,tostring(case), nil, 0.1) + self:_Number2Radio( self.MarshalRadio, tostring( case ), nil, 0.1 ) -- ops. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.OPS) + self:RadioTransmission( self.MarshalRadio, self.MarshalCall.OPS ) - --Marshal Radial - if case>1 then + -- Marshal Radial + if case > 1 then -- Marshal radial.. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.MARSHALRADIAL) + self:RadioTransmission( self.MarshalRadio, self.MarshalCall.MARSHALRADIAL ) -- XYZ.. - self:_Number2Radio(self.MarshalRadio, string.format("%03d", radial), nil, 0.2) + self:_Number2Radio( self.MarshalRadio, string.format( "%03d", radial ), nil, 0.2 ) -- Degrees. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.DEGREES, nil, nil, nil, true) + self:RadioTransmission( self.MarshalRadio, self.MarshalCall.DEGREES, nil, nil, nil, true ) end end @@ -113212,68 +119793,66 @@ end -- @param #number altitude Holding alitude. -- @param #string charlie Charlie Time estimate. -- @param #number qfe Alitmeter inHg. -function AIRBOSS:_MarshalCallArrived(modex, case, brc, altitude, charlie, qfe) - self:F({modex=modex,case=case,brc=brc,altitude=altitude,charlie=charlie,qfe=qfe}) +function AIRBOSS:_MarshalCallArrived( modex, case, brc, altitude, charlie, qfe ) + self:F( { modex = modex, case = case, brc = brc, altitude = altitude, charlie = charlie, qfe = qfe } ) -- Split strings etc. - local angels=self:_GetAngels(altitude) - --local QFE=UTILS.Split(tostring(UTILS.Round(qfe,2)), ".") - local QFE=UTILS.Split(string.format("%.2f", qfe), ".") - local clock=UTILS.Split(charlie, "+") - local CT=UTILS.Split(clock[1], ":") + local angels = self:_GetAngels( altitude ) + -- local QFE=UTILS.Split(tostring(UTILS.Round(qfe,2)), ".") + local QFE = UTILS.Split( string.format( "%.2f", qfe ), "." ) + local clock = UTILS.Split( charlie, "+" ) + local CT = UTILS.Split( clock[1], ":" ) -- Subtitle text. - local text=string.format("Case %d, expected BRC %03d°, hold at angels %d. Expected Charlie Time %s. Altimeter %.2f. Report see me.", case, brc, angels, charlie, qfe) + local text = string.format( "Case %d, expected BRC %03d°, hold at angels %d. Expected Charlie Time %s. Altimeter %.2f. Report see me.", case, brc, angels, charlie, qfe ) -- Debug message. - self:I(self.lid..text) + self:I( self.lid .. text ) -- Create new call to display complete subtitle. - local casecall=self:_NewRadioCall(self.MarshalCall.CASE, "MARSHAL", text, self.Tmessage, modex) + local casecall = self:_NewRadioCall( self.MarshalCall.CASE, "MARSHAL", text, self.Tmessage, modex ) -- Case.. - self:RadioTransmission(self.MarshalRadio, casecall) + self:RadioTransmission( self.MarshalRadio, casecall ) -- X. - self:_Number2Radio(self.MarshalRadio, tostring(case)) + self:_Number2Radio( self.MarshalRadio, tostring( case ) ) -- Expected.. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.EXPECTED, nil, nil, 0.5) + self:RadioTransmission( self.MarshalRadio, self.MarshalCall.EXPECTED, nil, nil, 0.5 ) -- BRC.. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.BRC) + self:RadioTransmission( self.MarshalRadio, self.MarshalCall.BRC ) -- XYZ... - self:_Number2Radio(self.MarshalRadio, string.format("%03d", brc)) + self:_Number2Radio( self.MarshalRadio, string.format( "%03d", brc ) ) -- Degrees. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.DEGREES) - + self:RadioTransmission( self.MarshalRadio, self.MarshalCall.DEGREES ) -- Hold at.. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.HOLDATANGELS, nil, nil, 0.5) + self:RadioTransmission( self.MarshalRadio, self.MarshalCall.HOLDATANGELS, nil, nil, 0.5 ) -- X. - self:_Number2Radio(self.MarshalRadio, tostring(angels)) + self:_Number2Radio( self.MarshalRadio, tostring( angels ) ) -- Expected.. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.EXPECTED, nil, nil, 0.5) + self:RadioTransmission( self.MarshalRadio, self.MarshalCall.EXPECTED, nil, nil, 0.5 ) -- Charlie time.. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.CHARLIETIME) + self:RadioTransmission( self.MarshalRadio, self.MarshalCall.CHARLIETIME ) -- XY.. (hours) - self:_Number2Radio(self.MarshalRadio, CT[1]) + self:_Number2Radio( self.MarshalRadio, CT[1] ) -- XY (minutes). - self:_Number2Radio(self.MarshalRadio, CT[2]) + self:_Number2Radio( self.MarshalRadio, CT[2] ) -- hours. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.HOURS) - + self:RadioTransmission( self.MarshalRadio, self.MarshalCall.HOURS ) -- Altimeter.. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.ALTIMETER, nil, nil, 0.5) + self:RadioTransmission( self.MarshalRadio, self.MarshalCall.ALTIMETER, nil, nil, 0.5 ) -- XY.. - self:_Number2Radio(self.MarshalRadio, QFE[1]) + self:_Number2Radio( self.MarshalRadio, QFE[1] ) -- Point.. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.POINT) + self:RadioTransmission( self.MarshalRadio, self.MarshalCall.POINT ) -- XY. - self:_Number2Radio(self.MarshalRadio, QFE[2]) + self:_Number2Radio( self.MarshalRadio, QFE[2] ) -- Report see me. Click! - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.REPORTSEEME, nil, nil, 0.5, true) + self:RadioTransmission( self.MarshalRadio, self.MarshalCall.REPORTSEEME, nil, nil, 0.5, true ) end @@ -113284,28 +119863,28 @@ end --- Add menu commands for player. -- @param #AIRBOSS self -- @param #string _unitName Name of player unit. -function AIRBOSS:_AddF10Commands(_unitName) - self:F(_unitName) +function AIRBOSS:_AddF10Commands( _unitName ) + self:F( _unitName ) -- Get player unit and name. - local _unit, playername = self:_GetPlayerUnitAndName(_unitName) + local _unit, playername = self:_GetPlayerUnitAndName( _unitName ) -- Check for player unit. if _unit and playername then -- Get group and ID. - local group=_unit:GetGroup() - local gid=group:GetID() + local group = _unit:GetGroup() + local gid = group:GetID() if group and gid then if not self.menuadded[gid] then -- Enable switch so we don't do this twice. - self.menuadded[gid]=true + self.menuadded[gid] = true -- Set menu root path. - local _rootPath=nil + local _rootPath = nil if AIRBOSS.MenuF10Root then ------------------------ -- MISSON LEVEL MENUE -- @@ -113313,10 +119892,10 @@ function AIRBOSS:_AddF10Commands(_unitName) if self.menusingle then -- F10/Airboss/... - _rootPath=AIRBOSS.MenuF10Root + _rootPath = AIRBOSS.MenuF10Root else -- F10/Airboss//... - _rootPath=missionCommands.addSubMenuForGroup(gid, self.alias, AIRBOSS.MenuF10Root) + _rootPath = missionCommands.addSubMenuForGroup( gid, self.alias, AIRBOSS.MenuF10Root ) end else @@ -113325,116 +119904,114 @@ function AIRBOSS:_AddF10Commands(_unitName) ------------------------ -- Main F10 menu: F10/Airboss/ - if AIRBOSS.MenuF10[gid]==nil then - AIRBOSS.MenuF10[gid]=missionCommands.addSubMenuForGroup(gid, "Airboss") + if AIRBOSS.MenuF10[gid] == nil then + AIRBOSS.MenuF10[gid] = missionCommands.addSubMenuForGroup( gid, "Airboss" ) end - if self.menusingle then -- F10/Airboss/... - _rootPath=AIRBOSS.MenuF10[gid] + _rootPath = AIRBOSS.MenuF10[gid] else -- F10/Airboss//... - _rootPath=missionCommands.addSubMenuForGroup(gid, self.alias, AIRBOSS.MenuF10[gid]) + _rootPath = missionCommands.addSubMenuForGroup( gid, self.alias, AIRBOSS.MenuF10[gid] ) end end - -------------------------------- -- F10/Airboss//F1 Help -------------------------------- - local _helpPath=missionCommands.addSubMenuForGroup(gid, "Help", _rootPath) + local _helpPath = missionCommands.addSubMenuForGroup( gid, "Help", _rootPath ) -- F10/Airboss//F1 Help/F1 Mark Zones if self.menumarkzones then - local _markPath=missionCommands.addSubMenuForGroup(gid, "Mark Zones", _helpPath) + local _markPath = missionCommands.addSubMenuForGroup( gid, "Mark Zones", _helpPath ) -- F10/Airboss//F1 Help/F1 Mark Zones/ if self.menusmokezones then - missionCommands.addCommandForGroup(gid, "Smoke Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, false) -- F1 + missionCommands.addCommandForGroup( gid, "Smoke Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, false ) -- F1 end - missionCommands.addCommandForGroup(gid, "Flare Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, true) -- F2 + missionCommands.addCommandForGroup( gid, "Flare Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, true ) -- F2 if self.menusmokezones then - missionCommands.addCommandForGroup(gid, "Smoke Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, false) -- F3 + missionCommands.addCommandForGroup( gid, "Smoke Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, false ) -- F3 end - missionCommands.addCommandForGroup(gid, "Flare Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, true) -- F4 + missionCommands.addCommandForGroup( gid, "Flare Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, true ) -- F4 end -- F10/Airboss//F1 Help/F2 Skill Level - local _skillPath=missionCommands.addSubMenuForGroup(gid, "Skill Level", _helpPath) + local _skillPath = missionCommands.addSubMenuForGroup( gid, "Skill Level", _helpPath ) -- F10/Airboss//F1 Help/F2 Skill Level/ - missionCommands.addCommandForGroup(gid, "Flight Student", _skillPath, self._SetDifficulty, self, _unitName, AIRBOSS.Difficulty.EASY) -- F1 - missionCommands.addCommandForGroup(gid, "Naval Aviator", _skillPath, self._SetDifficulty, self, _unitName, AIRBOSS.Difficulty.NORMAL) -- F2 - missionCommands.addCommandForGroup(gid, "TOPGUN Graduate", _skillPath, self._SetDifficulty, self, _unitName, AIRBOSS.Difficulty.HARD) -- F3 - missionCommands.addCommandForGroup(gid, "Hints On/Off", _skillPath, self._SetHintsOnOff, self, _unitName) -- F4 + missionCommands.addCommandForGroup( gid, "Flight Student", _skillPath, self._SetDifficulty, self, _unitName, AIRBOSS.Difficulty.EASY ) -- F1 + missionCommands.addCommandForGroup( gid, "Naval Aviator", _skillPath, self._SetDifficulty, self, _unitName, AIRBOSS.Difficulty.NORMAL ) -- F2 + missionCommands.addCommandForGroup( gid, "TOPGUN Graduate", _skillPath, self._SetDifficulty, self, _unitName, AIRBOSS.Difficulty.HARD ) -- F3 + missionCommands.addCommandForGroup( gid, "Hints On/Off", _skillPath, self._SetHintsOnOff, self, _unitName ) -- F4 -- F10/Airboss//F1 Help/ - missionCommands.addCommandForGroup(gid, "My Status", _helpPath, self._DisplayPlayerStatus, self, _unitName) -- F3 - missionCommands.addCommandForGroup(gid, "Attitude Monitor", _helpPath, self._DisplayAttitude, self, _unitName) -- F4 - missionCommands.addCommandForGroup(gid, "Radio Check LSO", _helpPath, self._LSORadioCheck, self, _unitName) -- F5 - missionCommands.addCommandForGroup(gid, "Radio Check Marshal", _helpPath, self._MarshalRadioCheck, self, _unitName) -- F6 - missionCommands.addCommandForGroup(gid, "Subtitles On/Off", _helpPath, self._SubtitlesOnOff, self, _unitName) -- F7 - missionCommands.addCommandForGroup(gid, "Trapsheet On/Off", _helpPath, self._TrapsheetOnOff, self, _unitName) -- F8 + missionCommands.addCommandForGroup( gid, "My Status", _helpPath, self._DisplayPlayerStatus, self, _unitName ) -- F3 + missionCommands.addCommandForGroup( gid, "Attitude Monitor", _helpPath, self._DisplayAttitude, self, _unitName ) -- F4 + missionCommands.addCommandForGroup( gid, "Radio Check LSO", _helpPath, self._LSORadioCheck, self, _unitName ) -- F5 + missionCommands.addCommandForGroup( gid, "Radio Check Marshal", _helpPath, self._MarshalRadioCheck, self, _unitName ) -- F6 + missionCommands.addCommandForGroup( gid, "Subtitles On/Off", _helpPath, self._SubtitlesOnOff, self, _unitName ) -- F7 + missionCommands.addCommandForGroup( gid, "Trapsheet On/Off", _helpPath, self._TrapsheetOnOff, self, _unitName ) -- F8 ------------------------------------- -- F10/Airboss//F2 Kneeboard ------------------------------------- - local _kneeboardPath=missionCommands.addSubMenuForGroup(gid, "Kneeboard", _rootPath) + local _kneeboardPath = missionCommands.addSubMenuForGroup( gid, "Kneeboard", _rootPath ) -- F10/Airboss//F2 Kneeboard/F1 Results - local _resultsPath=missionCommands.addSubMenuForGroup(gid, "Results", _kneeboardPath) + local _resultsPath = missionCommands.addSubMenuForGroup( gid, "Results", _kneeboardPath ) -- F10/Airboss//F2 Kneeboard/F1 Results/ - missionCommands.addCommandForGroup(gid, "Greenie Board", _resultsPath, self._DisplayScoreBoard, self, _unitName) -- F1 - missionCommands.addCommandForGroup(gid, "My LSO Grades", _resultsPath, self._DisplayPlayerGrades, self, _unitName) -- F2 - missionCommands.addCommandForGroup(gid, "Last Debrief", _resultsPath, self._DisplayDebriefing, self, _unitName) -- F3 + missionCommands.addCommandForGroup( gid, "Greenie Board", _resultsPath, self._DisplayScoreBoard, self, _unitName ) -- F1 + missionCommands.addCommandForGroup( gid, "My LSO Grades", _resultsPath, self._DisplayPlayerGrades, self, _unitName ) -- F2 + missionCommands.addCommandForGroup( gid, "Last Debrief", _resultsPath, self._DisplayDebriefing, self, _unitName ) -- F3 -- F10/Airboss//F2 Kneeboard/F2 Skipper/ if self.skipperMenu then - local _skipperPath =missionCommands.addSubMenuForGroup(gid, "Skipper", _kneeboardPath) - local _menusetspeed=missionCommands.addSubMenuForGroup(gid, "Set Speed", _skipperPath) - missionCommands.addCommandForGroup(gid, "10 knots", _menusetspeed, self._SkipperRecoverySpeed, self, _unitName, 10) - missionCommands.addCommandForGroup(gid, "15 knots", _menusetspeed, self._SkipperRecoverySpeed, self, _unitName, 15) - missionCommands.addCommandForGroup(gid, "20 knots", _menusetspeed, self._SkipperRecoverySpeed, self, _unitName, 20) - missionCommands.addCommandForGroup(gid, "25 knots", _menusetspeed, self._SkipperRecoverySpeed, self, _unitName, 25) - missionCommands.addCommandForGroup(gid, "30 knots", _menusetspeed, self._SkipperRecoverySpeed, self, _unitName, 30) - local _menusetrtime=missionCommands.addSubMenuForGroup(gid, "Set Time", _skipperPath) - missionCommands.addCommandForGroup(gid, "15 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 15) - missionCommands.addCommandForGroup(gid, "30 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 30) - missionCommands.addCommandForGroup(gid, "45 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 45) - missionCommands.addCommandForGroup(gid, "60 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 60) - missionCommands.addCommandForGroup(gid, "90 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 90) - local _menusetrtime=missionCommands.addSubMenuForGroup(gid, "Set Marshal Radial", _skipperPath) - missionCommands.addCommandForGroup(gid, "+30°", _menusetrtime, self._SkipperRecoveryOffset, self, _unitName, 30) - missionCommands.addCommandForGroup(gid, "+15°", _menusetrtime, self._SkipperRecoveryOffset, self, _unitName, 15) - missionCommands.addCommandForGroup(gid, "0°", _menusetrtime, self._SkipperRecoveryOffset, self, _unitName, 0) - missionCommands.addCommandForGroup(gid, "-15°", _menusetrtime, self._SkipperRecoveryOffset, self, _unitName, -15) - missionCommands.addCommandForGroup(gid, "-30°", _menusetrtime, self._SkipperRecoveryOffset, self, _unitName, -30) - missionCommands.addCommandForGroup(gid, "U-turn On/Off", _skipperPath, self._SkipperRecoveryUturn, self, _unitName) - missionCommands.addCommandForGroup(gid, "Start CASE I", _skipperPath, self._SkipperStartRecovery, self, _unitName, 1) - missionCommands.addCommandForGroup(gid, "Start CASE II", _skipperPath, self._SkipperStartRecovery, self, _unitName, 2) - missionCommands.addCommandForGroup(gid, "Start CASE III",_skipperPath, self._SkipperStartRecovery, self, _unitName, 3) - missionCommands.addCommandForGroup(gid, "Stop Recovery", _skipperPath, self._SkipperStopRecovery, self, _unitName) + local _skipperPath = missionCommands.addSubMenuForGroup( gid, "Skipper", _kneeboardPath ) + local _menusetspeed = missionCommands.addSubMenuForGroup( gid, "Set Speed", _skipperPath ) + missionCommands.addCommandForGroup( gid, "10 knots", _menusetspeed, self._SkipperRecoverySpeed, self, _unitName, 10 ) + missionCommands.addCommandForGroup( gid, "15 knots", _menusetspeed, self._SkipperRecoverySpeed, self, _unitName, 15 ) + missionCommands.addCommandForGroup( gid, "20 knots", _menusetspeed, self._SkipperRecoverySpeed, self, _unitName, 20 ) + missionCommands.addCommandForGroup( gid, "25 knots", _menusetspeed, self._SkipperRecoverySpeed, self, _unitName, 25 ) + missionCommands.addCommandForGroup( gid, "30 knots", _menusetspeed, self._SkipperRecoverySpeed, self, _unitName, 30 ) + local _menusetrtime = missionCommands.addSubMenuForGroup( gid, "Set Time", _skipperPath ) + missionCommands.addCommandForGroup( gid, "15 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 15 ) + missionCommands.addCommandForGroup( gid, "30 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 30 ) + missionCommands.addCommandForGroup( gid, "45 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 45 ) + missionCommands.addCommandForGroup( gid, "60 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 60 ) + missionCommands.addCommandForGroup( gid, "90 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 90 ) + local _menusetrtime = missionCommands.addSubMenuForGroup( gid, "Set Marshal Radial", _skipperPath ) + missionCommands.addCommandForGroup( gid, "+30°", _menusetrtime, self._SkipperRecoveryOffset, self, _unitName, 30 ) + missionCommands.addCommandForGroup( gid, "+15°", _menusetrtime, self._SkipperRecoveryOffset, self, _unitName, 15 ) + missionCommands.addCommandForGroup( gid, "0°", _menusetrtime, self._SkipperRecoveryOffset, self, _unitName, 0 ) + missionCommands.addCommandForGroup( gid, "-15°", _menusetrtime, self._SkipperRecoveryOffset, self, _unitName, -15 ) + missionCommands.addCommandForGroup( gid, "-30°", _menusetrtime, self._SkipperRecoveryOffset, self, _unitName, -30 ) + missionCommands.addCommandForGroup( gid, "U-turn On/Off", _skipperPath, self._SkipperRecoveryUturn, self, _unitName ) + missionCommands.addCommandForGroup( gid, "Start CASE I", _skipperPath, self._SkipperStartRecovery, self, _unitName, 1 ) + missionCommands.addCommandForGroup( gid, "Start CASE II", _skipperPath, self._SkipperStartRecovery, self, _unitName, 2 ) + missionCommands.addCommandForGroup( gid, "Start CASE III", _skipperPath, self._SkipperStartRecovery, self, _unitName, 3 ) + missionCommands.addCommandForGroup( gid, "Stop Recovery", _skipperPath, self._SkipperStopRecovery, self, _unitName ) end -- F10/Airboss// ------------------------- - missionCommands.addCommandForGroup(gid, "Request Marshal", _rootPath, self._RequestMarshal, self, _unitName) -- F3 - missionCommands.addCommandForGroup(gid, "Request Commence", _rootPath, self._RequestCommence, self, _unitName) -- F4 - missionCommands.addCommandForGroup(gid, "Request Refueling", _rootPath, self._RequestRefueling, self, _unitName) -- F5 - missionCommands.addCommandForGroup(gid, "Spinning", _rootPath, self._RequestSpinning, self, _unitName) -- F6 - missionCommands.addCommandForGroup(gid, "Emergency Landing", _rootPath, self._RequestEmergency, self, _unitName) -- F7 - missionCommands.addCommandForGroup(gid, "[Reset My Status]", _rootPath, self._ResetPlayerStatus, self, _unitName) -- F8 + missionCommands.addCommandForGroup( gid, "Request Marshal", _rootPath, self._RequestMarshal, self, _unitName ) -- F3 + missionCommands.addCommandForGroup( gid, "Request Commence", _rootPath, self._RequestCommence, self, _unitName ) -- F4 + missionCommands.addCommandForGroup( gid, "Request Refueling", _rootPath, self._RequestRefueling, self, _unitName ) -- F5 + missionCommands.addCommandForGroup( gid, "Spinning", _rootPath, self._RequestSpinning, self, _unitName ) -- F6 + missionCommands.addCommandForGroup( gid, "Emergency Landing", _rootPath, self._RequestEmergency, self, _unitName ) -- F7 + missionCommands.addCommandForGroup( gid, "[Reset My Status]", _rootPath, self._ResetPlayerStatus, self, _unitName ) -- F8 end else - self:E(self.lid..string.format("ERROR: Could not find group or group ID in AddF10Menu() function. Unit name: %s.", _unitName)) + self:E( self.lid .. string.format( "ERROR: Could not find group or group ID in AddF10Menu() function. Unit name: %s.", _unitName ) ) end else - self:E(self.lid..string.format("ERROR: Player unit does not exist in AddF10Menu() function. Unit name: %s.", _unitName)) + self:E( self.lid .. string.format( "ERROR: Player unit does not exist in AddF10Menu() function. Unit name: %s.", _unitName ) ) end end @@ -113447,37 +120024,37 @@ end -- @param #AIRBOSS self -- @param #string _unitName Name fo the player unit. -- @param #number case Recovery case. -function AIRBOSS:_SkipperStartRecovery(_unitName, case) +function AIRBOSS:_SkipperStartRecovery( _unitName, case ) -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) -- Check if we have a unit which is a player. if _unit and _playername then - local playerData=self.players[_playername] --#AIRBOSS.PlayerData + local playerData = self.players[_playername] -- #AIRBOSS.PlayerData if playerData then -- Inform player. - local text=string.format("affirm, Case %d recovery will start in 5 min for %d min. Wind on deck %d knots. U-turn=%s.", case, self.skipperTime, self.skipperSpeed, tostring(self.skipperUturn)) - if case>1 then - text=text..string.format(" Marshal radial %d°.", self.skipperOffset) - end + local text = string.format( "affirm, Case %d recovery will start in 5 min for %d min. Wind on deck %d knots. U-turn=%s.", case, self.skipperTime, self.skipperSpeed, tostring( self.skipperUturn ) ) + if case > 1 then + text = text .. string.format( " Marshal radial %d°.", self.skipperOffset ) + end if self:IsRecovering() then - text="negative, carrier is already recovering." - self:MessageToPlayer(playerData, text, "AIRBOSS") + text = "negative, carrier is already recovering." + self:MessageToPlayer( playerData, text, "AIRBOSS" ) return end - self:MessageToPlayer(playerData, text, "AIRBOSS") + self:MessageToPlayer( playerData, text, "AIRBOSS" ) -- Recovery staring in 5 min for 30 min. - local t0=timer.getAbsTime()+5*60 - local t9=t0+self.skipperTime*60 - local C0=UTILS.SecondsToClock(t0) - local C9=UTILS.SecondsToClock(t9) + local t0 = timer.getAbsTime() + 5 * 60 + local t9 = t0 + self.skipperTime * 60 + local C0 = UTILS.SecondsToClock( t0 ) + local C9 = UTILS.SecondsToClock( t9 ) -- Carrier will turn into the wind. Wind on deck 25 knots. U-turn on. - self:AddRecoveryWindow(C0, C9, case, self.skipperOffset, true, self.skipperSpeed, self.skipperUturn) + self:AddRecoveryWindow( C0, C9, case, self.skipperOffset, true, self.skipperSpeed, self.skipperUturn ) end end @@ -113486,25 +120063,25 @@ end --- Skipper Stop recovery function. -- @param #AIRBOSS self -- @param #string _unitName Name fo the player unit. -function AIRBOSS:_SkipperStopRecovery(_unitName) +function AIRBOSS:_SkipperStopRecovery( _unitName ) -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) -- Check if we have a unit which is a player. if _unit and _playername then - local playerData=self.players[_playername] --#AIRBOSS.PlayerData + local playerData = self.players[_playername] -- #AIRBOSS.PlayerData if playerData then -- Inform player. - local text="roger, stopping recovery right away." + local text = "roger, stopping recovery right away." if not self:IsRecovering() then - text="negative, carrier is currently not recovering." - self:MessageToPlayer(playerData, text, "AIRBOSS") + text = "negative, carrier is currently not recovering." + self:MessageToPlayer( playerData, text, "AIRBOSS" ) return end - self:MessageToPlayer(playerData, text, "AIRBOSS") + self:MessageToPlayer( playerData, text, "AIRBOSS" ) self:RecoveryStop() end @@ -113515,22 +120092,22 @@ end -- @param #AIRBOSS self -- @param #string _unitName Name fo the player unit. -- @param #number offset Recovery holding offset angle in degrees for Case II/III. -function AIRBOSS:_SkipperRecoveryOffset(_unitName, offset) +function AIRBOSS:_SkipperRecoveryOffset( _unitName, offset ) -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) -- Check if we have a unit which is a player. if _unit and _playername then - local playerData=self.players[_playername] --#AIRBOSS.PlayerData + local playerData = self.players[_playername] -- #AIRBOSS.PlayerData if playerData then -- Inform player. - local text=string.format("roger, relative CASE II/III Marshal radial set to %d°.", offset) - self:MessageToPlayer(playerData, text, "AIRBOSS") + local text = string.format( "roger, relative CASE II/III Marshal radial set to %d°.", offset ) + self:MessageToPlayer( playerData, text, "AIRBOSS" ) - self.skipperOffset=offset + self.skipperOffset = offset end end end @@ -113539,22 +120116,22 @@ end -- @param #AIRBOSS self -- @param #string _unitName Name fo the player unit. -- @param #number time Recovery time in minutes. -function AIRBOSS:_SkipperRecoveryTime(_unitName, time) +function AIRBOSS:_SkipperRecoveryTime( _unitName, time ) -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) -- Check if we have a unit which is a player. if _unit and _playername then - local playerData=self.players[_playername] --#AIRBOSS.PlayerData + local playerData = self.players[_playername] -- #AIRBOSS.PlayerData if playerData then -- Inform player. - local text=string.format("roger, manual recovery time set to %d min.", time) - self:MessageToPlayer(playerData, text, "AIRBOSS") + local text = string.format( "roger, manual recovery time set to %d min.", time ) + self:MessageToPlayer( playerData, text, "AIRBOSS" ) - self.skipperTime=time + self.skipperTime = time end end @@ -113564,22 +120141,22 @@ end -- @param #AIRBOSS self -- @param #string _unitName Name fo the player unit. -- @param #number speed Recovery speed in knots. -function AIRBOSS:_SkipperRecoverySpeed(_unitName, speed) +function AIRBOSS:_SkipperRecoverySpeed( _unitName, speed ) -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) -- Check if we have a unit which is a player. if _unit and _playername then - local playerData=self.players[_playername] --#AIRBOSS.PlayerData + local playerData = self.players[_playername] -- #AIRBOSS.PlayerData if playerData then -- Inform player. - local text=string.format("roger, wind on deck set to %d knots.", speed) - self:MessageToPlayer(playerData, text, "AIRBOSS") + local text = string.format( "roger, wind on deck set to %d knots.", speed ) + self:MessageToPlayer( playerData, text, "AIRBOSS" ) - self.skipperSpeed=speed + self.skipperSpeed = speed end end end @@ -113587,28 +120164,27 @@ end --- Skipper set recovery speed. -- @param #AIRBOSS self -- @param #string _unitName Name fo the player unit. -function AIRBOSS:_SkipperRecoveryUturn(_unitName) +function AIRBOSS:_SkipperRecoveryUturn( _unitName ) -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) -- Check if we have a unit which is a player. if _unit and _playername then - local playerData=self.players[_playername] --#AIRBOSS.PlayerData + local playerData = self.players[_playername] -- #AIRBOSS.PlayerData if playerData then - self.skipperUturn=not self.skipperUturn + self.skipperUturn = not self.skipperUturn -- Inform player. - local text=string.format("roger, U-turn is now %s.", tostring(self.skipperUturn)) - self:MessageToPlayer(playerData, text, "AIRBOSS") + local text = string.format( "roger, U-turn is now %s.", tostring( self.skipperUturn ) ) + self:MessageToPlayer( playerData, text, "AIRBOSS" ) end end end - ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ROOT MENU ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -113616,33 +120192,33 @@ end --- Reset player status. Player is removed from all queues and its status is set to undefined. -- @param #AIRBOSS self -- @param #string _unitName Name fo the player unit. -function AIRBOSS:_ResetPlayerStatus(_unitName) - self:F(_unitName) +function AIRBOSS:_ResetPlayerStatus( _unitName ) + self:F( _unitName ) -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) -- Check if we have a unit which is a player. if _unit and _playername then - local playerData=self.players[_playername] --#AIRBOSS.PlayerData + local playerData = self.players[_playername] -- #AIRBOSS.PlayerData if playerData then -- Inform player. - local text="roger, status reset executed! You have been removed from all queues." - self:MessageToPlayer(playerData, text, "AIRBOSS") + local text = "roger, status reset executed! You have been removed from all queues." + self:MessageToPlayer( playerData, text, "AIRBOSS" ) -- Remove flight from queues. Collapse marshal stack if necessary. -- Section members are removed from the Spinning queue. If flight is member, he is removed from the section. - self:_RemoveFlight(playerData) + self:_RemoveFlight( playerData ) -- Stop pending debrief scheduler. if playerData.debriefschedulerID and self.Scheduler then - self.Scheduler:Stop(playerData.debriefschedulerID) + self.Scheduler:Stop( playerData.debriefschedulerID ) end -- Initialize player data. - self:_InitPlayer(playerData) + self:_InitPlayer( playerData ) end end @@ -113651,68 +120227,73 @@ end --- Request marshal. -- @param #AIRBOSS self -- @param #string _unitName Name fo the player unit. -function AIRBOSS:_RequestMarshal(_unitName) - self:F(_unitName) +function AIRBOSS:_RequestMarshal( _unitName ) + self:F( _unitName ) -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) -- Check if we have a unit which is a player. if _unit and _playername then - local playerData=self.players[_playername] --#AIRBOSS.PlayerData + local playerData = self.players[_playername] -- #AIRBOSS.PlayerData if playerData then + -- Voice over of inbound call (regardless of airboss rejecting it or not) + if self.xtVoiceOvers then + self:_MarshallInboundCall(_unit, playerData.onboard) + end + -- Check if player is in CCA - local inCCA=playerData.unit:IsInZone(self.zoneCCA) + local inCCA = playerData.unit:IsInZone( self.zoneCCA ) if inCCA then - if self:_InQueue(self.Qmarshal, playerData.group) then + if self:_InQueue( self.Qmarshal, playerData.group ) then -- Flight group is already in marhal queue. - local text=string.format("negative, you are already in the Marshal queue. New marshal request denied!") - self:MessageToPlayer(playerData, text, "MARSHAL") + local text = string.format( "negative, you are already in the Marshal queue. New marshal request denied!" ) + self:MessageToPlayer( playerData, text, "MARSHAL" ) - elseif self:_InQueue(self.Qpattern, playerData.group) then + elseif self:_InQueue( self.Qpattern, playerData.group ) then -- Flight group is already in pattern queue. - local text=string.format("negative, you are already in the Pattern queue. Marshal request denied!") - self:MessageToPlayer(playerData, text, "MARSHAL") + local text = string.format( "negative, you are already in the Pattern queue. Marshal request denied!" ) + self:MessageToPlayer( playerData, text, "MARSHAL" ) - elseif self:_InQueue(self.Qwaiting, playerData.group) then + elseif self:_InQueue( self.Qwaiting, playerData.group ) then -- Flight group is already in pattern queue. - local text=string.format("negative, you are in the Waiting queue with %d flights ahead of you. Marshal request denied!", #self.Qwaiting) - self:MessageToPlayer(playerData, text, "MARSHAL") + local text = string.format( "negative, you are in the Waiting queue with %d flights ahead of you. Marshal request denied!", #self.Qwaiting ) + self:MessageToPlayer( playerData, text, "MARSHAL" ) elseif not _unit:InAir() then -- Flight group is already in pattern queue. - local text=string.format("negative, you are not airborne. Marshal request denied!") - self:MessageToPlayer(playerData, text, "MARSHAL") + local text = string.format( "negative, you are not airborne. Marshal request denied!" ) + self:MessageToPlayer( playerData, text, "MARSHAL" ) - elseif playerData.name~=playerData.seclead then + elseif playerData.name ~= playerData.seclead then -- Flight group is already in pattern queue. - local text=string.format("negative, your section lead %s needs to request Marshal.", playerData.seclead) - self:MessageToPlayer(playerData, text, "MARSHAL") + local text = string.format( "negative, your section lead %s needs to request Marshal.", playerData.seclead ) + self:MessageToPlayer( playerData, text, "MARSHAL" ) else -- Get next free Marshal stack. - local freestack=self:_GetFreeStack(playerData.ai) + local freestack = self:_GetFreeStack( playerData.ai ) -- Check if stack is available. For Case I the number is limited. if freestack then -- Add flight to marshal stack. - self:_MarshalPlayer(playerData, freestack) + self:_MarshalPlayer( playerData, freestack ) else -- Add flight to waiting queue. - self:_WaitPlayer(playerData) + self:_WaitPlayer( playerData ) end @@ -113721,8 +120302,8 @@ function AIRBOSS:_RequestMarshal(_unitName) else -- Flight group is not in CCA yet. - local text=string.format("negative, you are not inside CCA. Marshal request denied!") - self:MessageToPlayer(playerData, text, "MARSHAL") + local text = string.format( "negative, you are not inside CCA. Marshal request denied!" ) + self:MessageToPlayer( playerData, text, "MARSHAL" ) end end @@ -113732,102 +120313,102 @@ end --- Request emergency landing. -- @param #AIRBOSS self -- @param #string _unitName Name fo the player unit. -function AIRBOSS:_RequestEmergency(_unitName) - self:F(_unitName) +function AIRBOSS:_RequestEmergency( _unitName ) + self:F( _unitName ) -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) -- Check if we have a unit which is a player. if _unit and _playername then - local playerData=self.players[_playername] --#AIRBOSS.PlayerData + local playerData = self.players[_playername] -- #AIRBOSS.PlayerData if playerData then - local text="" + local text = "" if not self.emergency then -- Mission designer did not allow emergency landing. - text="negative, no emergency landings on my carrier. We are currently busy. See how you get along!" + text = "negative, no emergency landings on my carrier. We are currently busy. See how you get along!" elseif not _unit:InAir() then -- Carrier zone. - local zone=self:_GetZoneCarrierBox() + local zone = self:_GetZoneCarrierBox() -- Check if player is on the carrier. - if playerData.unit:IsInZone(zone) then + if playerData.unit:IsInZone( zone ) then -- Bolter pattern. - text="roger, you are now technically in the bolter pattern. Your next step after takeoff is abeam!" + text = "roger, you are now technically in the bolter pattern. Your next step after takeoff is abeam!" -- Get flight lead. - local lead=self:_GetFlightLead(playerData) + local lead = self:_GetFlightLead( playerData ) -- Set set for lead. - self:_SetPlayerStep(lead, AIRBOSS.PatternStep.BOLTER) + self:_SetPlayerStep( lead, AIRBOSS.PatternStep.BOLTER ) -- Also set bolter pattern for all members. - for _,sec in pairs(lead.section) do - local sectionmember=sec --#AIRBOSS.PlayerData - self:_SetPlayerStep(sectionmember, AIRBOSS.PatternStep.BOLTER) + for _, sec in pairs( lead.section ) do + local sectionmember = sec -- #AIRBOSS.PlayerData + self:_SetPlayerStep( sectionmember, AIRBOSS.PatternStep.BOLTER ) end -- Remove flight from waiting queue just in case. - self:_RemoveFlightFromQueue(self.Qwaiting, lead) + self:_RemoveFlightFromQueue( self.Qwaiting, lead ) - if self:_InQueue(self.Qmarshal, lead.group) then + if self:_InQueue( self.Qmarshal, lead.group ) then -- Remove flight from Marshal queue and add to pattern. - self:_RemoveFlightFromMarshalQueue(lead) + self:_RemoveFlightFromMarshalQueue( lead ) else -- Add flight to pattern if he was not. - if not self:_InQueue(self.Qpattern, lead.group) then - self:_AddFlightToPatternQueue(lead) + if not self:_InQueue( self.Qpattern, lead.group ) then + self:_AddFlightToPatternQueue( lead ) end end else -- Flight group is not in air. - text=string.format("negative, you are not airborne. Request denied!") + text = string.format( "negative, you are not airborne. Request denied!" ) end else -- Cleared. - text="affirmative, you can bypass the pattern and are cleared for final approach!" + text = "affirmative, you can bypass the pattern and are cleared for final approach!" -- Now, if player is in the marshal or waiting queue he will be removed. But the new leader should stay in or not. - local lead=self:_GetFlightLead(playerData) + local lead = self:_GetFlightLead( playerData ) -- Set set for lead. - self:_SetPlayerStep(lead, AIRBOSS.PatternStep.EMERGENCY) + self:_SetPlayerStep( lead, AIRBOSS.PatternStep.EMERGENCY ) -- Also set emergency landing for all members. - for _,sec in pairs(lead.section) do - local sectionmember=sec --#AIRBOSS.PlayerData - self:_SetPlayerStep(sectionmember, AIRBOSS.PatternStep.EMERGENCY) + for _, sec in pairs( lead.section ) do + local sectionmember = sec -- #AIRBOSS.PlayerData + self:_SetPlayerStep( sectionmember, AIRBOSS.PatternStep.EMERGENCY ) -- Remove flight from spinning queue just in case (everone can spin on his own). - self:_RemoveFlightFromQueue(self.Qspinning, sectionmember) + self:_RemoveFlightFromQueue( self.Qspinning, sectionmember ) end -- Remove flight from waiting queue just in case. - self:_RemoveFlightFromQueue(self.Qwaiting, lead) + self:_RemoveFlightFromQueue( self.Qwaiting, lead ) - if self:_InQueue(self.Qmarshal, lead.group) then + if self:_InQueue( self.Qmarshal, lead.group ) then -- Remove flight from Marshal queue and add to pattern. - self:_RemoveFlightFromMarshalQueue(lead) + self:_RemoveFlightFromMarshalQueue( lead ) else -- Add flight to pattern if he was not. - if not self:_InQueue(self.Qpattern, lead.group) then - self:_AddFlightToPatternQueue(lead) + if not self:_InQueue( self.Qpattern, lead.group ) then + self:_AddFlightToPatternQueue( lead ) end end end -- Send message. - self:MessageToPlayer(playerData, text, "AIRBOSS") + self:MessageToPlayer( playerData, text, "AIRBOSS" ) end @@ -113837,60 +120418,60 @@ end --- Request spinning. -- @param #AIRBOSS self -- @param #string _unitName Name fo the player unit. -function AIRBOSS:_RequestSpinning(_unitName) - self:F(_unitName) +function AIRBOSS:_RequestSpinning( _unitName ) + self:F( _unitName ) -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) -- Check if we have a unit which is a player. if _unit and _playername then - local playerData=self.players[_playername] --#AIRBOSS.PlayerData + local playerData = self.players[_playername] -- #AIRBOSS.PlayerData if playerData then - local text="" - if not self:_InQueue(self.Qpattern, playerData.group) then + local text = "" + if not self:_InQueue( self.Qpattern, playerData.group ) then -- Player not in pattern queue. - text="negative, you have to be in the pattern to spin it!" + text = "negative, you have to be in the pattern to spin it!" - elseif playerData.step==AIRBOSS.PatternStep.SPINNING then + elseif playerData.step == AIRBOSS.PatternStep.SPINNING then -- Player is already spinning. - text="negative, you are already spinning." + text = "negative, you are already spinning." - -- Check if player is in the right step. - elseif not (playerData.step==AIRBOSS.PatternStep.BREAKENTRY or - playerData.step==AIRBOSS.PatternStep.EARLYBREAK or - playerData.step==AIRBOSS.PatternStep.LATEBREAK) then + -- Check if player is in the right step. + elseif not (playerData.step == AIRBOSS.PatternStep.BREAKENTRY or + playerData.step == AIRBOSS.PatternStep.EARLYBREAK or + playerData.step == AIRBOSS.PatternStep.LATEBREAK) then -- Player is not in the right step. - text="negative, you have to be in the right step to spin it!" + text = "negative, you have to be in the right step to spin it!" else -- Set player step. - self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.SPINNING) + self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.SPINNING ) -- Add player to spinning queue. - table.insert(self.Qspinning, playerData) + table.insert( self.Qspinning, playerData ) -- 405, Spin it! Click. - local call=self:_NewRadioCall(self.LSOCall.SPINIT, "AIRBOSS", "Spin it!", self.Tmessage, playerData.onboard) - self:RadioTransmission(self.LSORadio, call, nil, nil, nil, true) + local call = self:_NewRadioCall( self.LSOCall.SPINIT, "AIRBOSS", "Spin it!", self.Tmessage, playerData.onboard ) + self:RadioTransmission( self.LSORadio, call, nil, nil, nil, true ) -- Some advice. - if playerData.difficulty==AIRBOSS.Difficulty.EASY then - local text="Climb to 1200 feet and proceed to the initial again." - self:MessageToPlayer(playerData, text, "INSTRUCTOR", "") + if playerData.difficulty == AIRBOSS.Difficulty.EASY then + local text = "Climb to 1200 feet and proceed to the initial again." + self:MessageToPlayer( playerData, text, "INSTRUCTOR", "" ) end return end -- Send message. - self:MessageToPlayer(playerData, text, "AIRBOSS") + self:MessageToPlayer( playerData, text, "AIRBOSS" ) end end @@ -113899,69 +120480,74 @@ end --- Request to commence landing approach. -- @param #AIRBOSS self -- @param #string _unitName Name fo the player unit. -function AIRBOSS:_RequestCommence(_unitName) - self:F(_unitName) +function AIRBOSS:_RequestCommence( _unitName ) + self:F( _unitName ) -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) -- Check if we have a unit which is a player. if _unit and _playername then - local playerData=self.players[_playername] --#AIRBOSS.PlayerData + local playerData = self.players[_playername] -- #AIRBOSS.PlayerData if playerData then - + + -- Voice over of Commencing call (regardless of Airboss will rejected or not) + if self.xtVoiceOvers then + self:_CommencingCall(_unit, playerData.onboard) + end + -- Check if unit is in CCA. - local text="" - local cleared=false - if _unit:IsInZone(self.zoneCCA) then + local text = "" + local cleared = false + if _unit:IsInZone( self.zoneCCA ) then -- Get stack value. - local stack=playerData.flag + local stack = playerData.flag -- Number of airborne aircraft currently in pattern. - local _,npattern=self:_GetQueueInfo(self.Qpattern) + local _, npattern = self:_GetQueueInfo( self.Qpattern ) -- TODO: Check distance to initial or platform. Only allow commence if < max distance. Otherwise say bearing. - if self:_InQueue(self.Qpattern, playerData.group) then + if self:_InQueue( self.Qpattern, playerData.group ) then -- Flight group is already in pattern queue. - text=string.format("negative, %s, you are already in the Pattern queue.", playerData.name) + text = string.format( "negative, %s, you are already in the Pattern queue.", playerData.name ) elseif not _unit:InAir() then -- Flight group is already in pattern queue. - text=string.format("negative, %s, you are not airborne.", playerData.name) + text = string.format( "negative, %s, you are not airborne.", playerData.name ) - elseif playerData.seclead~=playerData.name then + elseif playerData.seclead ~= playerData.name then -- Flight group is already in pattern queue. - text=string.format("negative, %s, your section leader %s has to request commence!", playerData.name, playerData.seclead) + text = string.format( "negative, %s, your section leader %s has to request commence!", playerData.name, playerData.seclead ) - elseif stack>1 then + elseif stack > 1 then -- We are in a higher stack. - text=string.format("negative, %s, it's not your turn yet! You are in stack no. %s.", playerData.name, stack) + text = string.format( "negative, %s, it's not your turn yet! You are in stack no. %s.", playerData.name, stack ) - elseif npattern>=self.Nmaxpattern then + elseif npattern >= self.Nmaxpattern then -- Patern is full! - text=string.format("negative ghostrider, pattern is full!\nThere are %d aircraft currently in the pattern.", npattern) + text = string.format( "negative ghostrider, pattern is full!\nThere are %d aircraft currently in the pattern.", npattern ) - elseif self:IsRecovering()==false and not self.airbossnice then + elseif self:IsRecovering() == false and not self.airbossnice then -- Carrier is not recovering right now. if self.recoverywindow then - local clock=UTILS.SecondsToClock(self.recoverywindow.START) - text=string.format("negative, carrier is currently not recovery. Next window will open at %s.", clock) + local clock = UTILS.SecondsToClock( self.recoverywindow.START ) + text = string.format( "negative, carrier is currently not recovery. Next window will open at %s.", clock ) else - text=string.format("negative, carrier is not recovering. No future windows planned.") + text = string.format( "negative, carrier is not recovering. No future windows planned." ) end - elseif not self:_InQueue(self.Qmarshal, playerData.group) and not self.airbossnice then + elseif not self:_InQueue( self.Qmarshal, playerData.group ) and not self.airbossnice then - text="negative, you have to request Marshal before you can commence." + text = "negative, you have to request Marshal before you can commence." else @@ -113969,60 +120555,60 @@ function AIRBOSS:_RequestCommence(_unitName) -- Positive Response -- ----------------------- - text=text.."roger." + text = text .. "roger." -- Carrier is not recovering but Airboss has a good day. if not self:IsRecovering() then - text=text.." Carrier is not recovering currently! However, you are cleared anyway as I have a nice day." + text = text .. " Carrier is not recovering currently! However, you are cleared anyway as I have a nice day." end -- If player is not in the Marshal queue set player case to current case. - if not self:_InQueue(self.Qmarshal, playerData.group) then + if not self:_InQueue( self.Qmarshal, playerData.group ) then -- Set current case. - playerData.case=self.case + playerData.case = self.case -- Hint about TACAN bearing. - if self.TACANon and playerData.difficulty~=AIRBOSS.Difficulty.HARD then + if self.TACANon and playerData.difficulty ~= AIRBOSS.Difficulty.HARD then -- Get inverse magnetic radial potential offset. - local radial=self:GetRadial(playerData.case, true, true, true) - if playerData.case==1 then + local radial = self:GetRadial( playerData.case, true, true, true ) + if playerData.case == 1 then -- For case 1 we want the BRC but above routine return FB. - radial=self:GetBRC() + radial = self:GetBRC() end - text=text..string.format("\nSelect TACAN %03d°, Channel %d%s (%s).\n", radial, self.TACANchannel,self.TACANmode, self.TACANmorse) + text = text .. string.format( "\nSelect TACAN %03d°, Channel %d%s (%s).\n", radial, self.TACANchannel, self.TACANmode, self.TACANmorse ) end -- TODO: Inform section members. -- Set case of section members as well. Not sure if necessary any more since it is set as soon as the recovery case is changed. - for _,flight in pairs(playerData.section) do - flight.case=playerData.case + for _, flight in pairs( playerData.section ) do + flight.case = playerData.case end -- Add player to pattern queue. Usually this is done when the stack is collapsed but this player is not in the Marshal queue. - self:_AddFlightToPatternQueue(playerData) + self:_AddFlightToPatternQueue( playerData ) end -- Clear player for commence. - cleared=true + cleared = true end else -- This flight is not yet registered! - text=string.format("negative, %s, you are not inside the CCA!", playerData.name) + text = string.format( "negative, %s, you are not inside the CCA!", playerData.name ) end -- Debug - self:T(self.lid..text) + self:T( self.lid .. text ) -- Send message. - self:MessageToPlayer(playerData, text, "MARSHAL") + self:MessageToPlayer( playerData, text, "MARSHAL" ) -- Check if player was cleard. Need to do this after the message above is displayed. if cleared then -- Call commence routine. No zone check. NOTE: Commencing will set step for all section members as well. - self:_Commencing(playerData, false) + self:_Commencing( playerData, false ) end end end @@ -114031,14 +120617,14 @@ end --- Player requests refueling. -- @param #AIRBOSS self -- @param #string _unitName Name of the player unit. -function AIRBOSS:_RequestRefueling(_unitName) +function AIRBOSS:_RequestRefueling( _unitName ) -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) -- Check if we have a unit which is a player. if _unit and _playername then - local playerData=self.players[_playername] --#AIRBOSS.PlayerData + local playerData = self.players[_playername] -- #AIRBOSS.PlayerData if playerData then @@ -114047,72 +120633,71 @@ function AIRBOSS:_RequestRefueling(_unitName) if self.tanker then -- Check if player is in CCA. - if _unit:IsInZone(self.zoneCCA) then + if _unit:IsInZone( self.zoneCCA ) then -- Check if tanker is running or refueling or returning. if self.tanker:IsRunning() or self.tanker:IsRefueling() then -- Get alt of tanker in angels. - --local angels=UTILS.Round(UTILS.MetersToFeet(self.tanker.altitude)/1000, 0) - local angels=self:_GetAngels(self.tanker.altitude) + -- local angels=UTILS.Round(UTILS.MetersToFeet(self.tanker.altitude)/1000, 0) + local angels = self:_GetAngels( self.tanker.altitude ) -- Tanker is up and running. - text=string.format("affirmative, proceed to tanker at angels %d.", angels) + text = string.format( "affirmative, proceed to tanker at angels %d.", angels ) -- State TACAN channel of tanker if defined. if self.tanker.TACANon then - text=text..string.format("\nTanker TACAN channel %d%s (%s).", self.tanker.TACANchannel, self.tanker.TACANmode, self.tanker.TACANmorse) - text=text..string.format("\nRadio frequency %.3f MHz AM.", self.tanker.RadioFreq) + text = text .. string.format( "\nTanker TACAN channel %d%s (%s).", self.tanker.TACANchannel, self.tanker.TACANmode, self.tanker.TACANmorse ) + text = text .. string.format( "\nRadio frequency %.3f MHz AM.", self.tanker.RadioFreq ) end -- Tanker is currently refueling. Inform player. if self.tanker:IsRefueling() then - text=text.."\nTanker is currently refueling. You might have to queue up." + text = text .. "\nTanker is currently refueling. You might have to queue up." end -- Collapse marshal stack if player is in queue. - self:_RemoveFlightFromMarshalQueue(playerData, true) + self:_RemoveFlightFromMarshalQueue( playerData, true ) -- Set step to refueling. - self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.REFUELING) + self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.REFUELING ) -- Inform section and set step. - for _,sec in pairs(playerData.section) do - local sectext="follow your section leader to the tanker." - self:MessageToPlayer(sec, sectext, "MARSHAL") - self:_SetPlayerStep(sec, AIRBOSS.PatternStep.REFUELING) + for _, sec in pairs( playerData.section ) do + local sectext = "follow your section leader to the tanker." + self:MessageToPlayer( sec, sectext, "MARSHAL" ) + self:_SetPlayerStep( sec, AIRBOSS.PatternStep.REFUELING ) end elseif self.tanker:IsReturning() then -- Tanker is RTB. - text="negative, tanker is RTB. Request denied!\nWait for the tanker to be back on station if you can." + text = "negative, tanker is RTB. Request denied!\nWait for the tanker to be back on station if you can." end else - text="negative, you are not inside the CCA yet." + text = "negative, you are not inside the CCA yet." end else - text="negative, no refueling tanker available." + text = "negative, no refueling tanker available." end -- Send message. - self:MessageToPlayer(playerData, text, "MARSHAL") + self:MessageToPlayer( playerData, text, "MARSHAL" ) end end end - --- Remove a member from the player's section. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player -- @param #AIRBOSS.PlayerData sectionmember The section member to be removed. -- @return #boolean If true, flight was a section member and could be removed. False otherwise. -function AIRBOSS:_RemoveSectionMember(playerData, sectionmember) +function AIRBOSS:_RemoveSectionMember( playerData, sectionmember ) -- Loop over all flights in player's section - for i,_flight in pairs(playerData.section) do - local flight=_flight --#AIRBOSS.PlayerData - if flight.name==sectionmember.name then - table.remove(playerData.section, i) + for i, _flight in pairs( playerData.section ) do + local flight = _flight -- #AIRBOSS.PlayerData + if flight.name == sectionmember.name then + table.remove( playerData.section, i ) return true end end @@ -114122,148 +120707,150 @@ end --- Set all flights within 100 meters to be part of my section. -- @param #AIRBOSS self -- @param #string _unitName Name of the player unit. -function AIRBOSS:_SetSection(_unitName) +function AIRBOSS:_SetSection( _unitName ) -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) -- Check if we have a unit which is a player. if _unit and _playername then - local playerData=self.players[_playername] --#AIRBOSS.PlayerData + local playerData = self.players[_playername] -- #AIRBOSS.PlayerData if playerData then -- Coordinate of flight lead. - local mycoord=_unit:GetCoordinate() + local mycoord = _unit:GetCoordinate() -- Max distance up to which section members are allowed. - local dmax=100 + local dmax = 100 -- Check if player is in Marshal or pattern queue already. local text - if self.NmaxSection==0 then - text=string.format("negative, setting sections is disabled in this mission. You stay alone.") - elseif self:_InQueue(self.Qmarshal,playerData.group) then - text=string.format("negative, you are already in the Marshal queue. Setting section not possible any more!") - elseif self:_InQueue(self.Qpattern, playerData.group) then - text=string.format("negative, you are already in the Pattern queue. Setting section not possible any more!") + if self.NmaxSection == 0 then + text = string.format( "negative, setting sections is disabled in this mission. You stay alone." ) + elseif self:_InQueue( self.Qmarshal, playerData.group ) then + text = string.format( "negative, you are already in the Marshal queue. Setting section not possible any more!" ) + elseif self:_InQueue( self.Qpattern, playerData.group ) then + text = string.format( "negative, you are already in the Pattern queue. Setting section not possible any more!" ) else -- Check if player is member of another section already. If so, remove him from his current section. - if playerData.seclead~=playerData.name then - local lead=self.players[playerData.seclead] --#AIRBOSS.PlayerData + if playerData.seclead ~= playerData.name then + local lead = self.players[playerData.seclead] -- #AIRBOSS.PlayerData if lead then -- Remove player from his old section lead. - local removed=self:_RemoveSectionMember(lead, playerData) + local removed = self:_RemoveSectionMember( lead, playerData ) if removed then - self:MessageToPlayer(lead, string.format("Flight %s has been removed from your section.", playerData.name), "AIRBOSS", "", 5) - self:MessageToPlayer(playerData, string.format("You have been removed from %s's section.", lead.name), "AIRBOSS", "", 5) + self:MessageToPlayer( lead, string.format( "Flight %s has been removed from your section.", playerData.name ), "AIRBOSS", "", 5 ) + self:MessageToPlayer( playerData, string.format( "You have been removed from %s's section.", lead.name ), "AIRBOSS", "", 5 ) end end end -- Potential section members. - local section={} + local section = {} -- Loop over all registered flights. - for _,_flight in pairs(self.flights) do - local flight=_flight --#AIRBOSS.FlightGroup + for _, _flight in pairs( self.flights ) do + local flight = _flight -- #AIRBOSS.FlightGroup -- Only human flight groups excluding myself. Also only flights that dont have a section itself (would get messy) or are part of another section (no double membership). - if flight.ai==false and flight.groupname~=playerData.groupname and #flight.section==0 and flight.seclead==flight.name then + if flight.ai == false and flight.groupname ~= playerData.groupname and #flight.section == 0 and flight.seclead == flight.name then -- Distance (3D) to other flight group. - local distance=flight.group:GetCoordinate():Get3DDistance(mycoord) + local distance = flight.group:GetCoordinate():Get3DDistance( mycoord ) -- Check distance. - if distance remove it. if not gotit then - self:MessageToPlayer(flight, string.format("you were removed from %s's section and are on your own now.", playerData.name), "AIRBOSS", "", 5) - flight.seclead=flight.name - self:_RemoveSectionMember(playerData, flight) + self:MessageToPlayer( flight, string.format( "you were removed from %s's section and are on your own now.", playerData.name ), "AIRBOSS", "", 5 ) + flight.seclead = flight.name + self:_RemoveSectionMember( playerData, flight ) end end -- Remove all flights that are currently in the player's section already from scanned potential new section members. - for i,_new in pairs(section) do - local newflight=_new.flight --#AIRBOSS.PlayerData - for _,_flight in pairs(playerData.section) do - local currentflight=_flight --#AIRBOSS.PlayerData - if newflight.name==currentflight.name then - table.remove(section, i) + for i, _new in pairs( section ) do + local newflight = _new.flight -- #AIRBOSS.PlayerData + for _, _flight in pairs( playerData.section ) do + local currentflight = _flight -- #AIRBOSS.PlayerData + if newflight.name == currentflight.name then + table.remove( section, i ) end end end -- Init section table. Should not be necessary as all members are removed anyhow above. - --playerData.section={} + -- playerData.section={} -- Output text. - text=string.format("Registered flight section:") - text=text..string.format("\n- %s (lead)", playerData.seclead) + text = string.format( "Registered flight section:" ) + text = text .. string.format( "\n- %s (lead)", playerData.seclead ) -- Old members that stay (if any). - for _,_flight in pairs(playerData.section) do - local flight=_flight --#AIRBOSS.PlayerData - text=text..string.format("\n- %s", flight.name) + for _, _flight in pairs( playerData.section ) do + local flight = _flight -- #AIRBOSS.PlayerData + text = text .. string.format( "\n- %s", flight.name ) end -- New members (if any). - for i=1,math.min(self.NmaxSection-#playerData.section, #section) do - local flight=section[i].flight --#AIRBOSS.PlayerData + for i = 1, math.min( self.NmaxSection - #playerData.section, #section ) do + local flight = section[i].flight -- #AIRBOSS.PlayerData -- New flight members. - text=text..string.format("\n- %s", flight.name) + text = text .. string.format( "\n- %s", flight.name ) -- Set section lead of player flight. - flight.seclead=playerData.name + flight.seclead = playerData.name -- Set case of f - flight.case=playerData.case + flight.case = playerData.case -- Inform player that he is now part of a section. - self:MessageToPlayer(flight, string.format("your section lead is now %s.", playerData.name), "AIRBOSS") + self:MessageToPlayer( flight, string.format( "your section lead is now %s.", playerData.name ), "AIRBOSS" ) -- Add flight to section table. - table.insert(playerData.section, flight) + table.insert( playerData.section, flight ) end -- Section is empty. - if #playerData.section==0 then - text=text..string.format("\n- No other human flights found within radius of %.1f meters!", dmax) + if #playerData.section == 0 then + text = text .. string.format( "\n- No other human flights found within radius of %.1f meters!", dmax ) end end -- Message to section lead. - self:MessageToPlayer(playerData, text, "MARSHAL") + self:MessageToPlayer( playerData, text, "MARSHAL" ) end end end @@ -114275,33 +120862,33 @@ end --- Display top 10 player scores. -- @param #AIRBOSS self -- @param #string _unitName Name fo the player unit. -function AIRBOSS:_DisplayScoreBoard(_unitName) - self:F(_unitName) +function AIRBOSS:_DisplayScoreBoard( _unitName ) + self:F( _unitName ) -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) -- Check if we have a unit which is a player. if _unit and _playername then -- Results table. - local _playerResults={} + local _playerResults = {} -- Calculate average points for all players. - for playerName,playerGrades in pairs(self.playerscores) do + for playerName, playerGrades in pairs( self.playerscores ) do if playerGrades then -- Loop over all grades - local Paverage=0 - local n=0 - for _,_grade in pairs(playerGrades) do - local grade=_grade --#AIRBOSS.LSOgrade + local Paverage = 0 + local n = 0 + for _, _grade in pairs( playerGrades ) do + local grade = _grade -- #AIRBOSS.LSOgrade -- Add up only final scores for the average. - if grade.finalscore then --grade.points>=0 then - Paverage=Paverage+grade.finalscore - n=n+1 + if grade.finalscore then -- grade.points>=0 then + Paverage = Paverage + grade.finalscore + n = n + 1 else -- Case when the player just leaves after an unfinished pass, e.g bolter, without landing. -- But this should now be solved by deleteing all unfinished results. @@ -114309,50 +120896,52 @@ function AIRBOSS:_DisplayScoreBoard(_unitName) end -- We dont want to devide by zero. - if n>0 then - _playerResults[playerName]=Paverage/n + if n > 0 then + _playerResults[playerName] = Paverage / n end end end -- Message text. - local text = string.format("Greenie Board (top ten):") - local i=1 - for _playerName,_points in UTILS.spairs(_playerResults, function(t, a, b) return t[b] < t[a] end) do + local text = string.format( "Greenie Board (top ten):" ) + local i = 1 + for _playerName, _points in UTILS.spairs( _playerResults, function( t, a, b ) + return t[b] < t[a] + end ) do -- Text. - text=text..string.format("\n[%d] %s %.1f||", i,_playerName, _points) + text = text .. string.format( "\n[%d] %s %.1f||", i, _playerName, _points ) -- All player grades. - local playerGrades=self.playerscores[_playerName] + local playerGrades = self.playerscores[_playerName] -- Add grades of passes. We use the actual grade of each pass here and not the average after player has landed. - for _,_grade in pairs(playerGrades) do - local grade=_grade --#AIRBOSS.LSOgrade + for _, _grade in pairs( playerGrades ) do + local grade = _grade -- #AIRBOSS.LSOgrade if grade.finalscore then - text=text..string.format("%.1f|", grade.points) - elseif grade.points>=0 then -- Only points >=0 as foul deck gives -1. - text=text..string.format("(%.1f)", grade.points) + text = text .. string.format( "%.1f|", grade.points ) + elseif grade.points >= 0 then -- Only points >=0 as foul deck gives -1. + text = text .. string.format( "(%.1f)", grade.points ) end end -- Display only the top ten. - i=i+1 - if i>10 then + i = i + 1 + if i > 10 then break end end -- If no results yet. - if i==1 then - text=text.."\nNo results yet." + if i == 1 then + text = text .. "\nNo results yet." end -- Send message. - local playerData=self.players[_playername] --#AIRBOSS.PlayerData + local playerData = self.players[_playername] -- #AIRBOSS.PlayerData if playerData.client then - MESSAGE:New(text, 30, nil, true):ToClient(playerData.client) + MESSAGE:New( text, 30, nil, true ):ToClient( playerData.client ) end end @@ -114361,74 +120950,73 @@ end --- Display top 10 player scores. -- @param #AIRBOSS self -- @param #string _unitName Name fo the player unit. -function AIRBOSS:_DisplayPlayerGrades(_unitName) - self:F(_unitName) +function AIRBOSS:_DisplayPlayerGrades( _unitName ) + self:F( _unitName ) -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) -- Check if we have a unit which is a player. if _unit and _playername then - local playerData=self.players[_playername] --#AIRBOSS.PlayerData + local playerData = self.players[_playername] -- #AIRBOSS.PlayerData if playerData then -- Grades of player: - local text=string.format("Your last 10 grades, %s:", _playername) + local text = string.format( "Your last 10 grades, %s:", _playername ) -- All player grades. - local playerGrades=self.playerscores[_playername] or {} + local playerGrades = self.playerscores[_playername] or {} - local p=0 -- Average points. - local n=0 -- Number of final passes. - local m=0 -- Number of total passes. - --for i,_grade in pairs(playerGrades) do - for i=#playerGrades,1,-1 do - --local grade=_grade --#AIRBOSS.LSOgrade - local grade=playerGrades[i] --#AIRBOSS.LSOgrade + local p = 0 -- Average points. + local n = 0 -- Number of final passes. + local m = 0 -- Number of total passes. + -- for i,_grade in pairs(playerGrades) do + for i = #playerGrades, 1, -1 do + -- local grade=_grade --#AIRBOSS.LSOgrade + local grade = playerGrades[i] -- #AIRBOSS.LSOgrade -- Check if points >=0. For foul deck WO we give -1 and pass is not counted. - if grade.points>=0 then + if grade.points >= 0 then -- Show final points or points of pass. - local points=grade.finalscore or grade.points + local points = grade.finalscore or grade.points -- Display max 10 results. - if m<10 then - text=text..string.format("\n[%d] %s %.1f PT - %s", i, grade.grade, points, grade.details) + if m < 10 then + text = text .. string.format( "\n[%d] %s %.1f PT - %s", i, grade.grade, points, grade.details ) -- Wire trapped if any. - if grade.wire and grade.wire<=4 then - text=text..string.format(" %d-wire", grade.wire) + if grade.wire and grade.wire <= 4 then + text = text .. string.format( " %d-wire", grade.wire ) end -- Time in the groove if any. - if grade.Tgroove and grade.Tgroove<=360 then - text=text..string.format(" Tgroove=%.1f s", grade.Tgroove) + if grade.Tgroove and grade.Tgroove <= 360 then + text = text .. string.format( " Tgroove=%.1f s", grade.Tgroove ) end end -- Add up final points. if grade.finalscore then - p=p+grade.finalscore - n=n+1 + p = p + grade.finalscore + n = n + 1 end -- Total passes - m=m+1 + m = m + 1 end end - - if n>0 then - text=text..string.format("\nAverage points = %.1f", p/n) + if n > 0 then + text = text .. string.format( "\nAverage points = %.1f", p / n ) else - text=text..string.format("\nNo data available.") + text = text .. string.format( "\nNo data available." ) end -- Send message. if playerData.client then - MESSAGE:New(text, 30, nil, true):ToClient(playerData.client) + MESSAGE:New( text, 30, nil, true ):ToClient( playerData.client ) end end end @@ -114437,36 +121025,36 @@ end --- Display last debriefing. -- @param #AIRBOSS self -- @param #string _unitName Name fo the player unit. -function AIRBOSS:_DisplayDebriefing(_unitName) - self:F(_unitName) +function AIRBOSS:_DisplayDebriefing( _unitName ) + self:F( _unitName ) -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) -- Check if we have a unit which is a player. if _unit and _playername then - local playerData=self.players[_playername] --#AIRBOSS.PlayerData + local playerData = self.players[_playername] -- #AIRBOSS.PlayerData if playerData then -- Debriefing text. - local text=string.format("Debriefing:") + local text = string.format( "Debriefing:" ) -- Check if data is present. - if #playerData.lastdebrief>0 then - text=text..string.format("\n================================\n") - for _,_data in pairs(playerData.lastdebrief) do - local step=_data.step - local comment=_data.hint - text=text..string.format("* %s:",step) - text=text..string.format("%s\n", comment) + if #playerData.lastdebrief > 0 then + text = text .. string.format( "\n================================\n" ) + for _, _data in pairs( playerData.lastdebrief ) do + local step = _data.step + local comment = _data.hint + text = text .. string.format( "* %s:", step ) + text = text .. string.format( "%s\n", comment ) end else - text=text.." Nothing to show yet." + text = text .. " Nothing to show yet." end -- Send debrief message to player - self:MessageToPlayer(playerData, text, nil , "", 30, true) + self:MessageToPlayer( playerData, text, nil, "", 30, true ) end end @@ -114480,134 +121068,133 @@ end -- @param #AIRBOSS self -- @param #string _unitname Name of the player unit. -- @param #string qname Name of the queue. -function AIRBOSS:_DisplayQueue(_unitname, qname) +function AIRBOSS:_DisplayQueue( _unitname, qname ) -- Get player unit and player name. - local unit, playername = self:_GetPlayerUnitAndName(_unitname) + local unit, playername = self:_GetPlayerUnitAndName( _unitname ) -- Check if we have a player. if unit and playername then -- Player data. - local playerData=self.players[playername] --#AIRBOSS.PlayerData + local playerData = self.players[playername] -- #AIRBOSS.PlayerData if playerData then -- Queue to display. - local queue=nil - if qname=="Marshal" then - queue=self.Qmarshal - elseif qname=="Pattern" then - queue=self.Qpattern - elseif qname=="Waiting" then - queue=self.Qwaiting + local queue = nil + if qname == "Marshal" then + queue = self.Qmarshal + elseif qname == "Pattern" then + queue = self.Qpattern + elseif qname == "Waiting" then + queue = self.Qwaiting end -- Number of group and units in queue - local Nqueue,nqueue=self:_GetQueueInfo(queue, playerData.case) + local Nqueue, nqueue = self:_GetQueueInfo( queue, playerData.case ) - local text=string.format("%s Queue:", qname) - if #queue==0 then - text=text.." empty" + local text = string.format( "%s Queue:", qname ) + if #queue == 0 then + text = text .. " empty" else - local N=0 - if qname=="Marshal" then - for i,_flight in pairs(queue) do - local flight=_flight --#AIRBOSS.FlightGroup - local charlie=self:_GetCharlieTime(flight) - local Charlie=UTILS.SecondsToClock(charlie) - local stack=flight.flag - local angels=self:_GetAngels(self:_GetMarshalAltitude(stack, flight.case)) - local _,nunit,nsec=self:_GetFlightUnits(flight, true) - local nick=self:_GetACNickname(flight.actype) - N=N+nunit - text=text..string.format("\n[Stack %d] %s (%s*%d+%d): Case %d, Angels %d, Charlie %s", stack, flight.onboard, nick, nunit, nsec, flight.case, angels, tostring(Charlie)) + local N = 0 + if qname == "Marshal" then + for i, _flight in pairs( queue ) do + local flight = _flight -- #AIRBOSS.FlightGroup + local charlie = self:_GetCharlieTime( flight ) + local Charlie = UTILS.SecondsToClock( charlie ) + local stack = flight.flag + local angels = self:_GetAngels( self:_GetMarshalAltitude( stack, flight.case ) ) + local _, nunit, nsec = self:_GetFlightUnits( flight, true ) + local nick = self:_GetACNickname( flight.actype ) + N = N + nunit + text = text .. string.format( "\n[Stack %d] %s (%s*%d+%d): Case %d, Angels %d, Charlie %s", stack, flight.onboard, nick, nunit, nsec, flight.case, angels, tostring( Charlie ) ) end - elseif qname=="Pattern" or qname=="Waiting" then - for i,_flight in pairs(queue) do - local flight=_flight --#AIRBOSS.FlightGroup - local _,nunit,nsec=self:_GetFlightUnits(flight, true) - local nick=self:_GetACNickname(flight.actype) - local ptime=UTILS.SecondsToClock(timer.getAbsTime()-flight.time) - N=N+nunit - text=text..string.format("\n[%d] %s (%s*%d+%d): Case %d, T=%s", i, flight.onboard, nick, nunit, nsec, flight.case, ptime) + elseif qname == "Pattern" or qname == "Waiting" then + for i, _flight in pairs( queue ) do + local flight = _flight -- #AIRBOSS.FlightGroup + local _, nunit, nsec = self:_GetFlightUnits( flight, true ) + local nick = self:_GetACNickname( flight.actype ) + local ptime = UTILS.SecondsToClock( timer.getAbsTime() - flight.time ) + N = N + nunit + text = text .. string.format( "\n[%d] %s (%s*%d+%d): Case %d, T=%s", i, flight.onboard, nick, nunit, nsec, flight.case, ptime ) end end - text=text..string.format("\nTotal AC: %d (airborne %d)", N, nqueue) + text = text .. string.format( "\nTotal AC: %d (airborne %d)", N, nqueue ) end -- Send message. - self:MessageToPlayer(playerData, text, nil, "", nil, true) + self:MessageToPlayer( playerData, text, nil, "", nil, true ) end end end - --- Report information about carrier. -- @param #AIRBOSS self -- @param #string _unitname Name of the player unit. -function AIRBOSS:_DisplayCarrierInfo(_unitname) - self:F2(_unitname) +function AIRBOSS:_DisplayCarrierInfo( _unitname ) + self:F2( _unitname ) -- Get player unit and player name. - local unit, playername = self:_GetPlayerUnitAndName(_unitname) + local unit, playername = self:_GetPlayerUnitAndName( _unitname ) -- Check if we have a player. if unit and playername then -- Player data. - local playerData=self.players[playername] --#AIRBOSS.PlayerData + local playerData = self.players[playername] -- #AIRBOSS.PlayerData if playerData then -- Current coordinates. - local coord=self:GetCoordinate() + local coord = self:GetCoordinate() -- Carrier speed and heading. - local carrierheading=self.carrier:GetHeading() - local carrierspeed=UTILS.MpsToKnots(self.carrier:GetVelocityMPS()) + local carrierheading = self.carrier:GetHeading() + local carrierspeed = UTILS.MpsToKnots( self.carrier:GetVelocityMPS() ) -- TACAN/ICLS. - local tacan="unknown" - local icls="unknown" - if self.TACANon and self.TACANchannel~=nil then - tacan=string.format("%d%s (%s)", self.TACANchannel, self.TACANmode, self.TACANmorse) + local tacan = "unknown" + local icls = "unknown" + if self.TACANon and self.TACANchannel ~= nil then + tacan = string.format( "%d%s (%s)", self.TACANchannel, self.TACANmode, self.TACANmorse ) end - if self.ICLSon and self.ICLSchannel~=nil then - icls=string.format("%d (%s)", self.ICLSchannel, self.ICLSmorse) + if self.ICLSon and self.ICLSchannel ~= nil then + icls = string.format( "%d (%s)", self.ICLSchannel, self.ICLSmorse ) end -- Wind on flight deck - local wind=UTILS.MpsToKnots(select(1, self:GetWindOnDeck())) + local wind = UTILS.MpsToKnots( select( 1, self:GetWindOnDeck() ) ) -- Get groups, units in queues. - local Nmarshal,nmarshal = self:_GetQueueInfo(self.Qmarshal, playerData.case) - local Npattern,npattern = self:_GetQueueInfo(self.Qpattern) - local Nspinning,nspinning = self:_GetQueueInfo(self.Qspinning) - local Nwaiting,nwaiting = self:_GetQueueInfo(self.Qwaiting) - local Ntotal,ntotal = self:_GetQueueInfo(self.flights) + local Nmarshal, nmarshal = self:_GetQueueInfo( self.Qmarshal, playerData.case ) + local Npattern, npattern = self:_GetQueueInfo( self.Qpattern ) + local Nspinning, nspinning = self:_GetQueueInfo( self.Qspinning ) + local Nwaiting, nwaiting = self:_GetQueueInfo( self.Qwaiting ) + local Ntotal, ntotal = self:_GetQueueInfo( self.flights ) -- Current abs time. - local Tabs=timer.getAbsTime() + local Tabs = timer.getAbsTime() -- Get recovery times of carrier. - local recoverytext="Recovery time windows (max 5):" - if #self.recoverytimes==0 then - recoverytext=recoverytext.." none." + local recoverytext = "Recovery time windows (max 5):" + if #self.recoverytimes == 0 then + recoverytext = recoverytext .. " none." else -- Loop over recovery windows. - local rw=0 - for _,_recovery in pairs(self.recoverytimes) do - local recovery=_recovery --#AIRBOSS.Recovery + local rw = 0 + for _, _recovery in pairs( self.recoverytimes ) do + local recovery = _recovery -- #AIRBOSS.Recovery -- Only include current and future recovery windows. - if Tabs=5 then + rw = rw + 1 + if rw >= 5 then -- Break the loop after 5 recovery times. break end @@ -114616,140 +121203,139 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) end -- Recovery tanker TACAN text. - local tankertext=nil + local tankertext = nil if self.tanker then - tankertext=string.format("Recovery tanker frequency %.3f MHz\n", self.tanker.RadioFreq) + tankertext = string.format( "Recovery tanker frequency %.3f MHz\n", self.tanker.RadioFreq ) if self.tanker.TACANon then - tankertext=tankertext..string.format("Recovery tanker TACAN %d%s (%s)",self.tanker.TACANchannel, self.tanker.TACANmode, self.tanker.TACANmorse) + tankertext = tankertext .. string.format( "Recovery tanker TACAN %d%s (%s)", self.tanker.TACANchannel, self.tanker.TACANmode, self.tanker.TACANmorse ) else - tankertext=tankertext.."Recovery tanker TACAN n/a" + tankertext = tankertext .. "Recovery tanker TACAN n/a" end end -- Carrier FSM state. Idle is not clear enough. - local state=self:GetState() - if state=="Idle" then - state="Deck closed" + local state = self:GetState() + if state == "Idle" then + state = "Deck closed" end if self.turning then - state=state.." (turning currently)" + state = state .. " (turning currently)" end -- Message text. - local text=string.format("%s info:\n", self.alias) - text=text..string.format("================================\n") - text=text..string.format("Carrier state: %s\n", state) - if self.case==1 then - text=text..string.format("Case %d recovery ops\n", self.case) + local text = string.format( "%s info:\n", self.alias ) + text = text .. string.format( "================================\n" ) + text = text .. string.format( "Carrier state: %s\n", state ) + if self.case == 1 then + text = text .. string.format( "Case %d recovery ops\n", self.case ) else - local radial=self:GetRadial(self.case, true, true, false) - text=text..string.format("Case %d recovery ops\nMarshal radial %03d°\n", self.case, radial) + local radial = self:GetRadial( self.case, true, true, false ) + text = text .. string.format( "Case %d recovery ops\nMarshal radial %03d°\n", self.case, radial ) end - text=text..string.format("BRC %03d° - FB %03d°\n", self:GetBRC(), self:GetFinalBearing(true)) - text=text..string.format("Speed %.1f kts - Wind on deck %.1f kts\n", carrierspeed, wind) - text=text..string.format("Tower frequency %.3f MHz\n", self.TowerFreq) - text=text..string.format("Marshal radio %.3f MHz\n", self.MarshalFreq) - text=text..string.format("LSO radio %.3f MHz\n", self.LSOFreq) - text=text..string.format("TACAN Channel %s\n", tacan) - text=text..string.format("ICLS Channel %s\n", icls) + text = text .. string.format( "BRC %03d° - FB %03d°\n", self:GetBRC(), self:GetFinalBearing( true ) ) + text = text .. string.format( "Speed %.1f kts - Wind on deck %.1f kts\n", carrierspeed, wind ) + text = text .. string.format( "Tower frequency %.3f MHz\n", self.TowerFreq ) + text = text .. string.format( "Marshal radio %.3f MHz\n", self.MarshalFreq ) + text = text .. string.format( "LSO radio %.3f MHz\n", self.LSOFreq ) + text = text .. string.format( "TACAN Channel %s\n", tacan ) + text = text .. string.format( "ICLS Channel %s\n", icls ) if tankertext then - text=text..tankertext.."\n" + text = text .. tankertext .. "\n" end - text=text..string.format("# A/C total %d (%d)\n", Ntotal, ntotal) - text=text..string.format("# A/C marshal %d (%d)\n", Nmarshal, nmarshal) - text=text..string.format("# A/C pattern %d (%d) - spinning %d (%d)\n", Npattern, npattern, Nspinning, nspinning) - text=text..string.format("# A/C waiting %d (%d)\n", Nwaiting, nwaiting) - text=text..string.format(recoverytext) - self:T2(self.lid..text) + text = text .. string.format( "# A/C total %d (%d)\n", Ntotal, ntotal ) + text = text .. string.format( "# A/C marshal %d (%d)\n", Nmarshal, nmarshal ) + text = text .. string.format( "# A/C pattern %d (%d) - spinning %d (%d)\n", Npattern, npattern, Nspinning, nspinning ) + text = text .. string.format( "# A/C waiting %d (%d)\n", Nwaiting, nwaiting ) + text = text .. string.format( recoverytext ) + self:T2( self.lid .. text ) -- Send message. - self:MessageToPlayer(playerData, text, nil, "", 30, true) + self:MessageToPlayer( playerData, text, nil, "", 30, true ) else - self:E(self.lid..string.format("ERROR: Could not get player data for player %s.", playername)) + self:E( self.lid .. string.format( "ERROR: Could not get player data for player %s.", playername ) ) end end end - --- Report weather conditions at the carrier location. Temperature, QFE pressure and wind data. -- @param #AIRBOSS self -- @param #string _unitname Name of the player unit. -function AIRBOSS:_DisplayCarrierWeather(_unitname) - self:F2(_unitname) +function AIRBOSS:_DisplayCarrierWeather( _unitname ) + self:F2( _unitname ) -- Get player unit and player name. - local unit, playername = self:_GetPlayerUnitAndName(_unitname) + local unit, playername = self:_GetPlayerUnitAndName( _unitname ) -- Check if we have a player. if unit and playername then -- Message text. - local text="" + local text = "" -- Current coordinates. - local coord=self:GetCoordinate() + local coord = self:GetCoordinate() -- Get atmospheric data at carrier location. - local T=coord:GetTemperature() - local P=coord:GetPressure() + local T = coord:GetTemperature() + local P = coord:GetPressure() -- Get wind direction (magnetic) and strength. - local Wd,Ws=self:GetWind(nil, true) + local Wd, Ws = self:GetWind( nil, true ) -- Get Beaufort wind scale. - local Bn,Bd=UTILS.BeaufortScale(Ws) + local Bn, Bd = UTILS.BeaufortScale( Ws ) -- Wind on flight deck. - local WodPA,WodPP=self:GetWindOnDeck() - local WodPA=UTILS.MpsToKnots(WodPA) - local WodPP=UTILS.MpsToKnots(WodPP) + local WodPA, WodPP = self:GetWindOnDeck() + local WodPA = UTILS.MpsToKnots( WodPA ) + local WodPP = UTILS.MpsToKnots( WodPP ) - local WD=string.format('%03d°', Wd) - local Ts=string.format("%d°C",T) + local WD = string.format( '%03d°', Wd ) + local Ts = string.format( "%d°C", T ) - local tT=string.format("%d°C",T) - local tW=string.format("%.1f knots", UTILS.MpsToKnots(Ws)) - local tP=string.format("%.2f inHg", UTILS.hPa2inHg(P)) + local tT = string.format( "%d°C", T ) + local tW = string.format( "%.1f knots", UTILS.MpsToKnots( Ws ) ) + local tP = string.format( "%.2f inHg", UTILS.hPa2inHg( P ) ) -- Report text. - text=text..string.format("Weather Report at Carrier %s:\n", self.alias) - text=text..string.format("================================\n") - text=text..string.format("Temperature %s\n", tT) - text=text..string.format("Wind from %s at %s (%s)\n", WD, tW, Bd) - text=text..string.format("Wind on deck || %.1f kts, == %.1f kts\n", WodPA, WodPP) - text=text..string.format("QFE %.1f hPa = %s", P, tP) + text = text .. string.format( "Weather Report at Carrier %s:\n", self.alias ) + text = text .. string.format( "================================\n" ) + text = text .. string.format( "Temperature %s\n", tT ) + text = text .. string.format( "Wind from %s at %s (%s)\n", WD, tW, Bd ) + text = text .. string.format( "Wind on deck || %.1f kts, == %.1f kts\n", WodPA, WodPP ) + text = text .. string.format( "QFE %.1f hPa = %s", P, tP ) -- More info only reliable if Mission uses static weather. if self.staticweather then - local clouds, visibility, fog, dust=self:_GetStaticWeather() - text=text..string.format("\nVisibility %.1f NM", UTILS.MetersToNM(visibility)) - text=text..string.format("\nCloud base %d ft", UTILS.MetersToFeet(clouds.base)) - text=text..string.format("\nCloud thickness %d ft", UTILS.MetersToFeet(clouds.thickness)) - text=text..string.format("\nCloud density %d", clouds.density) - text=text..string.format("\nPrecipitation %d", clouds.iprecptns) + local clouds, visibility, fog, dust = self:_GetStaticWeather() + text = text .. string.format( "\nVisibility %.1f NM", UTILS.MetersToNM( visibility ) ) + text = text .. string.format( "\nCloud base %d ft", UTILS.MetersToFeet( clouds.base ) ) + text = text .. string.format( "\nCloud thickness %d ft", UTILS.MetersToFeet( clouds.thickness ) ) + text = text .. string.format( "\nCloud density %d", clouds.density ) + text = text .. string.format( "\nPrecipitation %d", clouds.iprecptns ) if fog then - text=text..string.format("\nFog thickness %d ft", UTILS.MetersToFeet(fog.thickness)) - text=text..string.format("\nFog visibility %d ft", UTILS.MetersToFeet(fog.visibility)) + text = text .. string.format( "\nFog thickness %d ft", UTILS.MetersToFeet( fog.thickness ) ) + text = text .. string.format( "\nFog visibility %d ft", UTILS.MetersToFeet( fog.visibility ) ) else - text=text..string.format("\nNo fog") + text = text .. string.format( "\nNo fog" ) end if dust then - text=text..string.format("\nDust density %d", dust) + text = text .. string.format( "\nDust density %d", dust ) else - text=text..string.format("\nNo dust") + text = text .. string.format( "\nNo dust" ) end end -- Debug output. - self:T2(self.lid..text) + self:T2( self.lid .. text ) -- Send message to player group. - self:MessageToPlayer(self.players[playername], text, nil, "", 30, true) + self:MessageToPlayer( self.players[playername], text, nil, "", 30, true ) else - self:E(self.lid..string.format("ERROR! Could not find player unit in CarrierWeather! Unit name = %s", _unitname)) + self:E( self.lid .. string.format( "ERROR! Could not find player unit in CarrierWeather! Unit name = %s", _unitname ) ) end end @@ -114761,31 +121347,31 @@ end -- @param #AIRBOSS self -- @param #string _unitname Name of the player unit. -- @param #AIRBOSS.Difficulty difficulty Difficulty level. -function AIRBOSS:_SetDifficulty(_unitname, difficulty) - self:T2({difficulty=difficulty, unitname=_unitname}) +function AIRBOSS:_SetDifficulty( _unitname, difficulty ) + self:T2( { difficulty = difficulty, unitname = _unitname } ) -- Get player unit and player name. - local unit, playername = self:_GetPlayerUnitAndName(_unitname) + local unit, playername = self:_GetPlayerUnitAndName( _unitname ) -- Check if we have a player. if unit and playername then -- Player data. - local playerData=self.players[playername] --#AIRBOSS.PlayerData + local playerData = self.players[playername] -- #AIRBOSS.PlayerData if playerData then - playerData.difficulty=difficulty - local text=string.format("roger, your skill level is now: %s.", difficulty) - self:MessageToPlayer(playerData, text, nil, playerData.name, 5) + playerData.difficulty = difficulty + local text = string.format( "roger, your skill level is now: %s.", difficulty ) + self:MessageToPlayer( playerData, text, nil, playerData.name, 5 ) else - self:E(self.lid..string.format("ERROR: Could not get player data for player %s.", playername)) + self:E( self.lid .. string.format( "ERROR: Could not get player data for player %s.", playername ) ) end -- Set hints as well. - if playerData.difficulty==AIRBOSS.Difficulty.HARD then - playerData.showhints=false + if playerData.difficulty == AIRBOSS.Difficulty.HARD then + playerData.showhints = false else - playerData.showhints=true + playerData.showhints = true end end @@ -114794,31 +121380,31 @@ end --- Turn player's aircraft attitude display on or off. -- @param #AIRBOSS self -- @param #string _unitname Name of the player unit. -function AIRBOSS:_SetHintsOnOff(_unitname) - self:F2(_unitname) +function AIRBOSS:_SetHintsOnOff( _unitname ) + self:F2( _unitname ) -- Get player unit and player name. - local unit, playername = self:_GetPlayerUnitAndName(_unitname) + local unit, playername = self:_GetPlayerUnitAndName( _unitname ) -- Check if we have a player. if unit and playername then -- Player data. - local playerData=self.players[playername] --#AIRBOSS.PlayerData + local playerData = self.players[playername] -- #AIRBOSS.PlayerData if playerData then -- Invert hints. - playerData.showhints=not playerData.showhints + playerData.showhints = not playerData.showhints -- Inform player. - local text="" - if playerData.showhints==true then - text=string.format("roger, hints are now ON.") + local text = "" + if playerData.showhints == true then + text = string.format( "roger, hints are now ON." ) else - text=string.format("affirm, hints are now OFF.") + text = string.format( "affirm, hints are now OFF." ) end - self:MessageToPlayer(playerData, text, nil, playerData.name, 5) + self:MessageToPlayer( playerData, text, nil, playerData.name, 5 ) end end @@ -114827,20 +121413,20 @@ end --- Turn player's aircraft attitude display on or off. -- @param #AIRBOSS self -- @param #string _unitname Name of the player unit. -function AIRBOSS:_DisplayAttitude(_unitname) - self:F2(_unitname) +function AIRBOSS:_DisplayAttitude( _unitname ) + self:F2( _unitname ) -- Get player unit and player name. - local unit, playername = self:_GetPlayerUnitAndName(_unitname) + local unit, playername = self:_GetPlayerUnitAndName( _unitname ) -- Check if we have a player. if unit and playername then -- Player data. - local playerData=self.players[playername] --#AIRBOSS.PlayerData + local playerData = self.players[playername] -- #AIRBOSS.PlayerData if playerData then - playerData.attitudemonitor=not playerData.attitudemonitor + playerData.attitudemonitor = not playerData.attitudemonitor end end @@ -114849,28 +121435,28 @@ end --- Turn radio subtitles of player on or off. -- @param #AIRBOSS self -- @param #string _unitname Name of the player unit. -function AIRBOSS:_SubtitlesOnOff(_unitname) - self:F2(_unitname) +function AIRBOSS:_SubtitlesOnOff( _unitname ) + self:F2( _unitname ) -- Get player unit and player name. - local unit, playername = self:_GetPlayerUnitAndName(_unitname) + local unit, playername = self:_GetPlayerUnitAndName( _unitname ) -- Check if we have a player. if unit and playername then -- Player data. - local playerData=self.players[playername] --#AIRBOSS.PlayerData + local playerData = self.players[playername] -- #AIRBOSS.PlayerData if playerData then - playerData.subtitles=not playerData.subtitles + playerData.subtitles = not playerData.subtitles -- Inform player. - local text="" - if playerData.subtitles==true then - text=string.format("roger, subtitiles are now ON.") - elseif playerData.subtitles==false then - text=string.format("affirm, subtitiles are now OFF.") + local text = "" + if playerData.subtitles == true then + text = string.format( "roger, subtitiles are now ON." ) + elseif playerData.subtitles == false then + text = string.format( "affirm, subtitiles are now OFF." ) end - self:MessageToPlayer(playerData, text, nil, playerData.name, 5) + self:MessageToPlayer( playerData, text, nil, playerData.name, 5 ) end end @@ -114879,154 +121465,152 @@ end --- Turn radio subtitles of player on or off. -- @param #AIRBOSS self -- @param #string _unitname Name of the player unit. -function AIRBOSS:_TrapsheetOnOff(_unitname) - self:F2(_unitname) +function AIRBOSS:_TrapsheetOnOff( _unitname ) + self:F2( _unitname ) -- Get player unit and player name. - local unit, playername = self:_GetPlayerUnitAndName(_unitname) + local unit, playername = self:_GetPlayerUnitAndName( _unitname ) -- Check if we have a player. if unit and playername then -- Player data. - local playerData=self.players[playername] --#AIRBOSS.PlayerData + local playerData = self.players[playername] -- #AIRBOSS.PlayerData if playerData then -- Check if option is enabled at all. - local text="" + local text = "" if self.trapsheet then -- Invert current setting. - playerData.trapon=not playerData.trapon + playerData.trapon = not playerData.trapon -- Inform player. - if playerData.trapon==true then - text=string.format("roger, your trapsheets are now SAVED.") + if playerData.trapon == true then + text = string.format( "roger, your trapsheets are now SAVED." ) else - text=string.format("affirm, your trapsheets are NOT SAVED.") + text = string.format( "affirm, your trapsheets are NOT SAVED." ) end else - text="negative, trap sheet data recorder is broken on this carrier." + text = "negative, trap sheet data recorder is broken on this carrier." end -- Message to player. - self:MessageToPlayer(playerData, text, nil, playerData.name, 5) + self:MessageToPlayer( playerData, text, nil, playerData.name, 5 ) end end end - --- Display player status. -- @param #AIRBOSS self -- @param #string _unitName Name of the player unit. -function AIRBOSS:_DisplayPlayerStatus(_unitName) +function AIRBOSS:_DisplayPlayerStatus( _unitName ) -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) -- Check if we have a unit which is a player. if _unit and _playername then - local playerData=self.players[_playername] --#AIRBOSS.PlayerData + local playerData = self.players[_playername] -- #AIRBOSS.PlayerData if playerData then -- Pattern step text. - local steptext=playerData.step - if playerData.step==AIRBOSS.PatternStep.HOLDING then - if playerData.holding==nil then - steptext="Transit to Marshal" - elseif playerData.holding==false then - steptext="Marshal (outside zone)" - elseif playerData.holding==true then - steptext="Marshal Stack Holding" + local steptext = playerData.step + if playerData.step == AIRBOSS.PatternStep.HOLDING then + if playerData.holding == nil then + steptext = "Transit to Marshal" + elseif playerData.holding == false then + steptext = "Marshal (outside zone)" + elseif playerData.holding == true then + steptext = "Marshal Stack Holding" end end -- Stack. - local stack=playerData.flag + local stack = playerData.flag -- Stack text. - local stacktext=nil - if stack>0 then - local stackalt=self:_GetMarshalAltitude(stack) - local angels=self:_GetAngels(stackalt) - stacktext=string.format("Marshal Stack %d, Angels %d\n", stack, angels) - + local stacktext = nil + if stack > 0 then + local stackalt = self:_GetMarshalAltitude( stack ) + local angels = self:_GetAngels( stackalt ) + stacktext = string.format( "Marshal Stack %d, Angels %d\n", stack, angels ) -- Hint about TACAN bearing. - if playerData.step==AIRBOSS.PatternStep.HOLDING and playerData.case>1 then + if playerData.step == AIRBOSS.PatternStep.HOLDING and playerData.case > 1 then -- Get inverse magnetic radial potential offset. - local radial=self:GetRadial(playerData.case, true, true, true) - stacktext=stacktext..string.format("Select TACAN %03d°, %d DME\n", radial, angels+15) + local radial = self:GetRadial( playerData.case, true, true, true ) + stacktext = stacktext .. string.format( "Select TACAN %03d°, %d DME\n", radial, angels + 15 ) end end -- Fuel and fuel state. - local fuel=playerData.unit:GetFuel()*100 - local fuelstate=self:_GetFuelState(playerData.unit) + local fuel = playerData.unit:GetFuel() * 100 + local fuelstate = self:_GetFuelState( playerData.unit ) -- Number of units in group. - local _,nunitsGround=self:_GetFlightUnits(playerData, true) - local _,nunitsAirborne=self:_GetFlightUnits(playerData, false) + local _, nunitsGround = self:_GetFlightUnits( playerData, true ) + local _, nunitsAirborne = self:_GetFlightUnits( playerData, false ) -- Player data. - local text=string.format("Status of player %s (%s)\n", playerData.name, playerData.callsign) - text=text..string.format("================================\n") - text=text..string.format("Step: %s\n", steptext) + local text = string.format( "Status of player %s (%s)\n", playerData.name, playerData.callsign ) + text = text .. string.format( "================================\n" ) + text = text .. string.format( "Step: %s\n", steptext ) if stacktext then - text=text..stacktext + text = text .. stacktext end - text=text..string.format("Recovery Case: %d\n", playerData.case) - text=text..string.format("Skill Level: %s\n", playerData.difficulty) - text=text..string.format("Modex: %s (%s)\n", playerData.onboard, self:_GetACNickname(playerData.actype)) - text=text..string.format("Fuel State: %.1f lbs/1000 (%.1f %%)\n", fuelstate/1000, fuel) - text=text..string.format("# units: %d (%d airborne)\n", nunitsGround, nunitsAirborne) - text=text..string.format("Section Lead: %s (%d/%d)", tostring(playerData.seclead), #playerData.section+1, self.NmaxSection+1) - for _,_sec in pairs(playerData.section) do - local sec=_sec --#AIRBOSS.PlayerData - text=text..string.format("\n- %s", sec.name) + text = text .. string.format( "Recovery Case: %d\n", playerData.case ) + text = text .. string.format( "Skill Level: %s\n", playerData.difficulty ) + text = text .. string.format( "Modex: %s (%s)\n", playerData.onboard, self:_GetACNickname( playerData.actype ) ) + text = text .. string.format( "Fuel State: %.1f lbs/1000 (%.1f %%)\n", fuelstate / 1000, fuel ) + text = text .. string.format( "# units: %d (%d airborne)\n", nunitsGround, nunitsAirborne ) + text = text .. string.format( "Section Lead: %s (%d/%d)", tostring( playerData.seclead ), #playerData.section + 1, self.NmaxSection + 1 ) + for _, _sec in pairs( playerData.section ) do + local sec = _sec -- #AIRBOSS.PlayerData + text = text .. string.format( "\n- %s", sec.name ) end - if playerData.step==AIRBOSS.PatternStep.INITIAL then + if playerData.step == AIRBOSS.PatternStep.INITIAL then -- Create a point 3.0 NM astern for re-entry. - local zoneinitial=self:GetCoordinate():Translate(UTILS.NMToMeters(3.5), self:GetRadial(2, false, false, false)) + local zoneinitial = self:GetCoordinate():Translate( UTILS.NMToMeters( 3.5 ), self:GetRadial( 2, false, false, false ) ) -- Heading and distance to initial zone. - local flyhdg=playerData.unit:GetCoordinate():HeadingTo(zoneinitial) - local flydist=UTILS.MetersToNM(playerData.unit:GetCoordinate():Get2DDistance(zoneinitial)) - local brc=self:GetBRC() + local flyhdg = playerData.unit:GetCoordinate():HeadingTo( zoneinitial ) + local flydist = UTILS.MetersToNM( playerData.unit:GetCoordinate():Get2DDistance( zoneinitial ) ) + local brc = self:GetBRC() -- Help player to find its way to the initial zone. - text=text..string.format("\nTo Initial: Fly heading %03d° for %.1f NM and turn to BRC %03d°", flyhdg, flydist, brc) + text = text .. string.format( "\nTo Initial: Fly heading %03d° for %.1f NM and turn to BRC %03d°", flyhdg, flydist, brc ) - elseif playerData.step==AIRBOSS.PatternStep.PLATFORM then + elseif playerData.step == AIRBOSS.PatternStep.PLATFORM then -- Coordinate of the platform zone. - local zoneplatform=self:_GetZonePlatform(playerData.case):GetCoordinate() + local zoneplatform = self:_GetZonePlatform( playerData.case ):GetCoordinate() -- Heading and distance to platform zone. - local flyhdg=playerData.unit:GetCoordinate():HeadingTo(zoneplatform) - local flydist=UTILS.MetersToNM(playerData.unit:GetCoordinate():Get2DDistance(zoneplatform)) + local flyhdg = playerData.unit:GetCoordinate():HeadingTo( zoneplatform ) + local flydist = UTILS.MetersToNM( playerData.unit:GetCoordinate():Get2DDistance( zoneplatform ) ) -- Get heading. - local hdg=self:GetRadial(playerData.case, true, true, true) + local hdg = self:GetRadial( playerData.case, true, true, true ) -- Help player to find its way to the initial zone. - text=text..string.format("\nTo Platform: Fly heading %03d° for %.1f NM and turn to %03d°", flyhdg, flydist, hdg) + text = text .. string.format( "\nTo Platform: Fly heading %03d° for %.1f NM and turn to %03d°", flyhdg, flydist, hdg ) end -- Send message. - self:MessageToPlayer(playerData, text, nil, "", 30, true) + self:MessageToPlayer( playerData, text, nil, "", 30, true ) else - self:E(self.lid..string.format("ERROR: playerData=nil. Unit name=%s, player name=%s", _unitName, _playername)) + self:E( self.lid .. string.format( "ERROR: playerData=nil. Unit name=%s, player name=%s", _unitName, _playername ) ) end else - self:E(self.lid..string.format("ERROR: could not find player for unit %s", _unitName)) + self:E( self.lid .. string.format( "ERROR: could not find player for unit %s", _unitName ) ) end end @@ -115035,92 +121619,91 @@ end -- @param #AIRBOSS self -- @param #string _unitName Name of the player unit. -- @param #boolean flare If true, flare the zone. If false, smoke the zone. -function AIRBOSS:_MarkMarshalZone(_unitName, flare) +function AIRBOSS:_MarkMarshalZone( _unitName, flare ) -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) -- Check if we have a unit which is a player. if _unit and _playername then - local playerData=self.players[_playername] --#AIRBOSS.PlayerData + local playerData = self.players[_playername] -- #AIRBOSS.PlayerData if playerData then -- Get player stack and recovery case. - local stack=playerData.flag - local case=playerData.case + local stack = playerData.flag + local case = playerData.case - local text="" - if stack>0 then + local text = "" + if stack > 0 then -- Get current holding zone. - local zoneHolding=self:_GetZoneHolding(case, stack) + local zoneHolding = self:_GetZoneHolding( case, stack ) -- Get Case I commence zone at three position. - local zoneThree=self:_GetZoneCommence(case, stack) + local zoneThree = self:_GetZoneCommence( case, stack ) -- Pattern alitude. - local patternalt=self:_GetMarshalAltitude(stack, case) + local patternalt = self:_GetMarshalAltitude( stack, case ) -- Flare and smoke at the ground. - patternalt=5 + patternalt = 5 -- Roger! - text="roger, marking" + text = "roger, marking" if flare then -- Marshal WHITE flares. - text=text..string.format("\n* Marshal zone stack %d with WHITE flares.", stack) - zoneHolding:FlareZone(FLARECOLOR.White, 45, nil, patternalt) + text = text .. string.format( "\n* Marshal zone stack %d with WHITE flares.", stack ) + zoneHolding:FlareZone( FLARECOLOR.White, 45, nil, patternalt ) -- Commence RED flares. - text=text.."\n* Commence zone with RED flares." - zoneThree:FlareZone(FLARECOLOR.Red, 45, nil, patternalt) + text = text .. "\n* Commence zone with RED flares." + zoneThree:FlareZone( FLARECOLOR.Red, 45, nil, patternalt ) else -- Marshal WHITE smoke. - text=text..string.format("\n* Marshal zone stack %d with WHITE smoke.", stack) - zoneHolding:SmokeZone(SMOKECOLOR.White, 45, patternalt) + text = text .. string.format( "\n* Marshal zone stack %d with WHITE smoke.", stack ) + zoneHolding:SmokeZone( SMOKECOLOR.White, 45, patternalt ) -- Commence RED smoke - text=text.."\n* Commence zone with RED smoke." - zoneThree:SmokeZone(SMOKECOLOR.Red, 45, patternalt) + text = text .. "\n* Commence zone with RED smoke." + zoneThree:SmokeZone( SMOKECOLOR.Red, 45, patternalt ) end else - text="negative, you are currently not in a Marshal stack. No zones will be marked!" + text = "negative, you are currently not in a Marshal stack. No zones will be marked!" end -- Send message to player. - self:MessageToPlayer(playerData, text, "MARSHAL", playerData.name) + self:MessageToPlayer( playerData, text, "MARSHAL", playerData.name ) end end end - --- Mark CASE I or II/II zones by either smoke or flares. -- @param #AIRBOSS self -- @param #string _unitName Name of the player unit. -- @param #boolean flare If true, flare the zone. If false, smoke the zone. -function AIRBOSS:_MarkCaseZones(_unitName, flare) +function AIRBOSS:_MarkCaseZones( _unitName, flare ) -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) -- Check if we have a unit which is a player. if _unit and _playername then - local playerData=self.players[_playername] --#AIRBOSS.PlayerData + local playerData = self.players[_playername] -- #AIRBOSS.PlayerData if playerData then -- Player's recovery case. - local case=playerData.case + local case = playerData.case -- Initial - local text=string.format("affirm, marking CASE %d zones", case) + local text = string.format( "affirm, marking CASE %d zones", case ) -- Flare or smoke? if flare then @@ -115130,55 +121713,55 @@ function AIRBOSS:_MarkCaseZones(_unitName, flare) ----------- -- Case I/II: Initial - if case==1 or case==2 then - text=text.."\n* initial with GREEN flares" - self:_GetZoneInitial(case):FlareZone(FLARECOLOR.Green, 45) + if case == 1 or case == 2 then + text = text .. "\n* initial with GREEN flares" + self:_GetZoneInitial( case ):FlareZone( FLARECOLOR.Green, 45 ) end -- Case II/III: approach corridor - if case==2 or case==3 then - text=text.."\n* approach corridor with GREEN flares" - self:_GetZoneCorridor(case):FlareZone(FLARECOLOR.Green, 45) + if case == 2 or case == 3 then + text = text .. "\n* approach corridor with GREEN flares" + self:_GetZoneCorridor( case ):FlareZone( FLARECOLOR.Green, 45 ) end -- Case II/III: platform - if case==2 or case==3 then - text=text.."\n* platform with RED flares" - self:_GetZonePlatform(case):FlareZone(FLARECOLOR.Red, 45) + if case == 2 or case == 3 then + text = text .. "\n* platform with RED flares" + self:_GetZonePlatform( case ):FlareZone( FLARECOLOR.Red, 45 ) end -- Case III: dirty up - if case==3 then - text=text.."\n* dirty up with YELLOW flares" - self:_GetZoneDirtyUp(case):FlareZone(FLARECOLOR.Yellow, 45) + if case == 3 then + text = text .. "\n* dirty up with YELLOW flares" + self:_GetZoneDirtyUp( case ):FlareZone( FLARECOLOR.Yellow, 45 ) end -- Case II/III: arc in/out - if case==2 or case==3 then - if math.abs(self.holdingoffset)>0 then - self:_GetZoneArcIn(case):FlareZone(FLARECOLOR.White, 45) - text=text.."\n* arc turn in with WHITE flares" - self:_GetZoneArcOut(case):FlareZone(FLARECOLOR.White, 45) - text=text.."\n* arc trun out with WHITE flares" + if case == 2 or case == 3 then + if math.abs( self.holdingoffset ) > 0 then + self:_GetZoneArcIn( case ):FlareZone( FLARECOLOR.White, 45 ) + text = text .. "\n* arc turn in with WHITE flares" + self:_GetZoneArcOut( case ):FlareZone( FLARECOLOR.White, 45 ) + text = text .. "\n* arc trun out with WHITE flares" end end -- Case III: bullseye - if case==3 then - text=text.."\n* bullseye with GREEN flares" - self:_GetZoneBullseye(case):FlareZone(FLARECOLOR.Green, 45) + if case == 3 then + text = text .. "\n* bullseye with GREEN flares" + self:_GetZoneBullseye( case ):FlareZone( FLARECOLOR.Green, 45 ) end -- Tarawa, LHA and LHD landing spots. - if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then - text=text.."\n* abeam landing stop with RED flares" + if self.carriertype == AIRBOSS.CarrierType.HERMES or self.carriertype == AIRBOSS.CarrierType.TARAWA or self.carriertype == AIRBOSS.CarrierType.AMERICA or self.carriertype == AIRBOSS.CarrierType.JCARLOS or self.carriertype == AIRBOSS.CarrierType.CANBERRA then + text = text .. "\n* abeam landing stop with RED flares" -- Abeam landing spot zone. - local ALSPT=self:_GetZoneAbeamLandingSpot() - ALSPT:FlareZone(FLARECOLOR.Red, 5, nil, UTILS.FeetToMeters(110)) + local ALSPT = self:_GetZoneAbeamLandingSpot() + ALSPT:FlareZone( FLARECOLOR.Red, 5, nil, UTILS.FeetToMeters( 110 ) ) -- Primary landing spot zone. - text=text.."\n* primary landing spot with GREEN flares" - local LSPT=self:_GetZoneLandingSpot() - LSPT:FlareZone(FLARECOLOR.Green, 5, nil, self.carrierparam.deckheight) + text = text .. "\n* primary landing spot with GREEN flares" + local LSPT = self:_GetZoneLandingSpot() + LSPT:FlareZone( FLARECOLOR.Green, 5, nil, self.carrierparam.deckheight ) end else @@ -115188,49 +121771,49 @@ function AIRBOSS:_MarkCaseZones(_unitName, flare) ----------- -- Case I/II: Initial - if case==1 or case==2 then - text=text.."\n* initial with GREEN smoke" - self:_GetZoneInitial(case):SmokeZone(SMOKECOLOR.Green, 45) + if case == 1 or case == 2 then + text = text .. "\n* initial with GREEN smoke" + self:_GetZoneInitial( case ):SmokeZone( SMOKECOLOR.Green, 45 ) end -- Case II/III: Approach Corridor - if case==2 or case==3 then - text=text.."\n* approach corridor with GREEN smoke" - self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) + if case == 2 or case == 3 then + text = text .. "\n* approach corridor with GREEN smoke" + self:_GetZoneCorridor( case ):SmokeZone( SMOKECOLOR.Green, 45 ) end -- Case II/III: platform - if case==2 or case==3 then - text=text.."\n* platform with RED smoke" - self:_GetZonePlatform(case):SmokeZone(SMOKECOLOR.Red, 45) + if case == 2 or case == 3 then + text = text .. "\n* platform with RED smoke" + self:_GetZonePlatform( case ):SmokeZone( SMOKECOLOR.Red, 45 ) end -- Case II/III: arc in/out if offset>0. - if case==2 or case==3 then - if math.abs(self.holdingoffset)>0 then - self:_GetZoneArcIn(case):SmokeZone(SMOKECOLOR.Blue, 45) - text=text.."\n* arc turn in with BLUE smoke" - self:_GetZoneArcOut(case):SmokeZone(SMOKECOLOR.Blue, 45) - text=text.."\n* arc trun out with BLUE smoke" + if case == 2 or case == 3 then + if math.abs( self.holdingoffset ) > 0 then + self:_GetZoneArcIn( case ):SmokeZone( SMOKECOLOR.Blue, 45 ) + text = text .. "\n* arc turn in with BLUE smoke" + self:_GetZoneArcOut( case ):SmokeZone( SMOKECOLOR.Blue, 45 ) + text = text .. "\n* arc trun out with BLUE smoke" end end -- Case III: dirty up - if case==3 then - text=text.."\n* dirty up with ORANGE smoke" - self:_GetZoneDirtyUp(case):SmokeZone(SMOKECOLOR.Orange, 45) + if case == 3 then + text = text .. "\n* dirty up with ORANGE smoke" + self:_GetZoneDirtyUp( case ):SmokeZone( SMOKECOLOR.Orange, 45 ) end -- Case III: bullseye - if case==3 then - text=text.."\n* bullseye with GREEN smoke" - self:_GetZoneBullseye(case):SmokeZone(SMOKECOLOR.Green, 45) + if case == 3 then + text = text .. "\n* bullseye with GREEN smoke" + self:_GetZoneBullseye( case ):SmokeZone( SMOKECOLOR.Green, 45 ) end end -- Send message to player. - self:MessageToPlayer(playerData, text, "MARSHAL", playerData.name) + self:MessageToPlayer( playerData, text, "MARSHAL", playerData.name ) end end @@ -115239,18 +121822,18 @@ end --- LSO radio check. Will broadcase LSO message at given LSO frequency. -- @param #AIRBOSS self -- @param #string _unitName Name fo the player unit. -function AIRBOSS:_LSORadioCheck(_unitName) - self:F(_unitName) +function AIRBOSS:_LSORadioCheck( _unitName ) + self:F( _unitName ) -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) -- Check if we have a unit which is a player. if _unit and _playername then - local playerData=self.players[_playername] --#AIRBOSS.PlayerData + local playerData = self.players[_playername] -- #AIRBOSS.PlayerData if playerData then -- Broadcase LSO radio check message on LSO radio. - self:RadioTransmission(self.LSORadio, self.LSOCall.RADIOCHECK, nil, nil, nil, true) + self:RadioTransmission( self.LSORadio, self.LSOCall.RADIOCHECK, nil, nil, nil, true ) end end end @@ -115258,23 +121841,22 @@ end --- Marshal radio check. Will broadcase Marshal message at given Marshal frequency. -- @param #AIRBOSS self -- @param #string _unitName Name fo the player unit. -function AIRBOSS:_MarshalRadioCheck(_unitName) - self:F(_unitName) +function AIRBOSS:_MarshalRadioCheck( _unitName ) + self:F( _unitName ) -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) -- Check if we have a unit which is a player. if _unit and _playername then - local playerData=self.players[_playername] --#AIRBOSS.PlayerData + local playerData = self.players[_playername] -- #AIRBOSS.PlayerData if playerData then -- Broadcase Marshal radio check message on Marshal radio. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.RADIOCHECK, nil, nil, nil, true) + self:RadioTransmission( self.MarshalRadio, self.MarshalCall.RADIOCHECK, nil, nil, nil, true ) end end end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ -- Persistence Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ @@ -115283,94 +121865,92 @@ end -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. -- @param #AIRBOSS.LSOgrade grade LSO grad data. -function AIRBOSS:_SaveTrapSheet(playerData, grade) +function AIRBOSS:_SaveTrapSheet( playerData, grade ) -- Nothing to save. - if playerData.trapsheet==nil or #playerData.trapsheet==0 or not io then + if playerData.trapsheet == nil or #playerData.trapsheet == 0 or not io then return end --- Function that saves data to file - local function _savefile(filename, data) - local f = io.open(filename, "wb") + local function _savefile( filename, data ) + local f = io.open( filename, "wb" ) if f then - f:write(data) + f:write( data ) f:close() else - self:E(self.lid..string.format("ERROR: could not save trap sheet to file %s.\nFile may contain invalid characters.", tostring(filename))) + self:E( self.lid .. string.format( "ERROR: could not save trap sheet to file %s.\nFile may contain invalid characters.", tostring( filename ) ) ) end end -- Set path or default. - local path=self.trappath + local path = self.trappath if lfs then - path=path or lfs.writedir() + path = path or lfs.writedir() end - -- Create unused file name. - local filename=nil - for i=1,9999 do + local filename = nil + for i = 1, 9999 do -- Create file name - if self.trapprefix then - filename=string.format("%s_%s-%04d.csv", self.trapprefix, playerData.actype, i) - else - local name=UTILS.ReplaceIllegalCharacters(playerData.name, "_") - filename=string.format("AIRBOSS-%s_Trapsheet-%s_%s-%04d.csv", self.alias, name, playerData.actype, i) - end + if self.trapprefix then + filename = string.format( "%s_%s-%04d.csv", self.trapprefix, playerData.actype, i ) + else + local name = UTILS.ReplaceIllegalCharacters( playerData.name, "_" ) + filename = string.format( "AIRBOSS-%s_Trapsheet-%s_%s-%04d.csv", self.alias, name, playerData.actype, i ) + end -- Set path. - if path~=nil then - filename=path.."\\"..filename + if path ~= nil then + filename = path .. "\\" .. filename end -- Check if file exists. - local _exists=UTILS.FileExists(filename) + local _exists = UTILS.FileExists( filename ) if not _exists then break end end - -- Info - local text=string.format("Saving player %s trapsheet to file %s", playerData.name, filename) - self:I(self.lid..text) + local text = string.format( "Saving player %s trapsheet to file %s", playerData.name, filename ) + self:I( self.lid .. text ) -- Header line - local data="#Time,Rho,X,Z,Alt,AoA,GSE,LUE,Vtot,Vy,Gamma,Pitch,Roll,Yaw,Step,Grade,Points,Details\n" + local data = "#Time,Rho,X,Z,Alt,AoA,GSE,LUE,Vtot,Vy,Gamma,Pitch,Roll,Yaw,Step,Grade,Points,Details\n" - local g0=playerData.trapsheet[1] --#AIRBOSS.GrooveData - local T0=g0.Time + local g0 = playerData.trapsheet[1] -- #AIRBOSS.GrooveData + local T0 = g0.Time - --for _,_groove in ipairs(playerData.trapsheet) do - for i=1,#playerData.trapsheet do - --local groove=_groove --#AIRBOSS.GrooveData - local groove=playerData.trapsheet[i] - local t=groove.Time-T0 - local a=UTILS.MetersToNM(groove.Rho or 0) - local b=-groove.X or 0 - local c=groove.Z or 0 - local d=UTILS.MetersToFeet(groove.Alt or 0) - local e=groove.AoA or 0 - local f=groove.GSE or 0 - local g=-groove.LUE or 0 - local h=UTILS.MpsToKnots(groove.Vel or 0) - local i=(groove.Vy or 0)*196.85 - local j=groove.Gamma or 0 - local k=groove.Pitch or 0 - local l=groove.Roll or 0 - local m=groove.Yaw or 0 - local n=self:_GS(groove.Step, -1) or "n/a" - local o=groove.Grade or "n/a" - local p=groove.GradePoints or 0 - local q=groove.GradeDetail or "n/a" - -- t a b c d e f g h i j k l m n o p q - data=data..string.format("%.2f,%.3f,%.1f,%.1f,%.1f,%.2f,%.2f,%.2f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%s,%s,%.1f,%s\n",t,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q) + -- for _,_groove in ipairs(playerData.trapsheet) do + for i = 1, #playerData.trapsheet do + -- local groove=_groove --#AIRBOSS.GrooveData + local groove = playerData.trapsheet[i] + local t = groove.Time - T0 + local a = UTILS.MetersToNM( groove.Rho or 0 ) + local b = -groove.X or 0 + local c = groove.Z or 0 + local d = UTILS.MetersToFeet( groove.Alt or 0 ) + local e = groove.AoA or 0 + local f = groove.GSE or 0 + local g = -groove.LUE or 0 + local h = UTILS.MpsToKnots( groove.Vel or 0 ) + local i = (groove.Vy or 0) * 196.85 + local j = groove.Gamma or 0 + local k = groove.Pitch or 0 + local l = groove.Roll or 0 + local m = groove.Yaw or 0 + local n = self:_GS( groove.Step, -1 ) or "n/a" + local o = groove.Grade or "n/a" + local p = groove.GradePoints or 0 + local q = groove.GradeDetail or "n/a" + -- t a b c d e f g h i j k l m n o p q + data = data .. string.format( "%.2f,%.3f,%.1f,%.1f,%.1f,%.2f,%.2f,%.2f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%s,%s,%.1f,%s\n", t, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q ) end -- Save file. - _savefile(filename, data) + _savefile( filename, data ) end --- On before "Save" event. Checks if io and lfs are available. @@ -115380,17 +121960,17 @@ end -- @param #string To To state. -- @param #string path (Optional) Path where the file is saved. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized. -- @param #string filename (Optional) File name for saving the player grades. Default is "AIRBOSS-_LSOgrades.csv". -function AIRBOSS:onbeforeSave(From, Event, To, path, filename) +function AIRBOSS:onbeforeSave( From, Event, To, path, filename ) -- Check io module is available. if not io then - self:E(self.lid.."ERROR: io not desanitized. Can't save player grades.") + self:E( self.lid .. "ERROR: io not desanitized. Can't save player grades." ) return false end -- Check default path. - if path==nil and not lfs then - self:E(self.lid.."WARNING: lfs not desanitized. Results will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") + if path == nil and not lfs then + self:E( self.lid .. "WARNING: lfs not desanitized. Results will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder." ) end return true @@ -115403,72 +121983,69 @@ end -- @param #string To To state. -- @param #string path Path where the file is saved. If nil, file is saved in the DCS root installtion directory or your "Saved Games" folder if lfs was desanitized. -- @param #string filename (Optional) File name for saving the player grades. Default is "AIRBOSS-_LSOgrades.csv". -function AIRBOSS:onafterSave(From, Event, To, path, filename) +function AIRBOSS:onafterSave( From, Event, To, path, filename ) --- Function that saves data to file - local function _savefile(filename, data) - local f = assert(io.open(filename, "wb")) - f:write(data) + local function _savefile( filename, data ) + local f = assert( io.open( filename, "wb" ) ) + f:write( data ) f:close() end -- Set path or default. if lfs then - path=path or lfs.writedir() + path = path or lfs.writedir() end -- Set file name. - filename=filename or string.format("AIRBOSS-%s_LSOgrades.csv", self.alias) + filename = filename or string.format( "AIRBOSS-%s_LSOgrades.csv", self.alias ) -- Set path. - if path~=nil then - filename=path.."\\"..filename + if path ~= nil then + filename = path .. "\\" .. filename end -- Header line - local scores="Name,Pass,Points Final,Points Pass,Grade,Details,Wire,Tgroove,Case,Wind,Modex,Airframe,Carrier Type,Carrier Name,Theatre,Mission Time,Mission Date,OS Date\n" + local scores = "Name,Pass,Points Final,Points Pass,Grade,Details,Wire,Tgroove,Case,Wind,Modex,Airframe,Carrier Type,Carrier Name,Theatre,Mission Time,Mission Date,OS Date\n" -- Loop over all players. - local n=0 - for playername,grades in pairs(self.playerscores) do + local n = 0 + for playername, grades in pairs( self.playerscores ) do -- Loop over player grades table. - for i,_grade in pairs(grades) do - local grade=_grade --#AIRBOSS.LSOgrade + for i, _grade in pairs( grades ) do + local grade = _grade -- #AIRBOSS.LSOgrade -- Check some stuff that could be nil. - local wire="n/a" - if grade.wire and grade.wire<=4 then - wire=tostring(grade.wire) + local wire = "n/a" + if grade.wire and grade.wire <= 4 then + wire = tostring( grade.wire ) end - local Tgroove="n/a" - if grade.Tgroove and grade.Tgroove<=360 and grade.case<3 then - Tgroove=tostring(UTILS.Round(grade.Tgroove, 1)) + local Tgroove = "n/a" + if grade.Tgroove and grade.Tgroove <= 360 and grade.case < 3 then + Tgroove = tostring( UTILS.Round( grade.Tgroove, 1 ) ) end - local finalscore="n/a" + local finalscore = "n/a" if grade.finalscore then - finalscore=tostring(UTILS.Round(grade.finalscore, 1)) + finalscore = tostring( UTILS.Round( grade.finalscore, 1 ) ) end -- Compile grade line. - scores=scores..string.format("%s,%d,%s,%.1f,%s,%s,%s,%s,%d,%s,%s,%s,%s,%s,%s,%s,%s,%s\n", - playername, i, finalscore, grade.points, grade.grade, grade.details, wire, Tgroove, grade.case, - grade.wind, grade.modex, grade.airframe, grade.carriertype, grade.carriername, grade.theatre, grade.mitime, grade.midate, grade.osdate) - n=n+1 + scores = scores .. string.format( "%s,%d,%s,%.1f,%s,%s,%s,%s,%d,%s,%s,%s,%s,%s,%s,%s,%s,%s\n", playername, i, finalscore, grade.points, grade.grade, grade.details, wire, Tgroove, grade.case, grade.wind, grade.modex, grade.airframe, grade.carriertype, grade.carriername, grade.theatre, grade.mitime, grade.midate, grade.osdate ) + n = n + 1 end end -- Info - local text=string.format("Saving %d player LSO grades to file %s", n, filename) - self:I(self.lid..text) + local text = string.format( "Saving %d player LSO grades to file %s", n, filename ) + self:I( self.lid .. text ) -- Save file. - _savefile(filename, scores) + _savefile( filename, scores ) end - --- On before "Load" event. Checks if the file that the player grades from exists. -- @param #AIRBOSS self -- @param #string From From state. @@ -115476,13 +122053,13 @@ end -- @param #string To To state. -- @param #string path (Optional) Path where the file is loaded from. Default is the DCS installation root directory or your "Saved Games\\DCS" folder if lfs was desanizized. -- @param #string filename (Optional) File name for saving the player grades. Default is "AIRBOSS-_LSOgrades.csv". -function AIRBOSS:onbeforeLoad(From, Event, To, path, filename) +function AIRBOSS:onbeforeLoad( From, Event, To, path, filename ) --- Function that check if a file exists. - local function _fileexists(name) - local f=io.open(name,"r") - if f~=nil then - io.close(f) + local function _fileexists( name ) + local f = io.open( name, "r" ) + if f ~= nil then + io.close( f ) return true else return false @@ -115491,41 +122068,40 @@ function AIRBOSS:onbeforeLoad(From, Event, To, path, filename) -- Check io module is available. if not io then - self:E(self.lid.."WARNING: io not desanitized. Can't load player grades.") + self:E( self.lid .. "WARNING: io not desanitized. Can't load player grades." ) return false end -- Check default path. - if path==nil and not lfs then - self:E(self.lid.."WARNING: lfs not desanitized. Results will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") + if path == nil and not lfs then + self:E( self.lid .. "WARNING: lfs not desanitized. Results will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder." ) end -- Set path or default. if lfs then - path=path or lfs.writedir() + path = path or lfs.writedir() end -- Set file name. - filename=filename or string.format("AIRBOSS-%s_LSOgrades.csv", self.alias) + filename = filename or string.format( "AIRBOSS-%s_LSOgrades.csv", self.alias ) -- Set path. - if path~=nil then - filename=path.."\\"..filename + if path ~= nil then + filename = path .. "\\" .. filename end -- Check if file exists. - local exists=_fileexists(filename) + local exists = _fileexists( filename ) if exists then return true else - self:E(self.lid..string.format("WARNING: Player LSO grades file %s does not exist.", filename)) + self:E( self.lid .. string.format( "WARNING: Player LSO grades file %s does not exist.", filename ) ) return false end end - --- On after "Load" event. Loads grades of all players from file. -- @param #AIRBOSS self -- @param #string From From state. @@ -115533,102 +122109,102 @@ end -- @param #string To To state. -- @param #string path Path where the file is loaded from. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if lfs was desanizied. -- @param #string filename (Optional) File name for saving the player grades. Default is "AIRBOSS-_LSOgrades.csv". -function AIRBOSS:onafterLoad(From, Event, To, path, filename) +function AIRBOSS:onafterLoad( From, Event, To, path, filename ) --- Function that load data from a file. - local function _loadfile(filename) - local f=assert(io.open(filename, "rb")) - local data=f:read("*all") + local function _loadfile( filename ) + local f = assert( io.open( filename, "rb" ) ) + local data = f:read( "*all" ) f:close() return data end -- Set path or default. if lfs then - path=path or lfs.writedir() + path = path or lfs.writedir() end -- Set file name. - filename=filename or string.format("AIRBOSS-%s_LSOgrades.csv", self.alias) + filename = filename or string.format( "AIRBOSS-%s_LSOgrades.csv", self.alias ) -- Set path. - if path~=nil then - filename=path.."\\"..filename + if path ~= nil then + filename = path .. "\\" .. filename end -- Info message. - local text=string.format("Loading player LSO grades from file %s", filename) - MESSAGE:New(text,10):ToAllIf(self.Debug) - self:I(self.lid..text) + local text = string.format( "Loading player LSO grades from file %s", filename ) + MESSAGE:New( text, 10 ):ToAllIf( self.Debug ) + self:I( self.lid .. text ) -- Load asset data from file. - local data=_loadfile(filename) + local data = _loadfile( filename ) -- Split by line break. - local playergrades=UTILS.Split(data,"\n") + local playergrades = UTILS.Split( data, "\n" ) -- Remove first header line. - table.remove(playergrades, 1) + table.remove( playergrades, 1 ) -- Init player scores table. - self.playerscores={} + self.playerscores = {} -- Loop over all lines. - local n=0 - for _,gradeline in pairs(playergrades) do + local n = 0 + for _, gradeline in pairs( playergrades ) do -- Parameters are separated by commata. - local gradedata=UTILS.Split(gradeline, ",") + local gradedata = UTILS.Split( gradeline, "," ) -- Debug info. - self:T2(gradedata) + self:T2( gradedata ) -- Grade table - local grade={} --#AIRBOSS.LSOgrade + local grade = {} -- #AIRBOSS.LSOgrade --- Line format: -- playername, i, grade.finalscore, grade.points, grade.grade, grade.details, wire, Tgroove, case, -- time, wind, airframe, modex, carriertype, carriername, theatre, date - local playername=gradedata[1] - if gradedata[3]~=nil and gradedata[3]~="n/a" then - grade.finalscore=tonumber(gradedata[3]) + local playername = gradedata[1] + if gradedata[3] ~= nil and gradedata[3] ~= "n/a" then + grade.finalscore = tonumber( gradedata[3] ) end - grade.points=tonumber(gradedata[4]) - grade.grade=tostring(gradedata[5]) - grade.details=tostring(gradedata[6]) - if gradedata[7]~=nil and gradedata[7]~="n/a" then - grade.wire=tonumber(gradedata[7]) + grade.points = tonumber( gradedata[4] ) + grade.grade = tostring( gradedata[5] ) + grade.details = tostring( gradedata[6] ) + if gradedata[7] ~= nil and gradedata[7] ~= "n/a" then + grade.wire = tonumber( gradedata[7] ) end - if gradedata[8]~=nil and gradedata[8]~="n/a" then - grade.Tgroove=tonumber(gradedata[8]) + if gradedata[8] ~= nil and gradedata[8] ~= "n/a" then + grade.Tgroove = tonumber( gradedata[8] ) end - grade.case=tonumber(gradedata[9]) + grade.case = tonumber( gradedata[9] ) -- new - grade.wind=gradedata[10] or "n/a" - grade.modex=gradedata[11] or "n/a" - grade.airframe=gradedata[12] or "n/a" - grade.carriertype=gradedata[13] or "n/a" - grade.carriername=gradedata[14] or "n/a" - grade.theatre=gradedata[15] or "n/a" - grade.mitime=gradedata[16] or "n/a" - grade.midate=gradedata[17] or "n/a" - grade.osdate=gradedata[18] or "n/a" + grade.wind = gradedata[10] or "n/a" + grade.modex = gradedata[11] or "n/a" + grade.airframe = gradedata[12] or "n/a" + grade.carriertype = gradedata[13] or "n/a" + grade.carriername = gradedata[14] or "n/a" + grade.theatre = gradedata[15] or "n/a" + grade.mitime = gradedata[16] or "n/a" + grade.midate = gradedata[17] or "n/a" + grade.osdate = gradedata[18] or "n/a" -- Init player table if necessary. - self.playerscores[playername]=self.playerscores[playername] or {} + self.playerscores[playername] = self.playerscores[playername] or {} -- Add grade to table. - table.insert(self.playerscores[playername], grade) + table.insert( self.playerscores[playername], grade ) - n=n+1 + n = n + 1 -- Debug info. - self:T2({playername, self.playerscores[playername]}) + self:T2( { playername, self.playerscores[playername] } ) end -- Info message. - local text=string.format("Loaded %d player LSO grades from file %s", n, filename) - self:I(self.lid..text) + local text = string.format( "Loaded %d player LSO grades from file %s", n, filename ) + self:I( self.lid .. text ) end @@ -115668,7 +122244,7 @@ end -- @field #string tankergroupname Name of the late activated tanker template group. -- @field Wrapper.Group#GROUP tanker Tanker group. -- @field Wrapper.Airbase#AIRBASE airbase The home airbase object of the tanker. Normally the aircraft carrier. --- @field Core.Radio#BEACON beacon Tanker TACAN beacon. +-- @field Core.Beacon#BEACON beacon Tanker TACAN beacon. -- @field #number TACANchannel TACAN channel. Default 1. -- @field #string TACANmode TACAN mode, i.e. "X" or "Y". Default "Y". Use only "Y" for AA TACAN stations! -- @field #string TACANmorse TACAN morse code. Three letters identifying the TACAN station. Default "TKR". @@ -116421,10 +122997,11 @@ end -- @param #RECOVERYTANKER self -- @param #number channel TACAN channel. Default 1. -- @param #string morse TACAN morse code identifier. Three letters. Default "TKR". +-- @param #string mode TACAN mode, which can be either "Y" (default) or "X". -- @return #RECOVERYTANKER self -function RECOVERYTANKER:SetTACAN(channel, morse) +function RECOVERYTANKER:SetTACAN(channel, morse, mode) self.TACANchannel=channel or 1 - self.TACANmode="Y" + self.TACANmode=mode or "Y" self.TACANmorse=morse or "TKR" self.TACANon=true return self @@ -117262,7 +123839,6 @@ function RECOVERYTANKER:_ActivateTACAN(delay) if delay and delay>0 then -- Schedule TACAN activation. - --SCHEDULER:New(nil, self._ActivateTACAN, {self}, delay) self:ScheduleOnce(delay, RECOVERYTANKER._ActivateTACAN, self) else @@ -118135,7 +124711,7 @@ function RESCUEHELO:_OnEventCrashOrEject(EventData) -- Debug. local text=string.format("Unit %s crashed or ejected.", unitname) MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) - self:I(self.lid..text) + self:T(self.lid..text) -- Get coordinate of unit. local coord=unit:GetCoordinate() @@ -118681,7 +125257,6 @@ end --- 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. @@ -118938,7 +125513,6 @@ end -- @field #ATIS ATIS = { ClassName = "ATIS", - Debug = false, lid = nil, theatre = nil, airbasename = nil, @@ -119243,26 +125817,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 @@ -119369,15 +125943,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 @@ -119438,6 +126003,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 @@ -119766,15 +126340,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 @@ -120020,7 +126598,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 --- @@ -120088,8 +126667,8 @@ function ATIS:onafterBroadcast(From, Event, To) -- Convert to °F. if self.TDegF then - temperature=UTILS.CelciusToFarenheit(temperature) - dewpoint=UTILS.CelciusToFarenheit(dewpoint) + temperature=UTILS.CelsiusToFahrenheit(temperature) + dewpoint=UTILS.CelsiusToFahrenheit(dewpoint) end local TEMPERATURE=string.format("%d", math.abs(temperature)) @@ -120384,6 +126963,10 @@ function ATIS:onafterBroadcast(From, Event, To) end -- Wind + -- Adding a space after each digit of WINDFROM to convert this to aviation-speak for TTS via SRS + if self.useSRS then + WINDFROM = string.gsub(WINDFROM,".", "%1 ") + end if self.metric then subtitle=string.format("Wind from %s at %s m/s", WINDFROM, WINDSPEED) else @@ -120642,19 +127225,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 @@ -120766,7 +127349,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 @@ -120784,7 +127367,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 @@ -120802,7 +127385,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 @@ -120841,7 +127424,7 @@ function ATIS:onafterBroadcast(From, Event, To) -- TACAN if self.tacan then - subtitle=string.format("TACAN channel %dX", self.tacan) + subtitle=string.format("TACAN channel %dX Ray", self.tacan) if not self.useSRS then self:Transmission(ATIS.Sound.TACANChannel, 1.0, subtitle) self.radioqueue:Number2Transmission(tostring(self.tacan), nil, 0.2) @@ -120861,7 +127444,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 @@ -120988,39 +127571,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. @@ -121231,19 +127794,19 @@ end -- * Set mission start/stop times -- * Set mission priority and urgency (can cancel running missions) -- * Specific mission options for ROE, ROT, formation, etc. --- * Compatible with FLIGHTGROUP, NAVYGROUP, ARMYGROUP, AIRWING, WINGCOMMANDER and CHIEF classes +-- * Compatible with OPS classes like FLIGHTGROUP, NAVYGROUP, ARMYGROUP, AIRWING, etc. -- * FSM events when a mission is done, successful or failed -- -- === -- -- ## Example Missions: --- +-- -- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20Auftrag). --- +-- -- === --- +-- -- ### Author: **funkyfranky** --- +-- -- === -- @module Ops.Auftrag -- @image OPS_Auftrag.png @@ -121252,20 +127815,28 @@ end --- AUFTRAG class. -- @type AUFTRAG -- @field #string ClassName Name of the class. --- @field #boolean Debug Debug mode. Messages to all about status. -- @field #number verbose Verbosity level. -- @field #string lid Class id string for output to DCS log file. -- @field #number auftragsnummer Auftragsnummer. -- @field #string type Mission type. +-- @field #table categories Mission categories. -- @field #string status Mission status. +-- @field #table legions Assigned legions. +-- @field #table statusLegion Mission status of all assigned LEGIONs. +-- @field #string statusCommander Mission status of the COMMANDER. +-- @field #string statusChief Mission status of the CHIEF. -- @field #table groupdata Group specific data. -- @field #string name Mission name. -- @field #number prio Mission priority. -- @field #boolean urgent Mission is urgent. Running missions with lower prio might be cancelled. -- @field #number importance Importance. --- @field #number Tstart Mission start time in seconds. --- @field #number Tstop Mission stop time in seconds. +-- @field #number Tstart Mission start time in abs. seconds. +-- @field #number Tstop Mission stop time in abs. seconds. -- @field #number duration Mission duration in seconds. +-- @field #number durationExe Mission execution time in seconds. +-- @field #number Texecuting Time stamp (abs) when mission is executing. Is `#nil` on start. +-- @field #number Tpush Mission push/execute time in abs. seconds. +-- @field #number Tstarted Time stamp (abs) when mission is started. -- @field Wrapper.Marker#MARKER marker F10 map marker. -- @field #boolean markerOn If true, display marker on F10 map with the AUFTRAG status. -- @field #number markerCoaliton Coalition to which the marker is dispayed. @@ -121274,19 +127845,24 @@ end -- @field #number Nkills Number of (enemy) units killed by assets of this mission. -- @field #number Nelements Number of elements (units) assigned to mission. -- @field #number dTevaluate Time interval in seconds before the mission result is evaluated after mission is over. --- @field #number Tover Mission abs. time stamp, when mission was over. +-- @field #number Tover Mission abs. time stamp, when mission was over. -- @field #table conditionStart Condition(s) that have to be true, before the mission will be started. --- @field #table conditionSuccess If all stop conditions are true, the mission is cancelled. --- @field #table conditionFailure If all stop conditions are true, the mission is cancelled. --- +-- @field #table conditionSuccess If all conditions are true, the mission is cancelled. +-- @field #table conditionFailure If all conditions are true, the mission is cancelled. +-- @field #table conditionPush If all conditions are true, the mission is executed. Before, the group(s) wait at the mission execution waypoint. +-- -- @field #number orbitSpeed Orbit speed in m/s. -- @field #number orbitAltitude Orbit altitude in meters. -- @field #number orbitHeading Orbit heading in degrees. -- @field #number orbitLeg Length of orbit leg in meters. -- @field Core.Point#COORDINATE orbitRaceTrack Race-track orbit coordinate. --- +-- -- @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. -- @field #table engageTargetTypes Table of target types that are engaged in the engagement zone. -- @field #number engageAltitude Engagement altitude in meters. @@ -121297,211 +127873,282 @@ end -- @field #boolean engageAsGroup Group attack. -- @field #number engageMaxDistance Max engage distance. -- @field #number refuelSystem Refuel type (boom or probe) for TANKER missions. --- +-- -- @field Wrapper.Group#GROUP escortGroup The group to be escorted. -- @field DCS#Vec3 escortVec3 The 3D offset vector from the escorted group to the escort group. --- +-- -- @field #number facDesignation FAC designation type. -- @field #boolean facDatalink FAC datalink enabled. -- @field #number facFreq FAC radio frequency in MHz. -- @field #number facModu FAC radio modulation 0=AM 1=FM. --- +-- -- @field Core.Set#SET_GROUP transportGroupSet Groups to be transported. -- @field Core.Point#COORDINATE transportPickup Coordinate where to pickup the cargo. -- @field Core.Point#COORDINATE transportDropoff Coordinate where to drop off the cargo. --- +-- @field #number transportPickupRadius Radius in meters for pickup zone. Default 500 m. +-- +-- @field Ops.OpsTransport#OPSTRANSPORT opstransport OPS transport assignment. +-- @field #number NcarriersMin Min number of required carrier assets. +-- @field #number NcarriersMax Max number of required carrier assets. +-- @field Core.Zone#ZONE transportDeployZone Deploy zone of an OPSTRANSPORT. +-- @field Core.Zone#ZONE transportDisembarkZone Disembark zone of an OPSTRANSPORT. +-- -- @field #number artyRadius Radius in meters. -- @field #number artyShots Number of shots fired. +-- @field #number artyAltitude Altitude in meters. Can be used for a Barrage. +-- @field #number artyHeading Heading in degrees (for Barrage). +-- @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 Ops.WingCommander#WINGCOMMANDER wingcommander The WINGCOMMANDER managing this mission. --- @field Ops.AirWing#AIRWING airwing The assigned airwing. --- @field #table assets Airwing Assets assigned for this mission. --- @field #number nassets Number of required assets by the Airwing. --- @field #number requestID The ID of the queued warehouse request. Necessary to cancel the request if the mission was cancelled before the request is processed. --- @field #boolean cancelContactLost If true, cancel mission if the contact is lost. --- @field #table squadrons User specified airwing squadrons assigned for this mission. Only these will be considered for the job! --- @field #table payloads User specified airwing payloads for this mission. Only these will be considered for the job! +-- @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. +-- @field #table assets Warehouse assets assigned for this mission. +-- @field #number NassetsMin Min. number of required warehouse assets. +-- @field #number NassetsMax Max. number of required warehouse assets. +-- @field #number NescortMin Min. number of required escort assets for each group the mission is assigned to. +-- @field #number NescortMax Max. number of required escort assets for each group the mission is assigned to. +-- @field #string escortMissionType Escort mission type. +-- @field #table escortTargetTypes Target types that will be engaged. +-- @field #number escortEngageRange Engage range in nautical miles (NM). +-- @field #number Nassets Number of requested warehouse assets. +-- @field #table NassetsLegMin Number of required warehouse assets for each assigned legion. +-- @field #table NassetsLegMax Number of required warehouse assets for each assigned legion. +-- @field #table requestID The ID of the queued warehouse request. Necessary to cancel the request if the mission was cancelled before the request is processed. +-- @field #table payloads User specified airwing payloads for this mission. Only these will be considered for the job! -- @field Ops.AirWing#AIRWING.PatrolData patroldata Patrol data. --- +-- +-- @field #table specialLegions User specified legions assigned for this mission. Only these will be considered for the job! +-- @field #table specialCohorts User specified cohorts assigned for this mission. Only these will be considered for the job! +-- @field #table transportLegions Legions explicitly requested for providing transport carrier assets. +-- @field #table transportCohorts Cohorts explicitly requested for providing transport carrier assets. +-- @field #table escortLegions Legions explicitly requested for providing escorting assets. +-- @field #table escortCohorts Cohorts explicitly requested for providing escorting assets. +-- -- @field #string missionTask Mission task. See `ENUMS.MissionTask`. -- @field #number missionAltitude Mission altitude in meters. -- @field #number missionSpeed Mission speed in km/h. -- @field #number missionFraction Mission coordiante fraction. Default is 0.5. --- @field #number missionRange Mission range in meters. Used in AIRWING class. +-- @field #number missionRange Mission range in meters. Used by LEGION classes (AIRWING, BRIGADE, ...). -- @field Core.Point#COORDINATE missionWaypointCoord Mission waypoint coordinate. --- +-- @field Core.Point#COORDINATE missionEgressCoord Mission egress waypoint coordinate. +-- @field #number missionWaypointRadius Random radius in meters. +-- -- @field #table enrouteTasks Mission enroute tasks. --- +-- -- @field #number repeated Number of times mission was repeated. -- @field #number repeatedSuccess Number of times mission was repeated after a success. -- @field #number repeatedFailure Number of times mission was repeated after a failure. -- @field #number Nrepeat Number of times the mission is repeated. -- @field #number NrepeatFailure Number of times mission is repeated if failed. -- @field #number NrepeatSuccess Number of times mission is repeated if successful. --- +-- -- @field Ops.OpsGroup#OPSGROUP.Radio radio Radio freq and modulation. -- @field Ops.OpsGroup#OPSGROUP.Beacon tacan TACAN setting. -- @field Ops.OpsGroup#OPSGROUP.Beacon icls ICLS setting. --- +-- -- @field #number optionROE ROE. -- @field #number optionROT ROT. -- @field #number optionAlarm Alarm state. -- @field #number optionFormation Formation. +-- @field #boolean optionEPLRS EPLRS datalink. -- @field #number optionCM Counter measures. -- @field #number optionRTBammo RTB on out-of-ammo. -- @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 ---- *A warrior's mission is to foster the success of others.* - Morihei Ueshiba +--- *A warrior's mission is to foster the success of others.* -- Morihei Ueshiba -- -- === -- --- ![Banner Image](..\Presentations\OPS\Auftrag\_Main.png) --- -- # The AUFTRAG Concept --- +-- -- The AUFTRAG class significantly simplifies the workflow of using DCS tasks. -- -- You can think of an AUFTRAG as document, which contains the mission briefing, i.e. information about the target location, mission altitude, speed and various other parameters. -- This document can be handed over directly to a pilot (or multiple pilots) via the @{Ops.FlightGroup#FLIGHTGROUP} class. The pilots will then execute the mission. --- The AUFTRAG document can also be given to an AIRWING. The airwing will then determine the best assets (pilots and payloads) available for the job. --- One more up the food chain, an AUFTRAG can be passed to a WINGCOMMANDER. The wing commander will find the best AIRWING and pass the job over to it. +-- +-- The AUFTRAG document can also be given to an AIRWING. The airwing will then determine the best assets (pilots and payloads) available for the job. +-- +-- Similarly, an AUFTRAG can be given to ground or navel groups via the @{Ops.ArmyGroup#ARMYGROUP} or @{Ops.NavyGroup#NAVYGROUP} classes, respectively. These classes have also +-- AIRWING analouges, which are called BRIGADE and FLEET. Brigades and fleets will likewise select the best assets they have available and pass on the AUFTRAG to them. +-- +-- +-- One more up the food chain, an AUFTRAG can be passed to a COMMANDER. The commander will recruit the best assets of AIRWINGs, BRIGADEs and/or FLEETs and pass the job over to it. +-- -- -- # Airborne Missions --- +-- -- Several mission types are supported by this class. --- +-- -- ## Anti-Ship --- --- An anti-ship mission can be created with the @{#AUFTRAG.NewANTISHIP}(*Target, Altitude*) function. --- +-- +-- An anti-ship mission can be created with the @{#AUFTRAG.NewANTISHIP}() function. +-- -- ## AWACS --- +-- -- An AWACS mission can be created with the @{#AUFTRAG.NewAWACS}() function. --- +-- -- ## BAI --- +-- -- A BAI mission can be created with the @{#AUFTRAG.NewBAI}() function. --- +-- -- ## Bombing --- +-- -- A bombing mission can be created with the @{#AUFTRAG.NewBOMBING}() function. --- +-- -- ## Bombing Runway --- +-- -- A bombing runway mission can be created with the @{#AUFTRAG.NewBOMBRUNWAY}() function. --- +-- -- ## Bombing Carpet --- +-- -- A carpet bombing mission can be created with the @{#AUFTRAG.NewBOMBCARPET}() function. --- +-- -- ## CAP --- +-- -- A CAP mission can be created with the @{#AUFTRAG.NewCAP}() function. --- +-- -- ## CAS --- +-- -- A CAS mission can be created with the @{#AUFTRAG.NewCAS}() function. --- +-- -- ## Escort --- +-- -- An escort mission can be created with the @{#AUFTRAG.NewESCORT}() function. --- +-- -- ## FACA --- +-- -- An FACA mission can be created with the @{#AUFTRAG.NewFACA}() function. --- +-- -- ## Ferry --- +-- -- Not implemented yet. --- +-- -- ## Intercept --- +-- -- An intercept mission can be created with the @{#AUFTRAG.NewINTERCEPT}() function. --- +-- -- ## Orbit --- +-- -- An orbit mission can be created with the @{#AUFTRAG.NewORBIT}() function. --- +-- -- ## GCICAP --- +-- -- An patrol mission can be created with the @{#AUFTRAG.NewGCICAP}() function. --- +-- -- ## RECON --- --- Not implemented yet. --- +-- +-- An reconnaissance mission can be created with the @{#AUFTRAG.NewRECON}() function. +-- -- ## RESCUE HELO --- +-- -- An rescue helo mission can be created with the @{#AUFTRAG.NewRESCUEHELO}() function. --- +-- -- ## SEAD --- +-- -- An SEAD mission can be created with the @{#AUFTRAG.NewSEAD}() function. --- +-- -- ## STRIKE --- +-- -- An strike mission can be created with the @{#AUFTRAG.NewSTRIKE}() function. --- +-- -- ## Tanker --- +-- -- A refueling tanker mission can be created with the @{#AUFTRAG.NewTANKER}() function. --- +-- -- ## TROOPTRANSPORT --- +-- -- A troop transport mission can be created with the @{#AUFTRAG.NewTROOPTRANSPORT}() function. -- +-- ## CARGOTRANSPORT +-- +-- A cargo transport mission can be created with the @{#AUFTRAG.NewCARGOTRANSPORT}() function. +-- +-- ## HOVER +-- +-- A mission for a helicoptre or VSTOL plane to Hover at a point for a certain amount of time can be created with the @{#AUFTRAG.NewHOVER}() function. +-- -- # Ground Missions --- +-- -- ## ARTY --- +-- -- An arty mission can be created with the @{#AUFTRAG.NewARTY}() function. --- --- # Options and Parameters --- --- +-- +-- ## GROUNDATTACK +-- +-- A ground attack mission can be created with the @{#AUFTRAG.NewGROUNDATTACK}() function. +-- -- # Assigning Missions --- --- An AUFTRAG can be assigned to groups, airwings or wingcommanders --- +-- +-- An AUFTRAG can be assigned to groups (FLIGHTGROUP, ARMYGROUP, NAVYGROUP), legions (AIRWING, BRIGADE, FLEET) or to a COMMANDER. +-- -- ## Group Level --- +-- -- ### Flight Group --- --- Assigning an AUFTRAG to a flight groups is done via the @{Ops.FlightGroup#FLIGHTGROUP.AddMission} function. See FLIGHTGROUP docs for details. --- +-- +-- Assigning an AUFTRAG to a flight group is done via the @{Ops.FlightGroup#FLIGHTGROUP.AddMission} function. See FLIGHTGROUP docs for details. +-- +-- ### Army Group +-- +-- Assigning an AUFTRAG to an army group is done via the @{Ops.ArmyGroup#ARMYGROUP.AddMission} function. See ARMYGROUP docs for details. +-- -- ### Navy Group --- --- Assigning an AUFTRAG to a navy groups is done via the @{Ops.NavyGroup#NAVYGROUP.AddMission} function. See NAVYGROUP docs for details. --- --- ## Airwing Level --- +-- +-- Assigning an AUFTRAG to a navy group is done via the @{Ops.NavyGroup#NAVYGROUP.AddMission} function. See NAVYGROUP docs for details. +-- +-- ## Legion Level +-- -- Adding an AUFTRAG to an airwing is done via the @{Ops.AirWing#AIRWING.AddMission} function. See AIRWING docs for further details. +-- Similarly, an AUFTRAG can be added to a brigade via the @{Ops.Brigade#BRIGADE.AddMission} function. +-- +-- ## Commander Level +-- +-- Assigning an AUFTRAG to a commander is done via the @{Ops.Commander#COMMANDER.AddMission} function. +-- The commander will select the best assets available from all the legions under his command. See COMMANDER docs for details. -- --- ## Wing Commander Level --- --- Assigning an AUFTRAG to a wing commander is done via the @{Ops.WingCommander#WINGCOMMANDER.AddMission} function. See WINGCOMMADER docs for details. +-- ## Chief Level -- +-- Assigning an AUFTRAG to a commander is done via the @{Ops.Chief#CHIEF.AddMission} function. The chief will simply pass on the mission to his/her commander. +-- +-- # Transportation -- +-- TODO +-- +-- -- # Events --- --- The AUFTRAG class creates many useful (FSM) events, which can be used in the mission designers script. --- --- +-- +-- The AUFTRAG class creates many useful (FSM) events, which can be used in the mission designers script. +-- +-- TODO +-- +-- -- # Examples --- +-- +-- TODO +-- -- -- @field #AUFTRAG AUFTRAG = { ClassName = "AUFTRAG", - Debug = false, verbose = 0, lid = nil, auftragsnummer = nil, - groupdata = {}, + groupdata = {}, + legions = {}, + statusLegion = {}, + requestID = {}, assets = {}, + NassetsLegMin = {}, + NassetsLegMax = {}, missionFraction = 0.5, enrouteTasks = {}, marker = nil, @@ -121510,6 +128157,7 @@ AUFTRAG = { conditionStart = {}, conditionSuccess = {}, conditionFailure = {}, + conditionPush = {}, } --- Global mission counter. @@ -121528,7 +128176,7 @@ _AUFTRAGSNR=0 -- @field #string CAS Close Air Support. -- @field #string ESCORT Escort mission. -- @field #string FACA Forward AirController airborne mission. --- @field #string FERRY Ferry flight mission. +-- @field #string FERRY Ferry mission. -- @field #string INTERCEPT Intercept mission. -- @field #string ORBIT Orbit mission. -- @field #string GCICAP Similar to CAP but no auto engage targets. @@ -121541,9 +128189,27 @@ _AUFTRAGSNR=0 -- @field #string TROOPTRANSPORT Troop transport mission. -- @field #string ARTY Fire at point. -- @field #string PATROLZONE Patrol a zone. +-- @field #string OPSTRANSPORT Ops transport. +-- @field #string AMMOSUPPLY Ammo supply. +-- @field #string FUELSUPPLY Fuel supply. +-- @field #string ALERT5 Alert 5. +-- @field #string ONGUARD On guard. +-- @field #string ARMOREDGUARD On guard - with armored groups. +-- @field #string BARRAGE Barrage. +-- @field #string ARMORATTACK Armor attack. +-- @field #string CASENHANCED Enhanced CAS. +-- @field #string HOVER Hover. +-- @field #string GROUNDATTACK Ground attack. +-- @field #string CARGOTRANSPORT Cargo transport. +-- @field #string RELOCATECOHORT Relocate a cohort from one legion to another. +-- @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", - AWACS="AWACS", + AWACS="AWACS", BAI="BAI", BOMBING="Bombing", BOMBRUNWAY="Bomb Runway", @@ -121565,14 +128231,74 @@ AUFTRAG.Type={ TROOPTRANSPORT="Troop Transport", ARTY="Fire At Point", PATROLZONE="Patrol Zone", + OPSTRANSPORT="Ops Transport", + AMMOSUPPLY="Ammo Supply", + FUELSUPPLY="Fuel Supply", + ALERT5="Alert5", + ONGUARD="On Guard", + ARMOREDGUARD="Armored Guard", + BARRAGE="Barrage", + ARMORATTACK="Armor Attack", + CASENHANCED="CAS Enhanced", + HOVER="Hover", + GROUNDATTACK="Ground Attack", + CARGOTRANSPORT="Cargo Transport", + RELOCATECOHORT="Relocate Cohort", + AIRDEFENSE="Air Defence", + EWR="Early Warning Radar", + RECOVERYTANKER="Recovery Tanker", + REARMING="Rearming", + NOTHING="Nothing", +} + +--- Special task description. +-- @type AUFTRAG.SpecialTask +-- @field #string FORMATION AI formation task. +-- @field #string PATROLZONE Patrol zone task. +-- @field #string RECON Recon task. +-- @field #string AMMOSUPPLY Ammo Supply. +-- @field #string FUELSUPPLY Fuel Supply. +-- @field #string ALERT5 Alert 5 task. +-- @field #string ONGUARD On guard. +-- @field #string ARMOREDGUARD On guard with armor. +-- @field #string BARRAGE Barrage. +-- @field #string HOVER Hover. +-- @field #string GROUNDATTACK Ground attack. +-- @field #string FERRY Ferry mission. +-- @field #string RELOCATECOHORT Relocate cohort. +-- @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", + PATROLZONE="PatrolZone", + RECON="ReconMission", + AMMOSUPPLY="Ammo Supply", + FUELSUPPLY="Fuel Supply", + ALERT5="Alert5", + ONGUARD="On Guard", + ARMOREDGUARD="ArmoredGuard", + BARRAGE="Barrage", + ARMORATTACK="AmorAttack", + HOVER="Hover", + GROUNDATTACK="Ground Attack", + FERRY="Ferry", + RELOCATECOHORT="Relocate Cohort", + AIRDEFENSE="Air Defense", + EWR="Early Warning Radar", + RECOVERYTANKER="Recovery Tanker", + REARMING="Rearming", + NOTHING="Nothing", } --- Mission status. -- @type AUFTRAG.Status --- @field #string PLANNED Mission is at the early planning stage. --- @field #string QUEUED Mission is queued at an airwing. +-- @field #string PLANNED Mission is at the early planning stage and has not been added to any queue. +-- @field #string QUEUED Mission is queued at a LEGION. -- @field #string REQUESTED Mission assets were requested from the warehouse. --- @field #string SCHEDULED Mission is scheduled in a FLIGHGROUP queue waiting to be started. +-- @field #string SCHEDULED Mission is scheduled in an OPSGROUP queue waiting to be started. -- @field #string STARTED Mission has started but is not executed yet. -- @field #string EXECUTING Mission is being executed. -- @field #string DONE Mission is over. @@ -121628,6 +128354,22 @@ AUFTRAG.TargetType={ SETUNIT="SetUnit", } +--- Mission category. +-- @type AUFTRAG.Category +-- @field #string AIRCRAFT Airplanes and helicopters. +-- @field #string AIRPLANE Airplanes. +-- @field #string HELICOPTER Helicopter. +-- @field #string GROUND Ground troops. +-- @field #string NAVAL Naval grous. +AUFTRAG.Category={ + ALL="All", + AIRCRAFT="Aircraft", + AIRPLANE="Airplane", + HELICOPTER="Helicopter", + GROUND="Ground", + NAVAL="Naval", +} + --- Target data. -- @type AUFTRAG.TargetData -- @field Wrapper.Positionable#POSITIONABLE Target Target Object. @@ -121657,27 +128399,33 @@ AUFTRAG.TargetType={ --- Group specific data. Each ops group subscribed to this mission has different data for this. -- @type AUFTRAG.GroupData -- @field Ops.OpsGroup#OPSGROUP opsgroup The OPS group. --- @field Core.Point#COORDINATE waypointcoordinate Waypoint coordinate. --- @field #number waypointindex Waypoint index. +-- @field Core.Point#COORDINATE waypointcoordinate Ingress waypoint coordinate. +-- @field #number waypointindex Mission (ingress) Waypoint UID. +-- @field #number waypointEgressUID Egress Waypoint UID. +-- @field Core.Point#COORDINATE wpegresscoordinate Egress waypoint coordinate. +-- -- @field Ops.OpsGroup#OPSGROUP.Task waypointtask Waypoint task. -- @field #string status Group mission status. --- @field Ops.AirWing#AIRWING.SquadronAsset asset The squadron asset. +-- @field Functional.Warehouse#WAREHOUSE.Assetitem asset The warehouse asset. --- AUFTRAG class version. -- @field #string version -AUFTRAG.version="0.6.0" +AUFTRAG.version="0.9.6" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- DONE: Option to assign a specific payload for the mission (requires an AIRWING). +-- TODO: Replace engageRange by missionRange. Here and in other classes. CTRL+H is your friend! -- TODO: Mission success options damaged, destroyed. --- TODO: Recon mission. What input? Set of coordinates? --- NOPE: Clone mission. How? Deepcopy? ==> Create a new auftrag. -- TODO: F10 marker to create new missions. -- TODO: Add recovery tanker mission for boat ops. +-- DONE: Added auftrag category. +-- DONE: Missions can be assigned to multiple legions. +-- DONE: Option to assign a specific payload for the mission (requires an AIRWING). +-- NOPE: Clone mission. How? Deepcopy? ==> Create a new auftrag. +-- DONE: Recon mission. What input? Set of coordinates? -- DONE: Option to assign mission to specific squadrons (requires an AIRWING). -- DONE: Add mission start conditions. -- DONE: Add rescue helo mission for boat ops. @@ -121703,74 +128451,284 @@ function AUFTRAG:New(Type) -- Inherit everything from FSM class. local self=BASE:Inherit(self, FSM:New()) -- #AUFTRAG - + -- Increase global counter. _AUFTRAGSNR=_AUFTRAGSNR+1 - + -- Mission type. self.type=Type - + -- Auftragsnummer. self.auftragsnummer=_AUFTRAGSNR - + -- Log ID. self:_SetLogID() - + -- State is planned. self.status=AUFTRAG.Status.PLANNED - - -- Defaults - --self:SetVerbosity(0) + + -- Defaults . self:SetName() self:SetPriority() self:SetTime() + self:SetRequiredAssets() self.engageAsGroup=true + self.dTevaluate=5 + + -- Init counters and stuff. self.repeated=0 self.repeatedSuccess=0 self.repeatedFailure=0 self.Nrepeat=0 self.NrepeatFailure=0 self.NrepeatSuccess=0 - self.nassets=1 - self.dTevaluate=5 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) - + -- PLANNED --> (QUEUED) --> (REQUESTED) --> SCHEDULED --> STARTED --> EXECUTING --> DONE - - self:AddTransition("*", "Planned", AUFTRAG.Status.PLANNED) -- Mission is in planning stage. - self:AddTransition(AUFTRAG.Status.PLANNED, "Queued", AUFTRAG.Status.QUEUED) -- Mission is in queue of an AIRWING. + self:AddTransition("*", "Planned", AUFTRAG.Status.PLANNED) -- Mission is in planning stage. Could be in the queue of a COMMANDER or CHIEF. + self:AddTransition(AUFTRAG.Status.PLANNED, "Queued", AUFTRAG.Status.QUEUED) -- Mission is in queue of a LEGION. self:AddTransition(AUFTRAG.Status.QUEUED, "Requested", AUFTRAG.Status.REQUESTED) -- Mission assets have been requested from the warehouse. self:AddTransition(AUFTRAG.Status.REQUESTED, "Scheduled", AUFTRAG.Status.SCHEDULED) -- Mission added to the first ops group queue. - + self:AddTransition(AUFTRAG.Status.PLANNED, "Scheduled", AUFTRAG.Status.SCHEDULED) -- From planned directly to scheduled. - - self:AddTransition(AUFTRAG.Status.SCHEDULED, "Started", AUFTRAG.Status.STARTED) -- First asset has started the mission + + self:AddTransition(AUFTRAG.Status.SCHEDULED, "Started", AUFTRAG.Status.STARTED) -- First asset has started the mission. self:AddTransition(AUFTRAG.Status.STARTED, "Executing", AUFTRAG.Status.EXECUTING) -- First asset is executing the mission. - + self:AddTransition("*", "Done", AUFTRAG.Status.DONE) -- All assets have reported that mission is done. - - self:AddTransition("*", "Cancel", "*") -- Command to cancel the mission. - + + self:AddTransition("*", "Cancel", AUFTRAG.Status.CANCELLED) -- Command to cancel the mission. + self:AddTransition("*", "Success", AUFTRAG.Status.SUCCESS) self:AddTransition("*", "Failed", AUFTRAG.Status.FAILED) - + self:AddTransition("*", "Status", "*") self:AddTransition("*", "Stop", "*") - + self:AddTransition("*", "Repeat", AUFTRAG.Status.PLANNED) self:AddTransition("*", "ElementDestroyed", "*") - self:AddTransition("*", "GroupDead", "*") + self:AddTransition("*", "GroupDead", "*") self:AddTransition("*", "AssetDead", "*") - + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Status". + -- @function [parent=#AUFTRAG] Status + -- @param #AUFTRAG self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#AUFTRAG] __Status + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". + -- @function [parent=#AUFTRAG] Stop + -- @param #AUFTRAG self + + --- Triggers the FSM event "Stop" after a delay. + -- @function [parent=#AUFTRAG] __Stop + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Planned". + -- @function [parent=#AUFTRAG] Planned + -- @param #AUFTRAG self + + --- Triggers the FSM event "Planned" after a delay. + -- @function [parent=#AUFTRAG] __Planned + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Planned" event. + -- @function [parent=#AUFTRAG] OnAfterPlanned + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Queued". + -- @function [parent=#AUFTRAG] Queued + -- @param #AUFTRAG self + + --- Triggers the FSM event "Queued" after a delay. + -- @function [parent=#AUFTRAG] __Queued + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Queued" event. + -- @function [parent=#AUFTRAG] OnAfterQueued + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Requested". + -- @function [parent=#AUFTRAG] Requested + -- @param #AUFTRAG self + + --- Triggers the FSM event "Requested" after a delay. + -- @function [parent=#AUFTRAG] __Requested + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Requested" event. + -- @function [parent=#AUFTRAG] OnAfterRequested + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Scheduled". + -- @function [parent=#AUFTRAG] Scheduled + -- @param #AUFTRAG self + + --- Triggers the FSM event "Scheduled" after a delay. + -- @function [parent=#AUFTRAG] __Scheduled + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Scheduled" event. + -- @function [parent=#AUFTRAG] OnAfterScheduled + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Started". + -- @function [parent=#AUFTRAG] Started + -- @param #AUFTRAG self + + --- Triggers the FSM event "Started" after a delay. + -- @function [parent=#AUFTRAG] __Started + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Started" event. + -- @function [parent=#AUFTRAG] OnAfterStarted + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Executing". + -- @function [parent=#AUFTRAG] Executing + -- @param #AUFTRAG self + + --- Triggers the FSM event "Executing" after a delay. + -- @function [parent=#AUFTRAG] __Executing + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Executing" event. + -- @function [parent=#AUFTRAG] OnAfterExecuting + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Cancel". + -- @function [parent=#AUFTRAG] Cancel + -- @param #AUFTRAG self + + --- Triggers the FSM event "Cancel" after a delay. + -- @function [parent=#AUFTRAG] __Cancel + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Cancel" event. + -- @function [parent=#AUFTRAG] OnAfterCancel + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Done". + -- @function [parent=#AUFTRAG] Done + -- @param #AUFTRAG self + + --- Triggers the FSM event "Done" after a delay. + -- @function [parent=#AUFTRAG] __Done + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Done" event. + -- @function [parent=#AUFTRAG] OnAfterDone + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Success". + -- @function [parent=#AUFTRAG] Success + -- @param #AUFTRAG self + + --- Triggers the FSM event "Success" after a delay. + -- @function [parent=#AUFTRAG] __Success + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Success" event. + -- @function [parent=#AUFTRAG] OnAfterSuccess + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- Triggers the FSM event "Failed". + -- @function [parent=#AUFTRAG] Failed + -- @param #AUFTRAG self + + --- Triggers the FSM event "Failed" after a delay. + -- @function [parent=#AUFTRAG] __Failed + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Failed" event. + -- @function [parent=#AUFTRAG] OnAfterFailed + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- Triggers the FSM event "Repeat". + -- @function [parent=#AUFTRAG] Repeat + -- @param #AUFTRAG self + + --- Triggers the FSM event "Repeat" after a delay. + -- @function [parent=#AUFTRAG] __Repeat + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Repeat" event. + -- @function [parent=#AUFTRAG] OnAfterRepeat + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- Init status update. self:__Status(-1) - + return self end @@ -121778,7 +128736,7 @@ end -- Create Missions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Create an ANTI-SHIP mission. +--- **[AIR]** Create an ANTI-SHIP mission. -- @param #AUFTRAG self -- @param Wrapper.Positionable#POSITIONABLE Target The target to attack. Can be passed as a @{Wrapper.Group#GROUP} or @{Wrapper.Unit#UNIT} object. -- @param #number Altitude Engage altitude in feet. Default 2000 ft. @@ -121786,49 +128744,90 @@ end function AUFTRAG:NewANTISHIP(Target, Altitude) local mission=AUFTRAG:New(AUFTRAG.Type.ANTISHIP) - + mission:_TargetFromObject(Target) - + -- DCS task parameters: mission.engageWeaponType=ENUMS.WeaponFlag.Auto mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL mission.engageAltitude=UTILS.FeetToMeters(Altitude or 2000) - + -- Mission options: mission.missionTask=ENUMS.MissionTask.ANTISHIPSTRIKE mission.missionAltitude=mission.engageAltitude mission.missionFraction=0.4 mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.EvadeFire - + + mission.categories={AUFTRAG.Category.AIRCRAFT} + mission.DCStask=mission:GetDCSMissionTask() - + return mission end ---- Create an ORBIT mission, which can be either a circular orbit or a race-track pattern. +--- **[AIR ROTARY]** Create an HOVER mission. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Coordinate Where to hover. +-- @param #number Altitude Hover altitude in feet AGL. Default is 50 feet above ground. +-- @param #number Time Time in seconds to hold the hover. Default 300 seconds. +-- @param #number Speed Speed in knots to fly to the target coordinate. Default 150kn. +-- @param #number MissionAlt Altitide to fly towards the mission in feet AGL. Default 1000ft. +-- @return #AUFTRAG self +function AUFTRAG:NewHOVER(Coordinate, Altitude, Time, Speed, MissionAlt) + + local mission=AUFTRAG:New(AUFTRAG.Type.HOVER) + + -- Altitude. + if Altitude then + mission.hoverAltitude=Coordinate:GetLandHeight()+UTILS.FeetToMeters(Altitude) + else + mission.hoverAltitude=Coordinate:GetLandHeight()+UTILS.FeetToMeters(50) + end + + mission:_TargetFromObject(Coordinate) + + mission.hoverSpeed = 0.1 -- the DCS Task itself will shortly be build with this so MPS + mission.hoverTime = Time or 300 + mission.missionSpeed = UTILS.KnotsToMps(Speed or 150) + + -- Mission options: + mission.missionAltitude=mission.MissionAlt or UTILS.FeetToMeters(1000) + mission.missionFraction=0.9 + mission.optionROE=ENUMS.ROE.ReturnFire + mission.optionROT=ENUMS.ROT.PassiveDefense + + mission.categories={AUFTRAG.Category.HELICOPTER} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[AIR]** Create an ORBIT mission, which can be either a circular orbit or a race-track pattern. -- @param #AUFTRAG self -- @param Core.Point#COORDINATE Coordinate Where to orbit. -- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`. --- @param #number Speed Orbit speed in knots. Default 350 KIAS. +-- @param #number Speed Orbit speed in knots. Default 350 KIAS. -- @param #number Heading Heading of race-track pattern in degrees. If not specified, a circular orbit is performed. -- @param #number Leg Length of race-track in NM. If not specified, a circular orbit is performed. -- @return #AUFTRAG self function AUFTRAG:NewORBIT(Coordinate, Altitude, Speed, Heading, Leg) local mission=AUFTRAG:New(AUFTRAG.Type.ORBIT) - + -- Altitude. if Altitude then mission.orbitAltitude=UTILS.FeetToMeters(Altitude) else mission.orbitAltitude=Coordinate.y - end + end Coordinate.y=mission.orbitAltitude - + mission:_TargetFromObject(Coordinate) - mission.orbitSpeed = UTILS.KnotsToMps(Speed or 350) + mission.orbitSpeed = UTILS.KnotsToMps(Speed or 350) -- the DCS Task itself will shortly be build with this so MPS + mission.missionSpeed = UTILS.KnotsToKmph(Speed or 350) if Heading and Leg then mission.orbitHeading=Heading @@ -121836,23 +128835,25 @@ function AUFTRAG:NewORBIT(Coordinate, Altitude, Speed, Heading, Leg) mission.orbitRaceTrack=Coordinate:Translate(mission.orbitLeg, mission.orbitHeading, true) end - + -- Mission options: - mission.missionAltitude=mission.orbitAltitude*0.9 - mission.missionFraction=0.9 + mission.missionAltitude=mission.orbitAltitude*0.9 + mission.missionFraction=0.9 mission.optionROE=ENUMS.ROE.ReturnFire mission.optionROT=ENUMS.ROT.PassiveDefense + mission.categories={AUFTRAG.Category.AIRCRAFT} + mission.DCStask=mission:GetDCSMissionTask() return mission end ---- Create an ORBIT mission, where the aircraft will go in a circle around the specified coordinate. +--- **[AIR]** Create an ORBIT mission, where the aircraft will go in a circle around the specified coordinate. -- @param #AUFTRAG self -- @param Core.Point#COORDINATE Coordinate Position where to orbit around. -- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`. --- @param #number Speed Orbit speed in knots. Default 350 KIAS. +-- @param #number Speed Orbit speed in knots. Default 350 KIAS. -- @return #AUFTRAG self function AUFTRAG:NewORBIT_CIRCLE(Coordinate, Altitude, Speed) @@ -121861,7 +128862,7 @@ function AUFTRAG:NewORBIT_CIRCLE(Coordinate, Altitude, Speed) return mission end ---- Create an ORBIT mission, where the aircraft will fly a race-track pattern. +--- **[AIR]** Create an ORBIT mission, where the aircraft will fly a race-track pattern. -- @param #AUFTRAG self -- @param Core.Point#COORDINATE Coordinate Where to orbit. -- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`. @@ -121875,11 +128876,11 @@ function AUFTRAG:NewORBIT_RACETRACK(Coordinate, Altitude, Speed, Heading, Leg) Leg = Leg or 10 local mission=AUFTRAG:NewORBIT(Coordinate, Altitude, Speed, Heading, Leg) - + return mission end ---- Create a Ground Controlled CAP (GCICAP) mission. Flights with this task are considered for A2A INTERCEPT missions by the CHIEF class. They will perform a compat air patrol but not engage by +--- **[AIR]** Create a Ground Controlled CAP (GCICAP) mission. Flights with this task are considered for A2A INTERCEPT missions by the CHIEF class. They will perform a compat air patrol but not engage by -- themselfs. They wait for the CHIEF to tell them whom to engage. -- @param #AUFTRAG self -- @param Core.Point#COORDINATE Coordinate Where to orbit. @@ -121892,51 +128893,55 @@ function AUFTRAG:NewGCICAP(Coordinate, Altitude, Speed, Heading, Leg) -- Create ORBIT first. local mission=AUFTRAG:NewORBIT_RACETRACK(Coordinate, Altitude, Speed, Heading, Leg) - + -- Mission type GCICAP. mission.type=AUFTRAG.Type.GCICAP - + mission:_SetLogID() - -- Mission options: + -- Mission options: mission.missionTask=ENUMS.MissionTask.INTERCEPT mission.optionROT=ENUMS.ROT.PassiveDefense - + + mission.categories={AUFTRAG.Category.AIRCRAFT} + return mission end ---- Create a TANKER mission. +--- **[AIR]** Create a TANKER mission. -- @param #AUFTRAG self -- @param Core.Point#COORDINATE Coordinate Where to orbit. -- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`. -- @param #number Speed Orbit speed in knots. Default 350 kts. -- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West). -- @param #number Leg Length of race-track in NM. Default 10 NM. --- @param #number RefuelSystem Refueling system (1=boom, 0=probe). This info is *only* for AIRWINGs so they launch the right tanker type. +-- @param #number RefuelSystem Refueling system (0=boom, 1=probe). This info is *only* for AIRWINGs so they launch the right tanker type. -- @return #AUFTRAG self function AUFTRAG:NewTANKER(Coordinate, Altitude, Speed, Heading, Leg, RefuelSystem) -- Create ORBIT first. local mission=AUFTRAG:NewORBIT_RACETRACK(Coordinate, Altitude, Speed, Heading, Leg) - + -- Mission type TANKER. mission.type=AUFTRAG.Type.TANKER - + mission:_SetLogID() - + mission.refuelSystem=RefuelSystem - + -- Mission options: - mission.missionTask=ENUMS.MissionTask.REFUELING + mission.missionTask=ENUMS.MissionTask.REFUELING mission.optionROE=ENUMS.ROE.WeaponHold mission.optionROT=ENUMS.ROT.PassiveDefense - + + mission.categories={AUFTRAG.Category.AIRCRAFT} + mission.DCStask=mission:GetDCSMissionTask() - + return mission end ---- Create a AWACS mission. +--- **[AIR]** Create a AWACS mission. -- @param #AUFTRAG self -- @param Core.Point#COORDINATE Coordinate Where to orbit. Altitude is also taken from the coordinate. -- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`. @@ -121948,46 +128953,50 @@ function AUFTRAG:NewAWACS(Coordinate, Altitude, Speed, Heading, Leg) -- Create ORBIT first. local mission=AUFTRAG:NewORBIT_RACETRACK(Coordinate, Altitude, Speed, Heading, Leg) - + -- Mission type AWACS. mission.type=AUFTRAG.Type.AWACS - + mission:_SetLogID() - + -- Mission options: - mission.missionTask=ENUMS.MissionTask.AWACS + mission.missionTask=ENUMS.MissionTask.AWACS mission.optionROE=ENUMS.ROE.WeaponHold mission.optionROT=ENUMS.ROT.PassiveDefense - + + mission.categories={AUFTRAG.Category.AIRCRAFT} + mission.DCStask=mission:GetDCSMissionTask() - + return mission end ---- Create an INTERCEPT mission. +--- **[AIR]** Create an INTERCEPT mission. -- @param #AUFTRAG self -- @param Wrapper.Positionable#POSITIONABLE Target The target to intercept. Can also be passed as simple @{Wrapper.Group#GROUP} or @{Wrapper.Unit#UNIT} object. -- @return #AUFTRAG self function AUFTRAG:NewINTERCEPT(Target) - + local mission=AUFTRAG:New(AUFTRAG.Type.INTERCEPT) - + mission:_TargetFromObject(Target) - + -- Mission options: - mission.missionTask=ENUMS.MissionTask.INTERCEPT - mission.missionFraction=0.1 + mission.missionTask=ENUMS.MissionTask.INTERCEPT + mission.missionFraction=0.1 mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.EvadeFire - + + mission.categories={AUFTRAG.Category.AIRCRAFT} + mission.DCStask=mission:GetDCSMissionTask() - + return mission end ---- Create a CAP mission. +--- **[AIR]** Create a CAP mission. -- @param #AUFTRAG self -- @param Core.Zone#ZONE_RADIUS ZoneCAP Circular CAP zone. Detected targets in this zone will be engaged. -- @param #number Altitude Altitude at which to orbit in feet. Default is 10,000 ft. @@ -122008,26 +129017,28 @@ function AUFTRAG:NewCAP(ZoneCAP, Altitude, Speed, Coordinate, Heading, Leg, Targ -- Create ORBIT first. local mission=AUFTRAG:NewORBIT(Coordinate or ZoneCAP:GetCoordinate(), Altitude or 10000, Speed, Heading, Leg) - + -- Mission type CAP. mission.type=AUFTRAG.Type.CAP mission:_SetLogID() - + -- DCS task parameters: mission.engageZone=ZoneCAP mission.engageTargetTypes=TargetTypes or {"Air"} -- Mission options: - mission.missionTask=ENUMS.MissionTask.CAP + mission.missionTask=ENUMS.MissionTask.CAP mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.EvadeFire - + + mission.categories={AUFTRAG.Category.AIRCRAFT} + mission.DCStask=mission:GetDCSMissionTask() - + return mission end ---- Create a CAS mission. +--- **[AIR]** Create a CAS mission. -- @param #AUFTRAG self -- @param Core.Zone#ZONE_RADIUS ZoneCAS Circular CAS zone. Detected targets in this zone will be engaged. -- @param #number Altitude Altitude at which to orbit. Default is 10,000 ft. @@ -122035,7 +129046,7 @@ end -- @param Core.Point#COORDINATE Coordinate Where to orbit. Default is the center of the CAS zone. -- @param #number Heading Heading of race-track pattern in degrees. If not specified, a simple circular orbit is performed. -- @param #number Leg Length of race-track in NM. If not specified, a simple circular orbit is performed. --- @param #table TargetTypes (Optional) Table of target types. Default {"Helicopters", "Ground Units", "Light armed ships"}. +-- @param #table TargetTypes (Optional) Table of target types. Default `{"Helicopters", "Ground Units", "Light armed ships"}`. -- @return #AUFTRAG self function AUFTRAG:NewCAS(ZoneCAS, Altitude, Speed, Coordinate, Heading, Leg, TargetTypes) @@ -122048,26 +129059,67 @@ function AUFTRAG:NewCAS(ZoneCAS, Altitude, Speed, Coordinate, Heading, Leg, Targ -- Create ORBIT first. local mission=AUFTRAG:NewORBIT(Coordinate or ZoneCAS:GetCoordinate(), Altitude or 10000, Speed, Heading, Leg) - + -- Mission type CAS. mission.type=AUFTRAG.Type.CAS mission:_SetLogID() - + -- DCS Task options: mission.engageZone=ZoneCAS mission.engageTargetTypes=TargetTypes or {"Helicopters", "Ground Units", "Light armed ships"} - + -- Mission options: - mission.missionTask=ENUMS.MissionTask.CAS + mission.missionTask=ENUMS.MissionTask.CAS mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.EvadeFire + mission.categories={AUFTRAG.Category.AIRCRAFT} + mission.DCStask=mission:GetDCSMissionTask() - + return mission end ---- Create a FACA mission. +--- **[AIR]** Create a CASENHANCED mission. Group(s) will go to the zone and patrol it randomly. +-- @param #AUFTRAG self +-- @param Core.Zone#ZONE CasZone The CAS zone. +-- @param #number Altitude Altitude in feet. Only for airborne units. Default 2000 feet ASL. +-- @param #number Speed Speed in knots. +-- @param #number RangeMax Max range in NM. Only detected targets within this radius from the group will be engaged. Default is 25 NM. +-- @param Core.Set#SET_ZONE NoEngageZoneSet Set of zones in which targets are *not* engaged. Default is nowhere. +-- @param #table TargetTypes Types of target attributes that will be engaged. See [DCS enum attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes). Default `{"Helicopters", "Ground Units", "Light armed ships"}`. +-- @return #AUFTRAG self +function AUFTRAG:NewCASENHANCED(CasZone, Altitude, Speed, RangeMax, NoEngageZoneSet, TargetTypes) + + local mission=AUFTRAG:New(AUFTRAG.Type.CASENHANCED) + + -- Ensure we got a ZONE and not just the zone name. + if type(CasZone)=="string" then + CasZone=ZONE:New(CasZone) + end + + mission:_TargetFromObject(CasZone) + + mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.CASENHANCED) + + mission:SetEngageDetected(RangeMax, TargetTypes or {"Helicopters", "Ground Units", "Light armed ships"}, CasZone, NoEngageZoneSet) + + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionROT=ENUMS.ROT.EvadeFire + + mission.missionFraction=1.0 + mission.missionSpeed=Speed and UTILS.KnotsToKmph(Speed) or nil + mission.missionAltitude=Altitude and UTILS.FeetToMeters(Altitude) or nil + + mission.categories={AUFTRAG.Category.AIRCRAFT} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + + +--- **[AIR]** Create a FACA mission. -- @param #AUFTRAG self -- @param Wrapper.Group#GROUP Target Target group. Must be a GROUP object. -- @param #string Designation Designation of target. See `AI.Task.Designation`. Default `AI.Task.Designation.AUTO`. @@ -122080,15 +129132,15 @@ function AUFTRAG:NewFACA(Target, Designation, DataLink, Frequency, Modulation) local mission=AUFTRAG:New(AUFTRAG.Type.FACA) mission:_TargetFromObject(Target) - + -- TODO: check that target is really a group object! - + -- DCS Task options: mission.facDesignation=Designation --or AI.Task.Designation.AUTO mission.facDatalink=true mission.facFreq=Frequency or 133 mission.facModu=Modulation or radio.modulation.AM - + -- Mission options: mission.missionTask=ENUMS.MissionTask.AFAC mission.missionAltitude=nil @@ -122096,130 +129148,139 @@ function AUFTRAG:NewFACA(Target, Designation, DataLink, Frequency, Modulation) mission.optionROE=ENUMS.ROE.ReturnFire mission.optionROT=ENUMS.ROT.PassiveDefense + mission.categories={AUFTRAG.Category.AIRCRAFT} + mission.DCStask=mission:GetDCSMissionTask() - + return mission end ---- Create a BAI mission. +--- **[AIR]** Create a BAI mission. -- @param #AUFTRAG self -- @param Wrapper.Positionable#POSITIONABLE Target The target to attack. Can be a GROUP, UNIT or STATIC object. --- @param #number Altitude Engage altitude in feet. Default 2000 ft. +-- @param #number Altitude Engage altitude in feet. Default 5000 ft. -- @return #AUFTRAG self function AUFTRAG:NewBAI(Target, Altitude) - + local mission=AUFTRAG:New(AUFTRAG.Type.BAI) mission:_TargetFromObject(Target) - + -- DCS Task options: - mission.engageWeaponType=ENUMS.WeaponFlag.AnyAG + mission.engageWeaponType=ENUMS.WeaponFlag.Auto mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL - mission.engageAltitude=UTILS.FeetToMeters(Altitude or 2000) - + mission.engageAltitude=UTILS.FeetToMeters(Altitude or 5000) + -- Mission options: mission.missionTask=ENUMS.MissionTask.GROUNDATTACK mission.missionAltitude=mission.engageAltitude mission.missionFraction=0.75 mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.PassiveDefense - - mission.DCStask=mission:GetDCSMissionTask() - + + mission.categories={AUFTRAG.Category.AIRCRAFT} + + mission.DCStask=mission:GetDCSMissionTask() + return mission end ---- Create a SEAD mission. +--- **[AIR]** Create a SEAD mission. -- @param #AUFTRAG self -- @param Wrapper.Positionable#POSITIONABLE Target The target to attack. Can be a GROUP or UNIT object. --- @param #number Altitude Engage altitude in feet. Default 2000 ft. +-- @param #number Altitude Engage altitude in feet. Default 25000 ft. -- @return #AUFTRAG self function AUFTRAG:NewSEAD(Target, Altitude) - + local mission=AUFTRAG:New(AUFTRAG.Type.SEAD) mission:_TargetFromObject(Target) - + -- DCS Task options: - mission.engageWeaponType=ENUMS.WeaponFlag.AnyAG --ENUMS.WeaponFlag.Cannons + mission.engageWeaponType=ENUMS.WeaponFlag.Auto mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL - mission.engageAltitude=UTILS.FeetToMeters(Altitude or 2000) - + mission.engageAltitude=UTILS.FeetToMeters(Altitude or 25000) + -- Mission options: mission.missionTask=ENUMS.MissionTask.SEAD mission.missionAltitude=mission.engageAltitude mission.missionFraction=0.2 mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.EvadeFire - --mission.optionROT=ENUMS.ROT.AllowAbortMission - - mission.DCStask=mission:GetDCSMissionTask() - + + mission.categories={AUFTRAG.Category.AIRCRAFT} + + mission.DCStask=mission:GetDCSMissionTask() + return mission end ---- Create a STRIKE mission. Flight will attack the closest map object to the specified coordinate. +--- **[AIR]** Create a STRIKE mission. Flight will attack the closest map object to the specified coordinate. -- @param #AUFTRAG self --- @param Core.Point#COORDINATE Target The target coordinate. Can also be given as a GROUP, UNIT or STATIC object. +-- @param Core.Point#COORDINATE Target The target coordinate. Can also be given as a GROUP, UNIT, STATIC or TARGET object. -- @param #number Altitude Engage altitude in feet. Default 2000 ft. -- @return #AUFTRAG self function AUFTRAG:NewSTRIKE(Target, Altitude) local mission=AUFTRAG:New(AUFTRAG.Type.STRIKE) - + mission:_TargetFromObject(Target) - + -- DCS Task options: - mission.engageWeaponType=ENUMS.WeaponFlag.AnyAG + mission.engageWeaponType=ENUMS.WeaponFlag.Auto mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL - mission.engageAltitude=UTILS.FeetToMeters(Altitude or 2000) - + mission.engageAltitude=UTILS.FeetToMeters(Altitude or 2000) + -- Mission options: mission.missionTask=ENUMS.MissionTask.GROUNDATTACK mission.missionAltitude=mission.engageAltitude mission.missionFraction=0.75 mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.PassiveDefense - + + mission.categories={AUFTRAG.Category.AIRCRAFT} + mission.DCStask=mission:GetDCSMissionTask() - + return mission end ---- Create a BOMBING mission. Flight will drop bombs a specified coordinate. +--- **[AIR]** Create a BOMBING mission. Flight will drop bombs a specified coordinate. -- @param #AUFTRAG self --- @param Core.Point#COORDINATE Target Target coordinate. Can also be specified as a GROUP, UNIT or STATIC object. +-- @param Core.Point#COORDINATE Target Target coordinate. Can also be specified as a GROUP, UNIT, STATIC or TARGET object. -- @param #number Altitude Engage altitude in feet. Default 25000 ft. -- @return #AUFTRAG self function AUFTRAG:NewBOMBING(Target, Altitude) local mission=AUFTRAG:New(AUFTRAG.Type.BOMBING) - + mission:_TargetFromObject(Target) - + -- DCS task options: - mission.engageWeaponType=ENUMS.WeaponFlag.AnyBomb + mission.engageWeaponType=ENUMS.WeaponFlag.Auto mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL mission.engageAltitude=UTILS.FeetToMeters(Altitude or 25000) -- Mission options: mission.missionTask=ENUMS.MissionTask.GROUNDATTACK - mission.missionAltitude=mission.engageAltitude*0.8 + mission.missionAltitude=mission.engageAltitude*0.8 mission.missionFraction=0.5 mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.NoReaction -- No reaction is better. - + -- Evaluate result after 5 min. We might need time until the bombs have dropped and targets have been detroyed. mission.dTevaluate=5*60 - + + mission.categories={AUFTRAG.Category.AIRCRAFT} + -- Get DCS task. mission.DCStask=mission:GetDCSMissionTask() - + return mission end ---- Create a BOMBRUNWAY mission. +--- **[AIR]** Create a BOMBRUNWAY mission. -- @param #AUFTRAG self -- @param Wrapper.Airbase#AIRBASE Airdrome The airbase to bomb. This must be an airdrome (not a FARP or ship) as these to not have a runway. -- @param #number Altitude Engage altitude in feet. Default 25000 ft. @@ -122229,37 +129290,35 @@ function AUFTRAG:NewBOMBRUNWAY(Airdrome, Altitude) if type(Airdrome)=="string" then Airdrome=AIRBASE:FindByName(Airdrome) end - - if Airdrome:IsInstanceOf("AIRBASE") then - - end local mission=AUFTRAG:New(AUFTRAG.Type.BOMBRUNWAY) - - mission:_TargetFromObject(Airdrome) - + + mission:_TargetFromObject(Airdrome) + -- DCS task options: - mission.engageWeaponType=ENUMS.WeaponFlag.AnyBomb + mission.engageWeaponType=ENUMS.WeaponFlag.Auto mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL mission.engageAltitude=UTILS.FeetToMeters(Altitude or 25000) -- Mission options: mission.missionTask=ENUMS.MissionTask.RUNWAYATTACK - mission.missionAltitude=mission.engageAltitude*0.8 + mission.missionAltitude=mission.engageAltitude*0.8 mission.missionFraction=0.75 mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.PassiveDefense - + -- Evaluate result after 5 min. mission.dTevaluate=5*60 - + + mission.categories={AUFTRAG.Category.AIRCRAFT} + -- Get DCS task. mission.DCStask=mission:GetDCSMissionTask() - + return mission end ---- Create a CARPET BOMBING mission. +--- **[AIR]** Create a CARPET BOMBING mission. -- @param #AUFTRAG self -- @param Core.Point#COORDINATE Target Target coordinate. Can also be specified as a GROUP, UNIT or STATIC object. -- @param #number Altitude Engage altitude in feet. Default 25000 ft. @@ -122268,11 +129327,11 @@ end function AUFTRAG:NewBOMBCARPET(Target, Altitude, CarpetLength) local mission=AUFTRAG:New(AUFTRAG.Type.BOMBCARPET) - - mission:_TargetFromObject(Target) - + + mission:_TargetFromObject(Target) + -- DCS task options: - mission.engageWeaponType=ENUMS.WeaponFlag.AnyBomb + mission.engageWeaponType=ENUMS.WeaponFlag.Auto mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL mission.engageAltitude=UTILS.FeetToMeters(Altitude or 25000) mission.engageCarpetLength=CarpetLength or 500 @@ -122281,85 +129340,122 @@ function AUFTRAG:NewBOMBCARPET(Target, Altitude, CarpetLength) -- Mission options: mission.missionTask=ENUMS.MissionTask.GROUNDATTACK - mission.missionAltitude=mission.engageAltitude*0.8 + mission.missionAltitude=mission.engageAltitude*0.8 mission.missionFraction=0.5 mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.NoReaction - + -- Evaluate result after 5 min. mission.dTevaluate=5*60 - + + mission.categories={AUFTRAG.Category.AIRCRAFT} + -- Get DCS task. mission.DCStask=mission:GetDCSMissionTask() - + return mission end ---- Create an ESCORT (or FOLLOW) mission. Flight will escort another group and automatically engage certain target types. +--- **[AIR]** Create an ESCORT (or FOLLOW) mission. Flight will escort another group and automatically engage certain target types. -- @param #AUFTRAG self -- @param Wrapper.Group#GROUP EscortGroup The group to escort. --- @param DCS#Vec3 OffsetVector A table with x, y and z components specifying the offset of the flight to the escorted group. Default {x=-100, y=0, z=200} for z=200 meters to the right, same alitude, x=100 meters behind. --- @param #number EngageMaxDistance Max engage distance of targets in nautical miles. Default auto (*nil*). +-- @param DCS#Vec3 OffsetVector A table with x, y and z components specifying the offset of the flight to the escorted group. Default {x=-100, y=0, z=200} for z=200 meters to the right, same alitude (y=0), x=-100 meters behind. +-- @param #number EngageMaxDistance Max engage distance of targets in nautical miles. Default auto 32 NM. -- @param #table TargetTypes Types of targets to engage automatically. Default is {"Air"}, i.e. all enemy airborne units. Use an empty set {} for a simple "FOLLOW" mission. -- @return #AUFTRAG self function AUFTRAG:NewESCORT(EscortGroup, OffsetVector, EngageMaxDistance, TargetTypes) local mission=AUFTRAG:New(AUFTRAG.Type.ESCORT) - - mission:_TargetFromObject(EscortGroup) - + + -- If only a string is passed we set a variable and check later if the group exists. + if type(EscortGroup)=="string" then + mission.escortGroupName=EscortGroup + mission:_TargetFromObject() + else + mission:_TargetFromObject(EscortGroup) + end + -- DCS task parameters: mission.escortVec3=OffsetVector or {x=-100, y=0, z=200} - mission.engageMaxDistance=EngageMaxDistance and UTILS.NMToMeters(EngageMaxDistance) or nil + mission.engageMaxDistance=EngageMaxDistance and UTILS.NMToMeters(EngageMaxDistance) or UTILS.NMToMeters(32) mission.engageTargetTypes=TargetTypes or {"Air"} - + -- Mission options: - mission.missionTask=ENUMS.MissionTask.ESCORT + mission.missionTask=ENUMS.MissionTask.ESCORT mission.missionFraction=0.1 mission.missionAltitude=1000 mission.optionROE=ENUMS.ROE.OpenFire -- TODO: what's the best ROE here? Make dependent on ESCORT or FOLLOW! mission.optionROT=ENUMS.ROT.PassiveDefense - + + mission.categories={AUFTRAG.Category.AIRCRAFT} + mission.DCStask=mission:GetDCSMissionTask() - + return mission end ---- Create a RESCUE HELO mission. +--- **[AIR ROTARY]** Create a RESCUE HELO mission. -- @param #AUFTRAG self -- @param Wrapper.Unit#UNIT Carrier The carrier unit. -- @return #AUFTRAG self function AUFTRAG:NewRESCUEHELO(Carrier) local mission=AUFTRAG:New(AUFTRAG.Type.RESCUEHELO) - - --mission.carrier=Carrier - + mission:_TargetFromObject(Carrier) - + -- Mission options: mission.missionTask=ENUMS.MissionTask.NOTHING mission.missionFraction=0.5 mission.optionROE=ENUMS.ROE.WeaponHold mission.optionROT=ENUMS.ROT.NoReaction - + + mission.categories={AUFTRAG.Category.HELICOPTER} + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[AIRPANE]** Create a RECOVERY TANKER mission. **WIP and not working coorectly yet!** +-- @param #AUFTRAG self +-- @param Wrapper.Unit#UNIT Carrier The carrier unit. +-- @return #AUFTRAG self +function AUFTRAG:NewRECOVERYTANKER(Carrier) + + local mission=AUFTRAG:New(AUFTRAG.Type.RECOVERYTANKER) + + mission:_TargetFromObject(Carrier) + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.REFUELING + mission.missionFraction=0.5 + mission.optionROE=ENUMS.ROE.WeaponHold + mission.optionROT=ENUMS.ROT.NoReaction + mission.missionAltitude=UTILS.FeetToMeters(6000) + mission.missionSpeed=UTILS.KnotsToKmph(274) + + mission.categories={AUFTRAG.Category.AIRPLANE} + + mission.DCStask=mission:GetDCSMissionTask() + return mission end ---- Create a TROOP TRANSPORT mission. +--- **[AIR ROTARY, GROUND]** Create a TROOP TRANSPORT mission. -- @param #AUFTRAG self -- @param Core.Set#SET_GROUP TransportGroupSet The set group(s) to be transported. -- @param Core.Point#COORDINATE DropoffCoordinate Coordinate where the helo will land drop off the the troops. --- @param Core.Point#COORDINATE PickupCoordinate Coordinate where the helo will land to pick up the the cargo. Default is the fist transport group. +-- @param Core.Point#COORDINATE PickupCoordinate Coordinate where the helo will land to pick up the the cargo. Default is the first transport group. +-- @param #number PickupRadius Radius around the pickup coordinate in meters. Default 100 m. -- @return #AUFTRAG self -function AUFTRAG:NewTROOPTRANSPORT(TransportGroupSet, DropoffCoordinate, PickupCoordinate) +function AUFTRAG:NewTROOPTRANSPORT(TransportGroupSet, DropoffCoordinate, PickupCoordinate, PickupRadius) local mission=AUFTRAG:New(AUFTRAG.Type.TROOPTRANSPORT) - + if TransportGroupSet:IsInstanceOf("GROUP") then mission.transportGroupSet=SET_GROUP:New() mission.transportGroupSet:AddGroup(TransportGroupSet) @@ -122369,97 +129465,567 @@ function AUFTRAG:NewTROOPTRANSPORT(TransportGroupSet, DropoffCoordinate, PickupC mission:E(mission.lid.."ERROR: TransportGroupSet must be a GROUP or SET_GROUP object!") return nil end - + mission:_TargetFromObject(mission.transportGroupSet) - - mission.transportPickup=PickupCoordinate or mission:GetTargetCoordinate() + + mission.transportPickup=PickupCoordinate or mission:GetTargetCoordinate() mission.transportDropoff=DropoffCoordinate - + + mission.transportPickupRadius=PickupRadius or 100 + + mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.TROOPTRANSPORT) + -- Debug. - mission.transportPickup:MarkToAll("Pickup") - mission.transportDropoff:MarkToAll("Drop off") + --mission.transportPickup:MarkToAll("Pickup Transport") + --mission.transportDropoff:MarkToAll("Drop off") -- TODO: what's the best ROE here? mission.optionROE=ENUMS.ROE.ReturnFire mission.optionROT=ENUMS.ROT.PassiveDefense - + + mission.categories={AUFTRAG.Category.HELICOPTER, AUFTRAG.Category.GROUND} + mission.DCStask=mission:GetDCSMissionTask() return mission end ---- Create an ARTY mission. +--- **[AIR ROTARY]** Create a CARGO TRANSPORT mission. +-- **Important Note:** +-- The dropoff zone has to be a zone defined in the Mission Editor. This is due to a restriction in the used DCS task, which takes the zone ID as input. +-- Only ME zones have an ID that can be referenced. +-- @param #AUFTRAG self +-- @param Wrapper.Static#STATIC StaticCargo Static cargo object. +-- @param Core.Zone#ZONE DropZone Zone where to drop off the cargo. **Has to be a zone defined in the ME!** +-- @return #AUFTRAG self +function AUFTRAG:NewCARGOTRANSPORT(StaticCargo, DropZone) + + local mission=AUFTRAG:New(AUFTRAG.Type.CARGOTRANSPORT) + + mission:_TargetFromObject(StaticCargo) + + mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.CARGOTRANSPORT) + + -- Set ROE and ROT. + mission.optionROE=ENUMS.ROE.ReturnFire + mission.optionROT=ENUMS.ROT.PassiveDefense + + mission.categories={AUFTRAG.Category.HELICOPTER} + + mission.DCStask=mission:GetDCSMissionTask() + + mission.DCStask.params.groupId=StaticCargo:GetID() + mission.DCStask.params.zoneId=DropZone.ZoneID + mission.DCStask.params.zone=DropZone + mission.DCStask.params.cargo=StaticCargo + + return mission +end + +--[[ + +--- **[AIR, GROUND, NAVAL]** Create a OPS TRANSPORT mission. +-- @param #AUFTRAG self +-- @param Core.Set#SET_GROUP CargoGroupSet The set group(s) to be transported. +-- @param Core.Zone#ZONE PickupZone Pick up zone +-- @param Core.Zone#ZONE DeployZone Deploy zone +-- @return #AUFTRAG self +function AUFTRAG:NewOPSTRANSPORT(CargoGroupSet, PickupZone, DeployZone) + + local mission=AUFTRAG:New(AUFTRAG.Type.OPSTRANSPORT) + + mission.transportGroupSet=CargoGroupSet + + mission:_TargetFromObject(mission.transportGroupSet) + + mission.opstransport=OPSTRANSPORT:New(CargoGroupSet, PickupZone, DeployZone) + + function mission.opstransport:OnAfterExecuting(From, Event, To) + mission:Executing() + end + + function mission.opstransport:OnAfterDelivered(From, Event, To) + mission:Done() + end + + -- TODO: what's the best ROE here? + mission.optionROE=ENUMS.ROE.ReturnFire + mission.optionROT=ENUMS.ROT.PassiveDefense + + mission.categories={AUFTRAG.Category.ALL} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +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 -function AUFTRAG:NewARTY(Target, Nshots, Radius) +function AUFTRAG:NewARTY(Target, Nshots, Radius, Altitude) local mission=AUFTRAG:New(AUFTRAG.Type.ARTY) - + mission:_TargetFromObject(Target) - - mission.artyShots=Nshots or 3 + + mission.artyShots=Nshots or nil mission.artyRadius=Radius or 100 - + mission.artyAltitude=Altitude + mission.engageWeaponType=ENUMS.WeaponFlag.Auto - + mission.optionROE=ENUMS.ROE.OpenFire -- Ground/naval need open fire! mission.optionAlarm=0 - + mission.missionFraction=0.0 - + -- Evaluate after 8 min. mission.dTevaluate=8*60 - + + mission.categories={AUFTRAG.Category.GROUND, AUFTRAG.Category.NAVAL} + mission.DCStask=mission:GetDCSMissionTask() return mission end ---- Create a PATROLZONE mission. Group(s) will go to the zone and patrol it randomly. +--- **[GROUND, NAVAL]** Create an BARRAGE mission. Assigned groups will move to a random coordinate within a given zone and start firing into the air. +-- @param #AUFTRAG self +-- @param Core.Zone#ZONE Zone The zone where the unit will go. +-- @param #number Heading Heading in degrees. Default random heading [0, 360). +-- @param #number Angle Shooting angle in degrees. Default random [45, 85]. +-- @param #number Radius Radius of the shells in meters. Default 100 meters. +-- @param #number Altitude Altitude in meters. Default 500 m. +-- @param #number Nshots Number of shots to be fired. Default is until ammo is empty (`#nil`). +-- @return #AUFTRAG self +function AUFTRAG:NewBARRAGE(Zone, Heading, Angle, Radius, Altitude, Nshots) + + local mission=AUFTRAG:New(AUFTRAG.Type.BARRAGE) + + mission:_TargetFromObject(Zone) + + mission.artyShots=Nshots + mission.artyRadius=Radius or 100 + mission.artyAltitude=Altitude + mission.artyHeading=Heading + mission.artyAngle=Angle + + mission.engageWeaponType=ENUMS.WeaponFlag.Auto + + mission.optionROE=ENUMS.ROE.OpenFire -- Ground/naval need open fire! + mission.optionAlarm=0 + + mission.missionFraction=0.0 + + -- Evaluate after instantly. + mission.dTevaluate=10 + + mission.categories={AUFTRAG.Category.GROUND, AUFTRAG.Category.NAVAL} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[AIR, GROUND, NAVAL]** Create a PATROLZONE mission. Group(s) will go to the zone and patrol it randomly. -- @param #AUFTRAG self -- @param Core.Zone#ZONE Zone The patrol zone. -- @param #number Speed Speed in knots. -- @param #number Altitude Altitude in feet. Only for airborne units. Default 2000 feet ASL. +-- @param #string Formation Formation used by ground units during patrol. Default "Off Road". -- @return #AUFTRAG self -function AUFTRAG:NewPATROLZONE(Zone, Speed, Altitude) +function AUFTRAG:NewPATROLZONE(Zone, Speed, Altitude, Formation) local mission=AUFTRAG:New(AUFTRAG.Type.PATROLZONE) - + + -- Ensure we got a ZONE and not just the zone name. + if type(Zone)=="string" then + Zone=ZONE:New(Zone) + end + mission:_TargetFromObject(Zone) - + + mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.PATROLZONE) + mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.PassiveDefense mission.optionAlarm=ENUMS.AlarmState.Auto - - mission.missionFraction=1.0 + + mission.missionFraction=1.0 mission.missionSpeed=Speed and UTILS.KnotsToKmph(Speed) or nil mission.missionAltitude=Altitude and UTILS.FeetToMeters(Altitude) or nil + + mission.categories={AUFTRAG.Category.ALL} + + mission.DCStask=mission:GetDCSMissionTask() + + mission.DCStask.params.formation=Formation or "Off Road" + + return mission +end + + +--- **[OBSOLETE]** Create a ARMORATTACK mission. +-- ** Note that this is actually creating a GROUNDATTACK mission!** +-- @param #AUFTRAG self +-- @param Ops.Target#TARGET Target The target to attack. Can be a GROUP, UNIT or STATIC object. +-- @param #number Speed Speed in knots. +-- @param #string Formation The attack formation, e.g. "Wedge", "Vee" etc. +-- @return #AUFTRAG self +function AUFTRAG:NewARMORATTACK(Target, Speed, Formation) + + local mission=AUFTRAG:NewGROUNDATTACK(Target, Speed, Formation) + -- Mission type. + mission.type=AUFTRAG.Type.ARMORATTACK + + return mission +end + +--- **[GROUND]** Create a GROUNDATTACK mission. Ground group(s) will go to a target object and attack. +-- @param #AUFTRAG self +-- @param Wrapper.Positionable#POSITIONABLE Target The target to attack. Can be a GROUP, UNIT or STATIC object. +-- @param #number Speed Speed in knots. Default max. +-- @param #string Formation The attack formation, e.g. "Wedge", "Vee" etc. Default `ENUMS.Formation.Vehicle.Vee`. +-- @return #AUFTRAG self +function AUFTRAG:NewGROUNDATTACK(Target, Speed, Formation) + + local mission=AUFTRAG:New(AUFTRAG.Type.GROUNDATTACK) + + mission:_TargetFromObject(Target) + + mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.GROUNDATTACK) + + -- Defaults. + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionAlarm=ENUMS.AlarmState.Auto + mission.optionFormation="On Road" + mission.missionFraction=0.70 + mission.missionSpeed=Speed and UTILS.KnotsToKmph(Speed) or nil + + mission.categories={AUFTRAG.Category.GROUND} + + mission.DCStask=mission:GetDCSMissionTask() + + mission.DCStask.params.speed=Speed + mission.DCStask.params.formation=Formation or ENUMS.Formation.Vehicle.Vee + + return mission +end + +--- **[AIR, GROUND, NAVAL]** Create a RECON mission. +-- @param #AUFTRAG self +-- @param Core.Set#SET_ZONE ZoneSet The recon zones. +-- @param #number Speed Speed in knots. +-- @param #number Altitude Altitude in feet. Only for airborne units. Default 2000 feet ASL. +-- @param #boolean Adinfinitum If `true`, the group will start over again after reaching the final zone. +-- @param #boolean Randomly If `true`, the group will select a random zone. +-- @param #string Formation Formation used during recon route. +-- @return #AUFTRAG self +function AUFTRAG:NewRECON(ZoneSet, Speed, Altitude, Adinfinitum, Randomly, Formation) + + local mission=AUFTRAG:New(AUFTRAG.Type.RECON) + + mission:_TargetFromObject(ZoneSet) + + mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.RECON) + + mission.optionROE=ENUMS.ROE.WeaponHold + mission.optionROT=ENUMS.ROT.PassiveDefense + mission.optionAlarm=ENUMS.AlarmState.Auto + + mission.missionFraction=0.5 + mission.missionSpeed=Speed and UTILS.KnotsToKmph(Speed) or nil + mission.missionAltitude=Altitude and UTILS.FeetToMeters(Altitude) or UTILS.FeetToMeters(2000) + + mission.categories={AUFTRAG.Category.ALL} + + mission.DCStask=mission:GetDCSMissionTask() + mission.DCStask.params.adinfinitum=Adinfinitum + mission.DCStask.params.randomly=Randomly + mission.DCStask.params.formation=Formation + + return mission +end + +--- **[GROUND]** Create a AMMO SUPPLY mission. +-- @param #AUFTRAG self +-- @param Core.Zone#ZONE Zone The zone, where supply units go. +-- @return #AUFTRAG self +function AUFTRAG:NewAMMOSUPPLY(Zone) + + local mission=AUFTRAG:New(AUFTRAG.Type.AMMOSUPPLY) + + 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 + +--- **[GROUND]** Create a FUEL SUPPLY mission. +-- @param #AUFTRAG self +-- @param Core.Zone#ZONE Zone The zone, where supply units go. +-- @return #AUFTRAG self +function AUFTRAG:NewFUELSUPPLY(Zone) + + local mission=AUFTRAG:New(AUFTRAG.Type.FUELSUPPLY) + + mission:_TargetFromObject(Zone) + + mission.optionROE=ENUMS.ROE.WeaponHold + mission.optionAlarm=ENUMS.AlarmState.Auto + + mission.missionFraction=1.0 + + mission.categories={AUFTRAG.Category.GROUND} + + mission.DCStask=mission:GetDCSMissionTask() + + 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 ---- Create a mission to attack a group. Mission type is automatically chosen from the group category. +--- **[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. +-- @param #AUFTRAG self +-- @param #string MissionType Mission type `AUFTRAG.Type.XXX`. Determines payload and mission task (intercept, ground attack, etc.). +-- @return #AUFTRAG self +function AUFTRAG:NewALERT5(MissionType) + + local mission=AUFTRAG:New(AUFTRAG.Type.ALERT5) + + mission.missionTask=self:GetMissionTaskforMissionType(MissionType) + + mission.optionROE=ENUMS.ROE.WeaponHold + mission.optionROT=ENUMS.ROT.NoReaction + + mission.alert5MissionType=MissionType + + mission.missionFraction=1.0 + + mission.categories={AUFTRAG.Category.AIRCRAFT} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[GROUND, NAVAL]** Create an ON GUARD mission. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Coordinate Coordinate, where to stand guard. +-- @return #AUFTRAG self +function AUFTRAG:NewONGUARD(Coordinate) + + local mission=AUFTRAG:New(AUFTRAG.Type.ONGUARD) + + mission:_TargetFromObject(Coordinate) + + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionAlarm=ENUMS.AlarmState.Auto + + mission.missionFraction=1.0 + + mission.categories={AUFTRAG.Category.GROUND, AUFTRAG.Category.NAVAL} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[GROUND, NAVAL]** Create an AIRDEFENSE mission. +-- @param #AUFTRAG self +-- @param Core.Zone#ZONE Zone Zone where the air defense group(s) should be stationed. +-- @return #AUFTRAG self +function AUFTRAG:NewAIRDEFENSE(Zone) + + local mission=AUFTRAG:New(AUFTRAG.Type.AIRDEFENSE) + + mission:_TargetFromObject(Zone) + + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionAlarm=ENUMS.AlarmState.Auto + + mission.missionFraction=1.0 + + mission.categories={AUFTRAG.Category.GROUND, AUFTRAG.Category.NAVAL} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[GROUND]** Create an EWR mission. +-- @param #AUFTRAG self +-- @param Core.Zone#ZONE Zone Zone where the Early Warning Radar group(s) should be stationed. +-- @return #AUFTRAG self +function AUFTRAG:NewEWR(Zone) + + local mission=AUFTRAG:New(AUFTRAG.Type.EWR) + + mission:_TargetFromObject(Zone) + + mission.optionROE=ENUMS.ROE.WeaponHold + mission.optionAlarm=ENUMS.AlarmState.Auto + + mission.missionFraction=1.0 + + mission.categories={AUFTRAG.Category.GROUND} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + + +--- **[PRIVATE, AIR, GROUND, NAVAL]** Create a mission to relocate all cohort assets to another LEGION. +-- @param #AUFTRAG self +-- @param Ops.Legion#LEGION Legion The new legion. +-- @param Ops.Cohort#COHORT Cohort The cohort to be relocated. +-- @return #AUFTRAG self +function AUFTRAG:_NewRELOCATECOHORT(Legion, Cohort) + + local mission=AUFTRAG:New(AUFTRAG.Type.RELOCATECOHORT) + + mission:_TargetFromObject(Legion.spawnzone) + + mission.optionROE=ENUMS.ROE.ReturnFire + mission.optionAlarm=ENUMS.AlarmState.Auto + + mission.missionFraction=0.0 + + mission.categories={AUFTRAG.Category.ALL} + + mission.DCStask=mission:GetDCSMissionTask() + + if Cohort.isGround then + mission.optionFormation=ENUMS.Formation.Vehicle.OnRoad + end + + mission.DCStask.params.legion=Legion + mission.DCStask.params.cohort=Cohort + + return mission +end + +--- **[GROUND, NAVAL]** Create a mission to do NOTHING. +-- @param #AUFTRAG self +-- @param Core.Zone#ZONE RelaxZone Zone where the assets are supposed to do nothing. +-- @return #AUFTRAG self +function AUFTRAG:NewNOTHING(RelaxZone) + + local mission=AUFTRAG:New(AUFTRAG.Type.NOTHING) + + mission:_TargetFromObject(RelaxZone) + + mission.optionROE=ENUMS.ROE.WeaponHold + mission.optionAlarm=ENUMS.AlarmState.Auto + + mission.missionFraction=1.0 + + mission.categories={AUFTRAG.Category.GROUND, AUFTRAG.Category.NAVAL} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[GROUND]** Create an ARMORED ON GUARD mission. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Coordinate Coordinate, where to stand guard. +-- @param #string Formation Formation to take, e.g. "on Road", "Vee" etc. +-- @return #AUFTRAG self +function AUFTRAG:NewARMOREDGUARD(Coordinate,Formation) + + local mission=AUFTRAG:New(AUFTRAG.Type.ARMOREDGUARD) + + mission:_TargetFromObject(Coordinate) + + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionAlarm=ENUMS.AlarmState.Auto + mission.optionFormation=Formation or "On Road" + + mission.missionFraction=1.0 + + mission.categories={AUFTRAG.Category.GROUND} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- Create a mission to attack a TARGET object. -- @param #AUFTRAG self -- @param Ops.Target#TARGET Target The target. +-- @param #string MissionType The mission type. -- @return #AUFTRAG self -function AUFTRAG:NewTargetAir(Target) +function AUFTRAG:NewFromTarget(Target, MissionType) local mission=nil --#AUFTRAG - - self.engageTarget=Target - - local target=self.engageTarget:GetObject() - - local mission=self:NewAUTO(target) - - if mission then - mission:SetPriority(10, true) + + if MissionType==AUFTRAG.Type.ANTISHIP then + mission=self:NewANTISHIP(Target, Altitude) + elseif MissionType==AUFTRAG.Type.ARTY then + mission=self:NewARTY(Target, Nshots, Radius) + elseif MissionType==AUFTRAG.Type.BAI then + mission=self:NewBAI(Target, Altitude) + elseif MissionType==AUFTRAG.Type.BOMBCARPET then + mission=self:NewBOMBCARPET(Target, Altitude, CarpetLength) + elseif MissionType==AUFTRAG.Type.BOMBING then + mission=self:NewBOMBING(Target, Altitude) + elseif MissionType==AUFTRAG.Type.BOMBRUNWAY then + mission=self:NewBOMBRUNWAY(Target, Altitude) + elseif MissionType==AUFTRAG.Type.INTERCEPT then + mission=self:NewINTERCEPT(Target) + elseif MissionType==AUFTRAG.Type.SEAD then + mission=self:NewSEAD(Target, Altitude) + elseif MissionType==AUFTRAG.Type.STRIKE then + mission=self:NewSTRIKE(Target, Altitude) + elseif MissionType==AUFTRAG.Type.ARMORATTACK then + mission=self:NewARMORATTACK(Target, Speed) + elseif MissionType==AUFTRAG.Type.GROUNDATTACK then + mission=self:NewGROUNDATTACK(Target, Speed, Formation) + else + return nil end return mission @@ -122479,7 +130045,7 @@ function AUFTRAG:_DetermineAuftragType(Target) local auftrag=nil if Target:IsInstanceOf("GROUP") then - group=Target --Target is already a group. + group=Target --Target is already a group. elseif Target:IsInstanceOf("UNIT") then group=Target:GetGroup() elseif Target:IsInstanceOf("AIRBASE") then @@ -122487,65 +130053,69 @@ function AUFTRAG:_DetermineAuftragType(Target) elseif Target:IsInstanceOf("SCENERY") then scenery=Target end - + if group then local category=group:GetCategory() local attribute=group:GetAttribute() if category==Group.Category.AIRPLANE or category==Group.Category.HELICOPTER then - + --- -- A2A: Intercept --- - + auftrag=AUFTRAG.Type.INTERCEPT - + elseif category==Group.Category.GROUND or category==Group.Category.TRAIN then - + --- -- GROUND --- if attribute==GROUP.Attribute.GROUND_SAM then - + -- SEAD/DEAD - + auftrag=AUFTRAG.Type.SEAD - + elseif attribute==GROUP.Attribute.GROUND_AAA then - + auftrag=AUFTRAG.Type.BAI - + elseif attribute==GROUP.Attribute.GROUND_ARTILLERY then - + auftrag=AUFTRAG.Type.BAI - + elseif attribute==GROUP.Attribute.GROUND_INFANTRY then - + + auftrag=AUFTRAG.Type.CAS + + elseif attribute==GROUP.Attribute.GROUND_TANK then + auftrag=AUFTRAG.Type.BAI - + else auftrag=AUFTRAG.Type.BAI - + end - + elseif category==Group.Category.SHIP then - + --- -- NAVAL --- - + auftrag=AUFTRAG.Type.ANTISHIP - + else - self:E(self.lid.."ERROR: Unknown Group category!") + self:T(self.lid.."ERROR: Unknown Group category!") end - + elseif airbase then - auftrag=AUFTRAG.Type.BOMBRUNWAY + auftrag=AUFTRAG.Type.BOMBRUNWAY elseif scenery then auftrag=AUFTRAG.Type.STRIKE elseif coordinate then @@ -122562,11 +130132,11 @@ end function AUFTRAG:NewAUTO(EngageGroup) local mission=nil --#AUFTRAG - + local Target=EngageGroup local auftrag=self:_DetermineAuftragType(EngageGroup) - + if auftrag==AUFTRAG.Type.ANTISHIP then mission=AUFTRAG:NewANTISHIP(Target) elseif auftrag==AUFTRAG.Type.ARTY then @@ -122584,13 +130154,13 @@ function AUFTRAG:NewAUTO(EngageGroup) elseif auftrag==AUFTRAG.Type.CAP then mission=AUFTRAG:NewCAP(ZoneCAP,Altitude,Speed,Coordinate,Heading,Leg,TargetTypes) elseif auftrag==AUFTRAG.Type.CAS then - mission=AUFTRAG:NewCAS(ZoneCAS,Altitude,Speed,Coordinate,Heading,Leg,TargetTypes) + mission=AUFTRAG:NewCAS(ZoneCAS,Altitude,Speed,Coordinate,Heading,Leg,TargetTypes) elseif auftrag==AUFTRAG.Type.ESCORT then - mission=AUFTRAG:NewESCORT(EscortGroup,OffsetVector,EngageMaxDistance,TargetTypes) + mission=AUFTRAG:NewESCORT(EscortGroup,OffsetVector,EngageMaxDistance,TargetTypes) elseif auftrag==AUFTRAG.Type.FACA then mission=AUFTRAG:NewFACA(Target,Designation,DataLink,Frequency,Modulation) elseif auftrag==AUFTRAG.Type.FERRY then - -- Not implemented yet. + -- Not implemented yet. elseif auftrag==AUFTRAG.Type.GCICAP then mission=AUFTRAG:NewGCICAP(Coordinate,Altitude,Speed,Heading,Leg) elseif auftrag==AUFTRAG.Type.INTERCEPT then @@ -122598,7 +130168,7 @@ function AUFTRAG:NewAUTO(EngageGroup) elseif auftrag==AUFTRAG.Type.ORBIT then mission=AUFTRAG:NewORBIT(Coordinate,Altitude,Speed,Heading,Leg) elseif auftrag==AUFTRAG.Type.RECON then - -- Not implemented yet. + -- Not implemented yet. elseif auftrag==AUFTRAG.Type.RESCUEHELO then mission=AUFTRAG:NewRESCUEHELO(Carrier) elseif auftrag==AUFTRAG.Type.SEAD then @@ -122610,9 +130180,9 @@ function AUFTRAG:NewAUTO(EngageGroup) elseif auftrag==AUFTRAG.Type.TROOPTRANSPORT then mission=AUFTRAG:NewTROOPTRANSPORT(TransportGroupSet,DropoffCoordinate,PickupCoordinate) else - + end - + if mission then mission:SetPriority(10, true) end @@ -122633,7 +130203,7 @@ function AUFTRAG: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 @@ -122649,18 +130219,57 @@ function AUFTRAG:SetTime(ClockStart, 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 + end return self end ---- Set mission priority and (optional) urgency. Urgent missions can cancel other running missions. +--- Set time how long the mission is executed. Once this time limit has passed, the mission is cancelled. +-- @param #AUFTRAG self +-- @param #number Duration Duration in seconds. +-- @return #AUFTRAG self +function AUFTRAG:SetDuration(Duration) + self.durationExe=Duration + return self +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(Switch) + if Switch==nil then + Switch=true + end + self.teleport=Switch + return self +end + + +--- Set mission push time. This is the time the mission is executed. If the push time is not passed, the group will wait at the mission execution waypoint. +-- @param #AUFTRAG self +-- @param #string ClockPush Time the mission is executed, e.g. "05:00" for 5 am. Can also be given as a `#number`, where it is interpreted as relative push time in seconds. +-- @return #AUFTRAG self +function AUFTRAG:SetPushTime(ClockPush) + + if ClockPush then + if type(ClockPush)=="string" then + self.Tpush=UTILS.ClockToSeconds(ClockPush) + elseif type(ClockPush)=="number" then + self.Tpush=timer.getAbsTime()+ClockPush + end + end + + return self +end + +--- Set mission priority and (optional) urgency. Urgent missions can cancel other running missions. -- @param #AUFTRAG self -- @param #number Prio Priority 1=high, 100=low. Default 50. -- @param #boolean Urgent If *true*, another running mission might be cancelled if it has a lower priority. @@ -122673,7 +130282,7 @@ function AUFTRAG:SetPriority(Prio, Urgent, Importance) return self end ---- Set how many times the mission is repeated. Only valid if the mission is handled by an AIRWING or higher level. +--- **[LEGION, COMMANDER, CHIEF]** Set how many times the mission is repeated. Only valid if the mission is handled by a LEGION (AIRWING, BRIGADE, FLEET) or higher level. -- @param #AUFTRAG self -- @param #number Nrepeat Number of repeats. Default 0. -- @return #AUFTRAG self @@ -122682,7 +130291,7 @@ function AUFTRAG:SetRepeat(Nrepeat) return self end ---- Set how many times the mission is repeated if it fails. Only valid if the mission is handled by an AIRWING or higher level. +--- **[LEGION, COMMANDER, CHIEF]** Set how many times the mission is repeated if it fails. Only valid if the mission is handled by a LEGION (AIRWING, BRIGADE, FLEET) or higher level. -- @param #AUFTRAG self -- @param #number Nrepeat Number of repeats. Default 0. -- @return #AUFTRAG self @@ -122691,7 +130300,7 @@ function AUFTRAG:SetRepeatOnFailure(Nrepeat) return self end ---- Set how many times the mission is repeated if it was successful. Only valid if the mission is handled by an AIRWING or higher level. +--- **[LEGION, COMMANDER, CHIEF]** Set how many times the mission is repeated if it was successful. Only valid if the mission is handled by a LEGION (AIRWING, BRIGADE, FLEET) or higher level. -- @param #AUFTRAG self -- @param #number Nrepeat Number of repeats. Default 0. -- @return #AUFTRAG self @@ -122700,12 +130309,93 @@ function AUFTRAG:SetRepeatOnSuccess(Nrepeat) return self end ---- Define how many assets are required to do the job. Only valid if the mission is handled by an AIRWING or higher level. +--- **[LEGION, COMMANDER, CHIEF]** Define how many assets are required to do the job. Only used if the mission is handled by a **LEGION** (AIRWING, BRIGADE, ...) or higher level. -- @param #AUFTRAG self --- @param #number Nassets Number of asset groups. Default 1. +-- @param #number NassetsMin Minimum number of asset groups. Default 1. +-- @param #number NassetsMax Maximum Number of asset groups. Default is same as `NassetsMin`. -- @return #AUFTRAG self -function AUFTRAG:SetRequiredAssets(Nassets) - self.nassets=Nassets or 1 +function AUFTRAG:SetRequiredAssets(NassetsMin, NassetsMax) + + self.NassetsMin=NassetsMin or 1 + + self.NassetsMax=NassetsMax or self.NassetsMin + + -- Ensure that max is at least equal to min. + if self.NassetsMaxself.Tstop or false then + if self.Tstop and Tnow>self.Tstop then return false end - + -- All start conditions true? local startme=self:EvalConditionsAll(self.conditionStart) - + if not startme then return false end - + -- We're good to go! return true @@ -123212,34 +131328,54 @@ end -- @param #AUFTRAG self -- @return #boolean If true, mission should be cancelled. function AUFTRAG:IsReadyToCancel() - + local Tnow=timer.getAbsTime() -- Stop time already passed. - if self.Tstop and Tnow>self.Tstop then + if self.Tstop and Tnow>=self.Tstop then return true end -- Evaluate failure condition. One is enough. local failure=self:EvalConditionsAny(self.conditionFailure) - + if failure then self.failurecondition=true return true - end - + end + -- Evaluate success consitions. One is enough. local success=self:EvalConditionsAny(self.conditionSuccess) - + if success then self.successcondition=true return true end - + -- No criterion matched. return false end +--- Check if mission is ready to be pushed. +-- * Mission push time already passed. +-- * **All** push conditions are true. +-- @param #AUFTRAG self +-- @return #boolean If true, mission groups can push. +function AUFTRAG:IsReadyToPush() + + local Tnow=timer.getAbsTime() + + -- Push time passed? + if self.Tpush and Tnow<=self.Tpush then + return false + end + + -- Evaluate push condition(s) if any. All need to be true. + local push=self:EvalConditionsAll(self.conditionPush) + + return push +end + --- Check if all given condition are true. -- @param #AUFTRAG self -- @param #table Conditions Table of conditions. @@ -123249,15 +131385,15 @@ function AUFTRAG:EvalConditionsAll(Conditions) -- Any stop condition must be true. for _,_condition in pairs(Conditions or {}) do local condition=_condition --#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. @@ -123274,15 +131410,15 @@ function AUFTRAG:EvalConditionsAny(Conditions) -- Any stop condition must be true. for _,_condition in pairs(Conditions or {}) do local condition=_condition --#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. @@ -123303,58 +131439,121 @@ function AUFTRAG:onafterStatus(From, Event, To) -- Current abs. mission time. local Tnow=timer.getAbsTime() + -- ESCORT: Check if only the group NAME of an escort had been specified. + if self.escortGroupName then + -- Try to find the group. + local group=GROUP:FindByName(self.escortGroupName) + if group and group:IsAlive() then + + -- Debug info. + self:T(self.lid..string.format("ESCORT group %s is now alive. Updating DCS task and adding group to TARGET", tostring(self.escortGroupName))) + + -- Add TARGET object. + self.engageTarget:AddObject(group) + + -- Update DCS task with the known group ID. + self.DCStask=self:GetDCSMissionTask() + + -- Set value to nil so we do not do this again in the next cycle. + self.escortGroupName=nil + end + end + -- Number of alive mission targets. local Ntargets=self:CountMissionTargets() local Ntargets0=self:GetTargetInitialNumber() - + -- Number of alive groups attached to this mission. local Ngroups=self:CountOpsGroups() -- Check if mission is not OVER yet. if self:IsNotOver() then - + if self:CheckGroupsDone() then - + -- All groups have reported MISSON DONE. self:Done() - - elseif (self.Tstop and Tnow>self.Tstop+10) or (Ntargets0>0 and Ntargets==0) then - + + elseif (self.Tstop and Tnow>self.Tstop+10) then + -- Cancel mission if stop time passed. self:Cancel() - + + elseif self.durationExe and self.Texecuting and Tnow-self.Texecuting>self.durationExe then + + -- Backup repeat values + local Nrepeat=self.Nrepeat + local NrepeatS=self.NrepeatSuccess + local NrepeatF=self.NrepeatFailure + + -- Cancel mission if stop time passed. + self:Cancel() + + self.Nrepeat=Nrepeat + self.NrepeatSuccess=NrepeatS + self.NrepeatFailure=NrepeatF + + elseif (Ntargets0>0 and Ntargets==0) then + + -- Cancel mission if mission targets are gone (if there were any in the beginning). + -- TODO: I commented this out for some reason but I forgot why... + self:T(self.lid.."No targets left cancelling mission!") + self:Cancel() + + elseif self:IsExecuting() then + + -- Had the case that mission was in state Executing but all assigned groups were dead. + -- TODO: might need to loop over all assigned groups + if Ngroups==0 then + self:Done() + else + local done=true + for groupname,data in pairs(self.groupdata or {}) do + local groupdata=data --#AUFTRAG.GroupData + local opsgroup=groupdata.opsgroup + if opsgroup:IsAlive() then + done=false + end + end + if done then + self:Done() + end + end + end - + end - + -- Current FSM state. local fsmstate=self:GetState() - - -- Check for error. + + -- Check for error. if fsmstate~=self.status then - self:E(self.lid..string.format("ERROR: FSM state %s != %s mission status!", fsmstate, self.status)) + self:T(self.lid..string.format("ERROR: FSM state %s != %s mission status!", fsmstate, self.status)) end - + -- General info. if self.verbose>=1 then - + -- Mission start stop time. local Cstart=UTILS.SecondsToClock(self.Tstart, true) local Cstop=self.Tstop and UTILS.SecondsToClock(self.Tstop, true) or "INF" - + local targetname=self:GetTargetName() or "unknown" - - local airwing=self.airwing and self.airwing.alias or "N/A" - local commander=self.wingcommander and tostring(self.wingcommander.coalition) or "N/A" - + + local Nlegions=#self.legions + local commander=self.commander and self.statusCommander or "N/A" + local chief=self.chief and self.statusChief or "N/A" + -- Info message. - self:I(self.lid..string.format("Status %s: Target=%s, T=%s-%s, assets=%d, groups=%d, targets=%d, wing=%s, commander=%s", self.status, targetname, Cstart, Cstop, #self.assets, Ngroups, Ntargets, airwing, commander)) + self:T(self.lid..string.format("Status %s: Target=%s, T=%s-%s, assets=%d, groups=%d, targets=%d, legions=%d, commander=%s, chief=%s", + self.status, targetname, Cstart, Cstop, #self.assets, Ngroups, Ntargets, Nlegions, commander, chief)) end -- Group info. if self.verbose>=2 then -- Data on assigned groups. - local text="Group data:" + local text="Group data:" for groupname,_groupdata in pairs(self.groupdata) do local groupdata=_groupdata --#AUFTRAG.GroupData text=text..string.format("\n- %s: status mission=%s opsgroup=%s", groupname, groupdata.status, groupdata.opsgroup and groupdata.opsgroup:GetState() or "N/A") @@ -123364,11 +131563,6 @@ function AUFTRAG:onafterStatus(From, Event, To) -- Ready to evaluate mission outcome? local ready2evaluate=self.Tover and Tnow-self.Tover>=self.dTevaluate or false - - --env.info("FF Tover="..tostring(self.Tover)) - --if self.Tover then - -- env.info("FF Tnow-Tover="..tostring(Tnow-self.Tover)) - --end -- Check if mission is OVER (done or cancelled) and enough time passed to evaluate the result. if self:IsOver() and ready2evaluate then @@ -123377,12 +131571,12 @@ function AUFTRAG:onafterStatus(From, Event, To) else self:__Status(-30) end - + -- Update F10 marker. if self.markerOn then self:UpdateMarker() end - + end --- Evaluate mission outcome - success or failure. @@ -123392,51 +131586,64 @@ function AUFTRAG:Evaluate() -- Assume success and check if any failed condition applies. local failed=false - + -- Target damage in %. local targetdamage=self:GetTargetDamage() - + -- Own damage in %. local owndamage=self.Ncasualties/self.Nelements*100 -- Current number of mission targets. local Ntargets=self:CountMissionTargets() local Ntargets0=self:GetTargetInitialNumber() - + local Life=self:GetTargetLife() local Life0=self:GetTargetInitialLife() - - + + if Ntargets0>0 then - + --- -- Mission had targets --- - + -- Check if failed. if self.type==AUFTRAG.Type.TROOPTRANSPORT or self.type==AUFTRAG.Type.ESCORT then - + -- Transported or escorted groups have to survive. if Ntargets0 then failed=true end - + end - + else --- @@ -123447,13 +131654,13 @@ function AUFTRAG:Evaluate() if self.Nelements==self.Ncasualties then failed=true end - + end -- Any success condition true? local successCondition=self:EvalConditionsAny(self.conditionSuccess) - + -- Any failure condition true? local failureCondition=self:EvalConditionsAny(self.conditionFailure) @@ -123462,27 +131669,38 @@ function AUFTRAG:Evaluate() elseif successCondition then failed=false end - + -- Debug text. - local text=string.format("Evaluating mission:\n") - text=text..string.format("Own casualties = %d/%d\n", self.Ncasualties, self.Nelements) - text=text..string.format("Own losses = %.1f %%\n", owndamage) - text=text..string.format("Killed units = %d\n", self.Nkills) - text=text..string.format("--------------------------\n") - text=text..string.format("Targets left = %d/%d\n", Ntargets, Ntargets0) - text=text..string.format("Targets life = %.1f/%.1f\n", Life, Life0) - text=text..string.format("Enemy losses = %.1f %%\n", targetdamage) - text=text..string.format("--------------------------\n") - text=text..string.format("Success Cond = %s\n", tostring(successCondition)) - text=text..string.format("Failure Cond = %s\n", tostring(failureCondition)) - text=text..string.format("--------------------------\n") - text=text..string.format("Final Success = %s\n", tostring(not failed)) - text=text..string.format("=========================") - self:I(self.lid..text) - + if self.verbose > 0 then + local text=string.format("Evaluating mission:\n") + text=text..string.format("Own casualties = %d/%d\n", self.Ncasualties, self.Nelements) + text=text..string.format("Own losses = %.1f %%\n", owndamage) + text=text..string.format("Killed units = %d\n", self.Nkills) + text=text..string.format("--------------------------\n") + text=text..string.format("Targets left = %d/%d\n", Ntargets, Ntargets0) + text=text..string.format("Targets life = %.1f/%.1f\n", Life, Life0) + text=text..string.format("Enemy losses = %.1f %%\n", targetdamage) + text=text..string.format("--------------------------\n") + text=text..string.format("Success Cond = %s\n", tostring(successCondition)) + text=text..string.format("Failure Cond = %s\n", tostring(failureCondition)) + text=text..string.format("--------------------------\n") + text=text..string.format("Final Success = %s\n", tostring(not failed)) + text=text..string.format("=========================") + self:I(self.lid..text) + end + + -- Trigger events. if failed then + self:I(self.lid..string.format("Mission %d [%s] failed!", self.auftragsnummer, self.type)) + if self.chief then + self.chief.Nfailure=self.chief.Nfailure+1 + end self:Failed() else + self:I(self.lid..string.format("Mission %d [%s] success!", self.auftragsnummer, self.type)) + if self.chief then + self.chief.Nsuccess=self.chief.Nsuccess+1 + end self:Success() end @@ -123528,65 +131746,156 @@ end -- @param #AUFTRAG self -- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. -- @param #string status New status. +-- @return #AUFTRAG self function AUFTRAG:SetGroupStatus(opsgroup, status) - self:T(self.lid..string.format("Setting flight %s to status %s", opsgroup and opsgroup.groupname or "nil", tostring(status))) - if self:GetGroupStatus(opsgroup)==AUFTRAG.GroupStatus.CANCELLED and status==AUFTRAG.GroupStatus.DONE then + -- Current status. + local oldstatus=self:GetGroupStatus(opsgroup) + + -- Debug info. + self:T(self.lid..string.format("Setting OPSGROUP %s to status %s-->%s", opsgroup and opsgroup.groupname or "nil", tostring(oldstatus), tostring(status))) + + if oldstatus==AUFTRAG.GroupStatus.CANCELLED and status==AUFTRAG.GroupStatus.DONE then -- Do not overwrite a CANCELLED status with a DONE status. else local groupdata=self:GetGroupData(opsgroup) if groupdata then groupdata.status=status else - self:E(self.lid.."WARNING: Could not SET flight data for flight group. Setting status to DONE") + self:T(self.lid.."WARNING: Could not SET flight data for flight group. Setting status to DONE") end end - + + -- Check if mission is NOT over. + local isNotOver=self:IsNotOver() + + -- Check if all assigned groups are done. + local groupsDone=self:CheckGroupsDone() + -- Debug info. - self:T2(self.lid..string.format("Setting flight %s status to %s. IsNotOver=%s CheckGroupsDone=%s", opsgroup.groupname, self:GetGroupStatus(opsgroup), tostring(self:IsNotOver()), tostring(self:CheckGroupsDone()))) + self:T2(self.lid..string.format("Setting OPSGROUP %s status to %s. IsNotOver=%s CheckGroupsDone=%s", opsgroup.groupname, self:GetGroupStatus(opsgroup), tostring(self:IsNotOver()), tostring(groupsDone))) -- Check if ALL flights are done with their mission. - if self:IsNotOver() and self:CheckGroupsDone() then - self:T3(self.lid.."All flights done ==> mission DONE!") + if isNotOver and groupsDone then + self:T3(self.lid.."All assigned OPSGROUPs done ==> mission DONE!") self:Done() else self:T3(self.lid.."Mission NOT DONE yet!") - end - + end + + return self end --- Get ops group mission status. -- @param #AUFTRAG self --- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. +-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group. +-- @return #string The group status. function AUFTRAG:GetGroupStatus(opsgroup) self:T3(self.lid..string.format("Trying to get Flight status for flight group %s", opsgroup and opsgroup.groupname or "nil")) - + local groupdata=self:GetGroupData(opsgroup) - + if groupdata then return groupdata.status else - - self:E(self.lid..string.format("WARNING: Could not GET groupdata for opsgroup %s. Returning status DONE.", opsgroup and opsgroup.groupname or "nil")) + + self:T(self.lid..string.format("WARNING: Could not GET groupdata for opsgroup %s. Returning status DONE.", opsgroup and opsgroup.groupname or "nil")) return AUFTRAG.GroupStatus.DONE - + end end - ---- Set Ops group waypoint coordinate. +--- Add LEGION to mission. -- @param #AUFTRAG self --- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. +-- @param Ops.Legion#LEGION Legion The legion. +-- @return #AUFTRAG self +function AUFTRAG:AddLegion(Legion) + + -- Debug info. + self:T(self.lid..string.format("Adding legion %s", Legion.alias)) + + -- Add legion to table. + table.insert(self.legions, Legion) + + return self +end + +--- Remove LEGION from mission. +-- @param #AUFTRAG self +-- @param Ops.Legion#LEGION Legion The legion. +-- @return #AUFTRAG self +function AUFTRAG:RemoveLegion(Legion) + + -- Loop over legions + for i=#self.legions,1,-1 do + local legion=self.legions[i] --Ops.Legion#LEGION + + if legion.alias==Legion.alias then + + -- Debug info. + self:T(self.lid..string.format("Removing legion %s", Legion.alias)) + table.remove(self.legions, i) + + -- Set legion status to nil. + self.statusLegion[Legion.alias]=nil + + return self + end + + end + + self:T(self.lid..string.format("ERROR: Legion %s not found and could not be removed!", Legion.alias)) + return self +end + +--- Set LEGION mission status. +-- @param #AUFTRAG self +-- @param Ops.Legion#LEGION Legion The legion. +-- @param #string Status New status. +-- @return #AUFTRAG self +function AUFTRAG:SetLegionStatus(Legion, Status) + + -- Old status + local status=self:GetLegionStatus(Legion) + + -- Debug info. + self:T(self.lid..string.format("Setting LEGION %s to status %s-->%s", Legion.alias, tostring(status), tostring(Status))) + + -- New status. + self.statusLegion[Legion.alias]=Status + + return self +end + +--- Get LEGION mission status. +-- @param #AUFTRAG self +-- @param Ops.Legion#LEGION Legion The legion. +-- @return #string status Current status. +function AUFTRAG:GetLegionStatus(Legion) + + -- New status. + local status=self.statusLegion[Legion.alias] or "unknown" + + return status +end + + +--- Set mission (ingress) waypoint coordinate for OPS group. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group. -- @param Core.Point#COORDINATE coordinate Waypoint Coordinate. +-- @return #AUFTRAG self function AUFTRAG:SetGroupWaypointCoordinate(opsgroup, coordinate) local groupdata=self:GetGroupData(opsgroup) if groupdata then groupdata.waypointcoordinate=coordinate end + return self end ---- Get opsgroup waypoint coordinate. +--- Get mission (ingress) waypoint coordinate of OPS group -- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group. -- @return Core.Point#COORDINATE Waypoint Coordinate. function AUFTRAG:GetGroupWaypointCoordinate(opsgroup) local groupdata=self:GetGroupData(opsgroup) @@ -123596,9 +131905,9 @@ function AUFTRAG:GetGroupWaypointCoordinate(opsgroup) end ---- Set Ops group waypoint task. +--- Set mission waypoint task for OPS group. -- @param #AUFTRAG self --- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. +-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group. -- @param Ops.OpsGroup#OPSGROUP.Task task Waypoint task. function AUFTRAG:SetGroupWaypointTask(opsgroup, task) self:T2(self.lid..string.format("Setting waypoint task %s", task and task.description or "WTF")) @@ -123608,9 +131917,9 @@ function AUFTRAG:SetGroupWaypointTask(opsgroup, task) end end ---- Get opsgroup waypoint task. +--- Get mission waypoint task of OPS group. -- @param #AUFTRAG self --- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. +-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group. -- @return Ops.OpsGroup#OPSGROUP.Task task Waypoint task. Waypoint task. function AUFTRAG:GetGroupWaypointTask(opsgroup) local groupdata=self:GetGroupData(opsgroup) @@ -123619,22 +131928,24 @@ function AUFTRAG:GetGroupWaypointTask(opsgroup) end end ---- Set opsgroup waypoint index. +--- Set mission (ingress) waypoint UID for OPS group. -- @param #AUFTRAG self --- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. --- @param #number waypointindex Waypoint index. +-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group. +-- @param #number waypointindex Waypoint UID. +-- @return #AUFTRAG self function AUFTRAG:SetGroupWaypointIndex(opsgroup, waypointindex) - self:T2(self.lid..string.format("Setting waypoint index %d", waypointindex)) + self:T2(self.lid..string.format("Setting Mission waypoint UID=%d", waypointindex)) local groupdata=self:GetGroupData(opsgroup) if groupdata then groupdata.waypointindex=waypointindex end + return self end ---- Get opsgroup waypoint index. +--- Get mission (ingress) waypoint UID of OPS group. -- @param #AUFTRAG self --- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. --- @return #number Waypoint index +-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group. +-- @return #number Waypoint UID. function AUFTRAG:GetGroupWaypointIndex(opsgroup) local groupdata=self:GetGroupData(opsgroup) if groupdata then @@ -123642,36 +131953,90 @@ function AUFTRAG:GetGroupWaypointIndex(opsgroup) end end +--- Set Egress waypoint UID for OPS group. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group. +-- @param #number waypointindex Waypoint UID. +-- @return #AUFTRAG self +function AUFTRAG:SetGroupEgressWaypointUID(opsgroup, waypointindex) + self:T2(self.lid..string.format("Setting Egress waypoint UID=%d", waypointindex)) + local groupdata=self:GetGroupData(opsgroup) + if groupdata then + groupdata.waypointEgressUID=waypointindex + end + return self +end + +--- Get Egress waypoint UID of OPS group. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group. +-- @return #number Waypoint UID. +function AUFTRAG:GetGroupEgressWaypointUID(opsgroup) + local groupdata=self:GetGroupData(opsgroup) + if groupdata then + return groupdata.waypointEgressUID + end +end + + --- Check if all flights are done with their mission (or dead). -- @param #AUFTRAG self -- @return #boolean If true, all flights are done with the mission. function AUFTRAG:CheckGroupsDone() - -- These are early stages, where we might not even have a opsgroup defined to be checked. - if self:IsPlanned() or self:IsQueued() or self:IsRequested() then - return false - end - - -- It could be that all flights were destroyed on the way to the mission execution waypoint. - -- TODO: would be better to check if everybody is dead by now. - if self:IsStarted() and self:CountOpsGroups()==0 then - return true - end - - -- Check status of all flight groups. + -- Check status of all OPS groups. for groupname,data in pairs(self.groupdata) do local groupdata=data --#AUFTRAG.GroupData if groupdata then - if groupdata.status==AUFTRAG.GroupStatus.DONE or groupdata.status==AUFTRAG.GroupStatus.CANCELLED then - -- This one is done or cancelled. - else + if not (groupdata.status==AUFTRAG.GroupStatus.DONE or groupdata.status==AUFTRAG.GroupStatus.CANCELLED) then -- At least this flight is not DONE or CANCELLED. - return false + 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 end + -- Check status of all LEGIONs. + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + local status=self:GetLegionStatus(legion) + if not status==AUFTRAG.Status.CANCELLED then + -- At least one LEGION has not CANCELLED. + 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 + + -- Check commander status. + if self.commander then + if not self.statusCommander==AUFTRAG.Status.CANCELLED then + self:T2(self.lid..string.format("CheckGroupsDone: COMMANDER is not CANCELLED but in state %s. Mission NOT DONE!", self.statusCommander)) + return false + end + end + + -- Check chief status. + if self.chief then + if not self.statusChief==AUFTRAG.Status.CANCELLED then + 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: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 + + -- It could be that all flights were destroyed on the way to the mission execution waypoint. + -- TODO: would be better to check if everybody is dead by now. + if self:IsStarted() and self:CountOpsGroups()==0 then + self:T(self.lid..string.format("CheckGroupsDone: Mission is STARTED state %s [FSM=%s] but count of alive OPSGROUP is zero. Mission DONE!", self.status, self:GetState())) + return true + end + return true end @@ -123689,14 +132054,14 @@ function AUFTRAG:OnEventUnitLost(EventData) local unit=EventData.IniUnit local group=EventData.IniGroup local unitname=EventData.IniUnitName - + for _,_groupdata in pairs(self.groupdata) do local groupdata=_groupdata --#AUFTRAG.GroupData if groupdata and groupdata.opsgroup and groupdata.opsgroup.groupname==EventData.IniGroupName then - self:I(self.lid..string.format("UNIT LOST event for opsgroup %s unit %s", groupdata.opsgroup.groupname, EventData.IniUnitName)) + self:T(self.lid..string.format("UNIT LOST event for opsgroup %s unit %s", groupdata.opsgroup.groupname, EventData.IniUnitName)) end end - + end end @@ -123716,16 +132081,14 @@ function AUFTRAG:onafterPlanned(From, Event, To) self:T(self.lid..string.format("New mission status=%s", self.status)) end ---- On after "Queue" event. Mission is added to the mission queue of an AIRWING. +--- On after "Queue" event. Mission is added to the mission queue of a LEGION. -- @param #AUFTRAG self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param Ops.AirWing#AIRWING Airwing The airwing. function AUFTRAG:onafterQueued(From, Event, To, Airwing) self.status=AUFTRAG.Status.QUEUED - self.airwing=Airwing - self:T(self.lid..string.format("New mission status=%s at airwing %s", self.status, tostring(Airwing.alias))) + self:T(self.lid..string.format("New mission status=%s", self.status)) end @@ -123739,24 +132102,24 @@ function AUFTRAG:onafterRequested(From, Event, To) self:T(self.lid..string.format("New mission status=%s", self.status)) end ---- On after "Assign" event. +--- On after "Assign" event. -- @param #AUFTRAG self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. function AUFTRAG:onafterAssign(From, Event, To) self.status=AUFTRAG.Status.ASSIGNED - self:T(self.lid..string.format("New mission status=%s", self.status)) + self:T(self.lid..string.format("New mission status=%s", self.status)) end ---- On after "Schedule" event. Mission is added to the mission queue of a FLIGHTGROUP. +--- On after "Schedule" event. Mission is added to the mission queue of an OPSGROUP. -- @param #AUFTRAG self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. function AUFTRAG:onafterScheduled(From, Event, To) self.status=AUFTRAG.Status.SCHEDULED - self:T(self.lid..string.format("New mission status=%s", self.status)) + self:T(self.lid..string.format("New mission status=%s", self.status)) end --- On after "Start" event. @@ -123766,7 +132129,8 @@ end -- @param #string To To state. function AUFTRAG:onafterStarted(From, Event, To) self.status=AUFTRAG.Status.STARTED - self:T(self.lid..string.format("New mission status=%s", self.status)) + self.Tstarted=timer.getAbsTime() + self:T(self.lid..string.format("New mission status=%s", self.status)) end --- On after "Execute" event. @@ -123776,21 +132140,8 @@ end -- @param #string To To state. function AUFTRAG:onafterExecuting(From, Event, To) self.status=AUFTRAG.Status.EXECUTING - self:T(self.lid..string.format("New mission status=%s", self.status)) -end - ---- On after "Done" event. --- @param #AUFTRAG self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. -function AUFTRAG:onafterDone(From, Event, To) - self.status=AUFTRAG.Status.DONE + self.Texecuting=timer.getAbsTime() self:T(self.lid..string.format("New mission status=%s", self.status)) - - -- Set time stamp. - self.Tover=timer.getAbsTime() - end --- On after "ElementDestroyed" event. @@ -123798,7 +132149,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 @@ -123817,6 +132169,9 @@ function AUFTRAG:onafterGroupDead(From, Event, To, OpsGroup) self:AssetDead(asset) end + -- Number of dead groups. + self.Ndead=self.Ndead+1 + end --- On after "AssetDead" event. @@ -123824,27 +132179,27 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param Ops.AirWing#AIRWING.SquadronAsset Asset The asset. +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset. function AUFTRAG:onafterAssetDead(From, Event, To, Asset) - - -- Number of groups alive. + + -- Number of groups alive. local N=self:CountOpsGroups() - - self:I(self.lid..string.format("Asset %s dead! Number of ops groups remaining %d", tostring(Asset.spawngroupname), N)) - + + self:T(self.lid..string.format("Asset %s dead! Number of ops groups remaining %d", tostring(Asset.spawngroupname), N)) + -- All assets dead? if N==0 then - + if self:IsNotOver() then - + -- Cancel mission. Wait for next mission update to evaluate SUCCESS or FAILURE. self:Cancel() - + else - + --self:E(self.lid.."ERROR: All assets are dead not but mission was already over... Investigate!") -- Now this can happen, because when a opsgroup dies (sometimes!), the mission is DONE - + end end @@ -123860,52 +132215,132 @@ end -- @param #string To To state. function AUFTRAG:onafterCancel(From, Event, To) + -- Number of OPSGROUPS assigned and alive. + local Ngroups = self:CountOpsGroups() + -- Debug info. - self:I(self.lid..string.format("CANCELLING mission in status %s. Will wait for groups to report mission DONE before evaluation", self.status)) - + self:T(self.lid..string.format("CANCELLING mission in status %s. Will wait for %d groups to report mission DONE before evaluation", self.status, Ngroups)) + -- Time stamp. self.Tover=timer.getAbsTime() - + -- No more repeats. self.Nrepeat=self.repeated self.NrepeatFailure=self.repeatedFailure self.NrepeatSuccess=self.repeatedSuccess - + -- Not necessary to delay the evaluaton?! self.dTevaluate=0 - - if self.wingcommander then - - self:T(self.lid..string.format("Wingcommander will cancel the mission. Will wait for mission DONE before evaluation!")) - - self.wingcommander:CancelMission(self) - elseif self.airwing then - - self:T(self.lid..string.format("Airwing %s will cancel the mission. Will wait for mission DONE before evaluation!", self.airwing.alias)) - - -- Airwing will cancel all flight missions and remove queued request from warehouse queue. - self.airwing:MissionCancel(self) - + if self.chief then + + -- Debug info. + self:T(self.lid..string.format("CHIEF will cancel the mission. Will wait for mission DONE before evaluation!")) + + -- CHIEF will cancel the mission. + self.chief:MissionCancel(self) + + elseif self.commander then + + -- Debug info. + self:T(self.lid..string.format("COMMANDER will cancel the mission. Will wait for mission DONE before evaluation!")) + + -- COMMANDER will cancel the mission. + self.commander:MissionCancel(self) + + elseif self.legions and #self.legions>0 then + + -- Loop over all LEGIONs. + for _,_legion in pairs(self.legions or {}) do + local legion=_legion --Ops.Legion#LEGION + + -- Debug info. + self:T(self.lid..string.format("LEGION %s will cancel the mission. Will wait for mission DONE before evaluation!", legion.alias)) + + -- Legion will cancel all flight missions and remove queued request from warehouse queue. + legion:MissionCancel(self) + + end + else - - self:T(self.lid..string.format("No airwing or wingcommander. Attached flights will cancel the mission on their own. Will wait for mission DONE before evaluation!")) - - for _,_groupdata in pairs(self.groupdata) do + + -- Debug info. + 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 local groupdata=_groupdata --#AUFTRAG.GroupData groupdata.opsgroup:MissionCancel(self) end - + end - + -- Special mission states. - if self.status==AUFTRAG.Status.PLANNED then - self:T(self.lid..string.format("Cancelled mission was in planned stage. Call it done!")) + if self:IsPlanned() or self:IsQueued() or self:IsRequested() or Ngroups==0 then + self:T(self.lid..string.format("Cancelled mission was in %s stage with %d groups assigned and alive. Call it done!", self.status, Ngroups)) self:Done() end end +--- On after "Done" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AUFTRAG:onafterDone(From, Event, To) + self.status=AUFTRAG.Status.DONE + self:T(self.lid..string.format("New mission status=%s", self.status)) + + -- Set time stamp. + self.Tover=timer.getAbsTime() + + -- Not executing any more. + self.Texecuting=nil + + -- Set status for CHIEF. + self.statusChief=AUFTRAG.Status.DONE + + -- Set status for COMMANDER. + self.statusCommander=AUFTRAG.Status.DONE + + -- Set status for LEGIONs. + for _,_legion in pairs(self.legions) do + local Legion=_legion --Ops.Legion#LEGION + + self:SetLegionStatus(Legion, AUFTRAG.Status.DONE) + + -- Remove pending request from legion queue. + if self.type==AUFTRAG.Type.RELOCATECOHORT then + + -- Get request ID + local requestid=self.requestID[Legion.alias] + + if requestid then + + -- Debug info. + self:T(self.lid.."Removing request from pending queue") + + -- Remove request from pending queue. + Legion:_DeleteQueueItemByID(requestid, Legion.pending) + + -- Remove cohort from old legion. + local Cohort=self.DCStask.params.cohort --Ops.Cohort#COHORT + Legion:DelCohort(Cohort) + + else + self:E(self.lid.."WARNING: Could NOT remove relocation request from from pending queue (all assets were spawned?)") + end + end + end + + -- Trigger relocated event. + if self.type==AUFTRAG.Type.RELOCATECOHORT then + local cohort=self.DCStask.params.cohort --Ops.Cohort#COHORT + cohort:Relocated() + end +end + --- On after "Success" event. -- @param #AUFTRAG self -- @param #string From From state. @@ -123915,9 +132350,17 @@ function AUFTRAG:onafterSuccess(From, Event, To) self.status=AUFTRAG.Status.SUCCESS self:T(self.lid..string.format("New mission status=%s", self.status)) - + + -- Set status for CHIEF, COMMANDER and LEGIONs + self.statusChief=self.status + self.statusCommander=self.status + for _,_legion in pairs(self.legions) do + local Legion=_legion --Ops.Legion#LEGION + self:SetLegionStatus(Legion, self.status) + end + local repeatme=self.repeatedSuccess Repeat mission!", self.repeated+1, N)) + self:T(self.lid..string.format("Mission SUCCESS! Repeating mission for the %d time (max %d times) ==> Repeat mission!", self.repeated+1, N)) self:Repeat() - + else - + -- Stop mission. - self:I(self.lid..string.format("Mission SUCCESS! Number of max repeats %d reached ==> Stopping mission!", self.repeated+1)) + self:T(self.lid..string.format("Mission SUCCESS! Number of max repeats %d reached ==> Stopping mission!", self.repeated+1)) self:Stop() - + end end @@ -123949,31 +132392,54 @@ function AUFTRAG:onafterFailed(From, Event, To) self.status=AUFTRAG.Status.FAILED self:T(self.lid..string.format("New mission status=%s", self.status)) - + + -- Set status for CHIEF, COMMANDER and LEGIONs + self.statusChief=self.status + self.statusCommander=self.status + for _,_legion in pairs(self.legions) do + local Legion=_legion --Ops.Legion#LEGION + self:SetLegionStatus(Legion, self.status) + end + local repeatme=self.repeatedFailure Repeat mission!", self.repeated+1, N)) + self:T(self.lid..string.format("Mission FAILED! Repeating mission for the %d time (max %d times) ==> Repeat mission!", self.repeated+1, N)) self:Repeat() - + else - + -- Stop mission. - self:I(self.lid..string.format("Mission FAILED! Number of max repeats %d reached ==> Stopping mission!", self.repeated+1)) + self:T(self.lid..string.format("Mission FAILED! Number of max repeats %d reached ==> Stopping mission!", self.repeated+1)) self:Stop() - - end + + end end +--- On before "Repeat" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AUFTRAG:onbeforeRepeat(From, Event, To) + + if not (self.chief or self.commander or #self.legions>0) then + self:E(self.lid.."ERROR: Mission can only be repeated by a CHIEF, COMMANDER or LEGION! Stopping AUFTRAG") + self:Stop() + return false + end + + return true +end --- On after "Repeat" event. -- @param #AUFTRAG self @@ -123984,52 +132450,92 @@ function AUFTRAG:onafterRepeat(From, Event, To) -- Set mission status to PLANNED. self.status=AUFTRAG.Status.PLANNED - + + -- Debug info. self:T(self.lid..string.format("New mission status=%s (on Repeat)", self.status)) + -- Set status for CHIEF, COMMANDER and LEGIONs + self.statusChief=self.status + self.statusCommander=self.status + for _,_legion in pairs(self.legions) do + local Legion=_legion --Ops.Legion#LEGION + self:SetLegionStatus(Legion, self.status) + end + -- Increase repeat counter. self.repeated=self.repeated+1 - + if self.chief then - - --TODO - - elseif self.wingcommander then - - -- Remove mission from airwing because WC will assign it again but maybe to a different wing. - if self.airwing then - self.airwing:RemoveMission(self) + + -- Set status for chief. + self.statusChief=AUFTRAG.Status.PLANNED + + -- Remove mission from wingcommander because Chief will assign it again. + if self.commander then + self.statusCommander=AUFTRAG.Status.PLANNED end - - elseif self.airwing then - - -- Already at the airwing ==> Queued() - self:Queued(self.airwing) - + + -- Remove mission from legions because commander will assign it again but maybe to different legion(s). + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + legion:RemoveMission(self) + end + + elseif self.commander then + + -- Set status for commander. + self.statusCommander=AUFTRAG.Status.PLANNED + + -- Remove mission from legion(s) because commander will assign it again but maybe to different legion(s). + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + legion:RemoveMission(self) + self:SetLegionStatus(legion, AUFTRAG.Status.PLANNED) + end + + elseif #self.legions>0 then + + -- Remove mission from airwing because WC will assign it again but maybe to a different wing. + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + legion:RemoveMission(self) + self:SetLegionStatus(legion, AUFTRAG.Status.PLANNED) + legion:AddMission(self) + end + else - self:E(self.lid.."ERROR: Mission can only be repeated by a CHIEF, WINGCOMMANDER or AIRWING! Stopping AUFTRAG") + self:E(self.lid.."ERROR: Mission can only be repeated by a CHIEF, COMMANDER or LEGION! Stopping AUFTRAG") self:Stop() + return end - - + + -- No mission assets. self.assets={} - - for _,_groupdata in pairs(self.groupdata) do + + + -- Remove OPS groups. This also removes the mission from the OPSGROUP mission queue. + for groupname,_groupdata in pairs(self.groupdata) do local groupdata=_groupdata --#AUFTRAG.GroupData local opsgroup=groupdata.opsgroup if opsgroup then self:DelOpsGroup(opsgroup) end - - end + + end -- No flight data. self.groupdata={} - + -- 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() + -- Call status again. self:__Status(-30) @@ -124042,19 +132548,30 @@ end -- @param #string To To state. function AUFTRAG:onafterStop(From, Event, To) - self:I(self.lid..string.format("STOPPED mission in status=%s. Removing missions from queues. Stopping CallScheduler!", self.status)) + -- Debug info. + self:T(self.lid..string.format("STOPPED mission in status=%s. Removing missions from queues. Stopping CallScheduler!", self.status)) - -- TODO: remove missions from queues in WINGCOMMANDER, AIRWING and FLIGHGROUPS! -- TODO: Mission should be OVER! we dont want to remove running missions from any queues. - - if self.wingcommander then - self.wingcommander:RemoveMission(self) - end - - if self.airwing then - self.airwing:RemoveMission(self) + + -- Remove mission from CHIEF queue. + if self.chief then + self.chief:RemoveMission(self) end + -- Remove mission from WINGCOMMANDER queue. + if self.commander then + self.commander:RemoveMission(self) + end + + -- Remove mission from LEGION queues. + if #self.legions>0 then + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + legion:RemoveMission(self) + end + end + + -- Remove mission from OPSGROUP queue for _,_groupdata in pairs(self.groupdata) do local groupdata=_groupdata --#AUFTRAG.GroupData groupdata.opsgroup:RemoveMission(self) @@ -124062,13 +132579,13 @@ function AUFTRAG:onafterStop(From, Event, To) -- No mission assets. self.assets={} - + -- No flight data. self.groupdata={} -- Clear pending scheduler calls. self.CallScheduler:Clear() - + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -124081,26 +132598,26 @@ end function AUFTRAG:_TargetFromObject(Object) if not self.engageTarget then - - if Object:IsInstanceOf("TARGET") then - + + if Object and Object:IsInstanceOf("TARGET") then + self.engageTarget=Object - - else - + + else --if Object then + self.engageTarget=TARGET:New(Object) - + end else - + -- Target was already specified elsewhere. - + end -- Debug info. --self:T2(self.lid..string.format("Mission Target %s Type=%s, Ntargets=%d, Lifepoints=%d", self.engageTarget.lid, self.engageTarget.lid, self.engageTarget.N0, self.engageTarget:GetLife())) - + return self end @@ -124110,12 +132627,13 @@ end -- @return #number Number of alive target units. function AUFTRAG:CountMissionTargets() + local N=0 + if self.engageTarget then - return self.engageTarget:CountTargets() - else - return 0 + N=self.engageTarget:CountTargets() end - + + return N end --- Get initial number of targets. @@ -124179,14 +132697,25 @@ end -- @param #AUFTRAG self -- @return Wrapper.Positionable#POSITIONABLE The target object. Could be many things. function AUFTRAG:GetObjective() - return self:GetTargetData():GetObject() + local objective=self:GetTargetData():GetObject() + return objective end --- Get type of target. -- @param #AUFTRAG self -- @return #string The target type. function AUFTRAG:GetTargetType() - return self:GetTargetData().Type + local target=self.engageTarget + if target then + local to=target:GetObjective() + if to then + return to.Type + else + return "Unknown" + end + else + return "Unknown" + end end --- Get 2D vector of target. @@ -124195,7 +132724,8 @@ end function AUFTRAG:GetTargetVec2() local coord=self:GetTargetCoordinate() if coord then - return coord:GetVec2() + local vec2=coord:GetVec2() + return vec2 end return nil end @@ -124204,18 +132734,24 @@ end -- @param #AUFTRAG self -- @return Core.Point#COORDINATE The target coordinate or *nil*. function AUFTRAG:GetTargetCoordinate() - + if self.transportPickup then - - -- Special case where we defined a + + -- Special case where we defined a return self.transportPickup - + elseif self.engageTarget then - return self.engageTarget:GetCoordinate() - + local coord=self.engageTarget:GetCoordinate() + return coord + + elseif self.type==AUFTRAG.Type.ALERT5 then + + -- For example, COMMANDER will not assign a coordiante. This will be done later, when the mission is assigned to an airwing. + return nil + else - self:E(self.lid.."ERROR: Cannot get target coordinate!") + self:T(self.lid.."ERROR: Cannot get target coordinate!") end return nil @@ -124225,11 +132761,12 @@ end -- @param #AUFTRAG self -- @return #string Name of the target or "N/A". function AUFTRAG:GetTargetName() - + if self.engageTarget then - return self.engageTarget:GetName() + local name=self.engageTarget:GetName() + return name end - + return "N/A" end @@ -124241,13 +132778,13 @@ end function AUFTRAG:GetTargetDistance(FromCoord) local TargetCoord=self:GetTargetCoordinate() - + if TargetCoord and FromCoord then return TargetCoord:Get2DDistance(FromCoord) else - self:E(self.lid.."ERROR: TargetCoord or FromCoord does not exist in AUFTRAG:GetTargetDistance() function! Returning 0") + self:T(self.lid.."ERROR: TargetCoord or FromCoord does not exist in AUFTRAG:GetTargetDistance() function! Returning 0") end - + return 0 end @@ -124258,32 +132795,54 @@ end --- Add asset to mission. -- @param #AUFTRAG self --- @param Ops.AirWing#AIRWING.SquadronAsset Asset The asset to be added to the mission. +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset to be added to the mission. -- @return #AUFTRAG self function AUFTRAG:AddAsset(Asset) + -- Debug info + self:T(self.lid..string.format("Adding asset \"%s\" to mission", tostring(Asset.spawngroupname))) + + -- 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 assets to mission. +-- @param #AUFTRAG self +-- @param #table Assets List of assets. +-- @return #AUFTRAG self +function AUFTRAG:_AddAssets(Assets) + + for _,asset in pairs(Assets) do + self:AddAsset(asset) + end return self end --- Delete asset from mission. -- @param #AUFTRAG self --- @param Ops.AirWing#AIRWING.SquadronAsset Asset The asset to be removed. +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset to be removed. -- @return #AUFTRAG self function AUFTRAG:DelAsset(Asset) for i,_asset in pairs(self.assets or {}) do - local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset - + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + if asset.uid==Asset.uid then - self:T(self.lid..string.format("Removing asset \"%s\" from mission", tostring(asset.spawngroupname))) + self:T(self.lid..string.format("Removing asset \"%s\" from mission", tostring(Asset.spawngroupname))) table.remove(self.assets, i) return self end - + end return self @@ -124292,24 +132851,24 @@ end --- Get asset by its spawn group name. -- @param #AUFTRAG self -- @param #string Name Asset spawn group name. --- @return Ops.AirWing#AIRWING.SquadronAsset +-- @return Functional.Warehouse#WAREHOUSE.Assetitem Asset. function AUFTRAG:GetAssetByName(Name) for i,_asset in pairs(self.assets or {}) do - local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset - + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + if asset.spawngroupname==Name then return asset end - + end return nil end ---- Count alive ops groups assigned for this mission. +--- Count alive OPS groups assigned for this mission. -- @param #AUFTRAG self --- @return #number Number of alive flight groups. +-- @return #number Number of alive OPS groups. function AUFTRAG:CountOpsGroups() local N=0 for _,_groupdata in pairs(self.groupdata) do @@ -124321,6 +132880,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 @@ -124336,19 +132911,81 @@ function AUFTRAG:GetMissionTypesText(MissionTypes) return text end ---- Set the mission waypoint coordinate where the mission is executed. +--- Set the mission waypoint coordinate where the mission is executed. Note that altitude is set via `:SetMissionAltitude`. -- @param #AUFTRAG self --- @return Core.Point#COORDINATE Coordinate where the mission is executed. +-- @param Core.Point#COORDINATE Coordinate Coordinate where the mission is executed. -- @return #AUFTRAG self function AUFTRAG:SetMissionWaypointCoord(Coordinate) + + -- Obviously a zone was passed. We get the coordinate. + if Coordinate:IsInstanceOf("ZONE_BASE") then + Coordinate=Coordinate:GetCoordinate() + end + self.missionWaypointCoord=Coordinate + return self +end + +--- Set randomization of the mission waypoint coordinate. Each assigned group will get a random ingress coordinate, where the mission is executed. +-- @param #AUFTRAG self +-- @param #number Radius Distance in meters. Default `#nil`. +-- @return #AUFTRAG self +function AUFTRAG:SetMissionWaypointRandomization(Radius) + self.missionWaypointRadius=Radius + return self +end + +--- Set the mission egress coordinate. This is the coordinate where the assigned group will go once the mission is finished. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Coordinate Egrees coordinate. +-- @param #number Altitude (Optional) Altitude in feet. Default is y component of coordinate. +-- @return #AUFTRAG self +function AUFTRAG:SetMissionEgressCoord(Coordinate, Altitude) + + -- Obviously a zone was passed. We get the coordinate. + if Coordinate:IsInstanceOf("ZONE_BASE") then + Coordinate=Coordinate:GetCoordinate() + end + + self.missionEgressCoord=Coordinate + + if Altitude then + self.missionEgressCoord.y=UTILS.FeetToMeters(Altitude) + end +end + +--- Get the mission egress coordinate if this was defined. +-- @param #AUFTRAG self +-- @return Core.Point#COORDINATE Coordinate Coordinate or nil. +function AUFTRAG:GetMissionEgressCoord() + return self.missionEgressCoord +end + +--- Get coordinate which was set as mission waypoint coordinate. +-- @param #AUFTRAG self +-- @return Core.Point#COORDINATE Coordinate where the mission is executed or `#nil`. +function AUFTRAG:_GetMissionWaypointCoordSet() + + -- Check if a coord has been explicitly set. + if self.missionWaypointCoord then + local coord=self.missionWaypointCoord + if self.missionAltitude then + coord.y=self.missionAltitude + end + + + return coord + end + end --- Get coordinate of target. First unit/group of the set is used. -- @param #AUFTRAG self -- @param Wrapper.Group#GROUP group Group. +-- @param #number randomradius Random radius in meters. +-- @param #table surfacetypes Surface types of random zone. -- @return Core.Point#COORDINATE Coordinate where the mission is executed. -function AUFTRAG:GetMissionWaypointCoord(group) +function AUFTRAG:GetMissionWaypointCoord(group, randomradius, surfacetypes) -- Check if a coord has been explicitly set. if self.missionWaypointCoord then @@ -124362,10 +132999,12 @@ function AUFTRAG:GetMissionWaypointCoord(group) -- Create waypoint coordinate half way between us and the target. local waypointcoord=group:GetCoordinate():GetIntermediateCoordinate(self:GetTargetCoordinate(), self.missionFraction) local alt=waypointcoord.y - + -- Add some randomization. - waypointcoord=ZONE_RADIUS:New("Temp", waypointcoord:GetVec2(), 1000):GetRandomCoordinate():SetAltitude(alt, false) - + if randomradius then + waypointcoord=ZONE_RADIUS:New("Temp", waypointcoord:GetVec2(), randomradius):GetRandomCoordinate(nil, nil, surfacetypes):SetAltitude(alt, false) + end + -- Set altitude of mission waypoint. if self.missionAltitude then waypointcoord:SetAltitude(self.missionAltitude, true) @@ -124392,25 +133031,25 @@ function AUFTRAG:UpdateMarker() local text=string.format("%s %s: %s", self.name, self.type:upper(), self.status:upper()) text=text..string.format("\n%s", self:GetTargetName()) text=text..string.format("\nTargets %d/%d, Life Points=%d/%d", self:CountMissionTargets(), self:GetTargetInitialNumber(), self:GetTargetLife(), self:GetTargetInitialLife()) - text=text..string.format("\nFlights %d/%d", self:CountOpsGroups(), self.nassets) + text=text..string.format("\nOpsGroups %d/%d", self:CountOpsGroups(), self:GetNumberOfRequiredAssets()) if not self.marker then - + -- Get target coordinates. Can be nil! local targetcoord=self:GetTargetCoordinate() - + if self.markerCoaliton and self.markerCoaliton>=0 then self.marker=MARKER:New(targetcoord, text):ReadOnly():ToCoalition(self.markerCoaliton) else self.marker=MARKER:New(targetcoord, text):ReadOnly():ToAll() - end - + end + else - + if self.marker:GetText()~=text then self.marker:UpdateText(text) end - + end return self @@ -124418,117 +133057,185 @@ end --- Get DCS task table for the given mission. -- @param #AUFTRAG self --- @param Wrapper.Controllable#CONTROLLABLE TaskControllable The controllable for which this task is set. Most tasks don't need it. -- @return DCS#Task The DCS task table. If multiple tasks are necessary, this is returned as a combo task. -function AUFTRAG:GetDCSMissionTask(TaskControllable) +function AUFTRAG:GetDCSMissionTask() local DCStasks={} -- Create DCS task based on current self. if self.type==AUFTRAG.Type.ANTISHIP then - + ---------------------- -- 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 - + ------------------- -- AWACS Mission -- - ------------------- + ------------------- local DCStask=CONTROLLABLE.EnRouteTaskAWACS(nil) - + table.insert(self.enrouteTasks, DCStask) - + elseif self.type==AUFTRAG.Type.BAI then - + ----------------- -- BAI Mission -- - ----------------- + ----------------- self:_GetDCSAttackTask(self.engageTarget, DCStasks) elseif self.type==AUFTRAG.Type.BOMBING then - + --------------------- -- BOMBING Mission -- --------------------- - + local DCStask=CONTROLLABLE.TaskBombing(nil, self:GetTargetVec2(), self.engageAsGroup, self.engageWeaponExpend, self.engageQuantity, self.engageDirection, self.engageAltitude, self.engageWeaponType, Divebomb) - + table.insert(DCStasks, DCStask) - + elseif self.type==AUFTRAG.Type.BOMBRUNWAY then - + ------------------------ -- BOMBRUNWAY Mission -- ------------------------ - + local DCStask=CONTROLLABLE.TaskBombingRunway(nil, self.engageTarget:GetObject(), self.engageWeaponType, self.engageWeaponExpend, self.engageQuantity, self.engageDirection, self.engageAsGroup) - - table.insert(DCStasks, DCStask) + + table.insert(DCStasks, DCStask) elseif self.type==AUFTRAG.Type.BOMBCARPET then - + ------------------------ -- BOMBCARPET Mission -- ------------------------ - + local DCStask=CONTROLLABLE.TaskCarpetBombing(nil, self:GetTargetVec2(), self.engageAsGroup, self.engageWeaponExpend, self.engageQuantity, self.engageDirection, self.engageAltitude, self.engageWeaponType, self.engageCarpetLength) - - table.insert(DCStasks, DCStask) + + table.insert(DCStasks, DCStask) elseif self.type==AUFTRAG.Type.CAP then - + ----------------- -- CAP Mission -- - ----------------- + ----------------- local DCStask=CONTROLLABLE.EnRouteTaskEngageTargetsInZone(nil, self.engageZone:GetVec2(), self.engageZone:GetRadius(), self.engageTargetTypes, Priority) - + table.insert(self.enrouteTasks, DCStask) - + elseif self.type==AUFTRAG.Type.CAS then - + ----------------- -- CAS Mission -- ----------------- local DCStask=CONTROLLABLE.EnRouteTaskEngageTargetsInZone(nil, self.engageZone:GetVec2(), self.engageZone:GetRadius(), self.engageTargetTypes, Priority) - + table.insert(self.enrouteTasks, DCStask) elseif self.type==AUFTRAG.Type.ESCORT then - + -------------------- -- ESCORT Mission -- -------------------- local DCStask=CONTROLLABLE.TaskEscort(nil, self.engageTarget:GetObject(), self.escortVec3, LastWaypointIndex, self.engageMaxDistance, self.engageTargetTypes) - + table.insert(DCStasks, DCStask) elseif self.type==AUFTRAG.Type.FACA then - + ----------------- -- FAC Mission -- - ----------------- + ----------------- local DCStask=CONTROLLABLE.TaskFAC_AttackGroup(nil, self.engageTarget:GetObject(), self.engageWeaponType, self.facDesignation, self.facDatalink, self.facFreq, self.facModu, CallsignName, CallsignNumber) - + table.insert(DCStasks, DCStask) - + elseif self.type==AUFTRAG.Type.FERRY then - + ------------------- -- FERRY Mission -- ------------------- - - -- TODO: Ferry mission type. How? - + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.FERRY + + -- We create a "fake" DCS task. + local param={} + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.RELOCATECOHORT then + + ---------------------- + -- RELOCATE Mission -- + ---------------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.RELOCATECOHORT + + -- We create a "fake" DCS task. + local param={} + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.RECOVERYTANKER then + + ---------------------------- + -- RECOVERYTANKER Mission -- + ---------------------------- + + -- Get the carrier unit. + local Carrier=self:GetObjective() --Wrapper.Unit#UNIT + + -- Carrier coordinate. + local Coord=Carrier:GetCoordinate() + + -- Get current heading of carrier. + local hdg=Carrier:GetHeading() + + -- Altitude + local Altitude=self.missionAltitude + + -- Race-track distances. + local distStern=UTILS.NMToMeters(4) + local distBow=UTILS.NMToMeters(10) + + -- Racetrack pattern points. + local p1=Coord:Translate(distStern, hdg):SetAltitude(self.missionAltitude) + local p2=Coord:Translate(distBow, hdg):SetAltitude(self.missionAltitude) + + p1:MarkToAll("p1") + p2:MarkToAll("p2") + + -- Set speed in m/s. + local Speed=UTILS.KmphToMps(self.missionSpeed) + + -- Orbit task. + local DCStask=CONTROLLABLE.TaskOrbit(nil, p1, Altitude, Speed, p2) + + -- Set carrier as parameter. + DCStask.params.carrier=Carrier + + -- Add to DCS tasks. + table.insert(DCStasks, DCStask) + elseif self.type==AUFTRAG.Type.INTERCEPT then ----------------------- @@ -124538,98 +133245,141 @@ function AUFTRAG:GetDCSMissionTask(TaskControllable) self:_GetDCSAttackTask(self.engageTarget, DCStasks) elseif self.type==AUFTRAG.Type.ORBIT then - + ------------------- -- ORBIT Mission -- ------------------- - + -- Done below as also other mission types use the orbit task. elseif self.type==AUFTRAG.Type.GCICAP then - + -------------------- -- GCICAP Mission -- -------------------- - + -- Done below as also other mission types use the orbit task. - + elseif self.type==AUFTRAG.Type.RECON then - + ------------------- -- RECON Mission -- - ------------------- + ------------------- - -- TODO: What? Table of coordinates? + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.RECON + + -- We create a "fake" DCS task and pass the parameters to the OPSGROUP. + local param={} + param.target=self.engageTarget + param.altitude=self.missionAltitude + param.speed=self.missionSpeed + param.lastindex=nil + + DCStask.params=param + + table.insert(DCStasks, DCStask) elseif self.type==AUFTRAG.Type.SEAD then - + ------------------ -- SEAD Mission -- - ------------------ + ------------------ --[[ local DCStask=CONTROLLABLE.EnRouteTaskEngageTargets(nil, nil ,{"Air Defence"} , 0) table.insert(self.enrouteTasks, DCStask) DCStask.key="SEAD" ]] - + self:_GetDCSAttackTask(self.engageTarget, DCStasks) - + elseif self.type==AUFTRAG.Type.STRIKE then - + -------------------- -- STRIKE Mission -- - -------------------- - + -------------------- + local DCStask=CONTROLLABLE.TaskAttackMapObject(nil, self:GetTargetVec2(), self.engageAsGroup, self.engageWeaponExpend, self.engageQuantity, self.engageDirection, self.engageAltitude, self.engageWeaponType) - + table.insert(DCStasks, DCStask) - + elseif self.type==AUFTRAG.Type.TANKER then - + -------------------- -- TANKER Mission -- - -------------------- + -------------------- local DCStask=CONTROLLABLE.EnRouteTaskTanker(nil) - - table.insert(self.enrouteTasks, DCStask) - + + table.insert(self.enrouteTasks, DCStask) + elseif self.type==AUFTRAG.Type.TROOPTRANSPORT then ---------------------------- -- TROOPTRANSPORT Mission -- ---------------------------- - + -- Task to embark the troops at the pick up point. local TaskEmbark=CONTROLLABLE.TaskEmbarking(TaskControllable, self.transportPickup, self.transportGroupSet, self.transportWaitForCargo) - + -- Task to disembark the troops at the drop off point. - local TaskDisEmbark=CONTROLLABLE.TaskDisembarking(TaskControllable, self.transportDropoff, self.transportGroupSet) - + local TaskDisEmbark=CONTROLLABLE.TaskDisembarking(TaskControllable, self.transportDropoff, self.transportGroupSet) + table.insert(DCStasks, TaskEmbark) table.insert(DCStasks, TaskDisEmbark) + elseif self.type==AUFTRAG.Type.OPSTRANSPORT then + + -------------------------- + -- OPSTRANSPORT Mission -- + -------------------------- + + local DCStask={} + + DCStask.id="OpsTransport" + + -- We create a "fake" DCS task and pass the parameters to the FLIGHTGROUP. + local param={} + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.CARGOTRANSPORT then + + ---------------------------- + -- CARGOTRANSPORT Mission -- + ---------------------------- + + -- Task to transport cargo. + local TaskCargoTransportation={ + id = "CargoTransportation", + params = {} + } + + table.insert(DCStasks, TaskCargoTransportation) + elseif self.type==AUFTRAG.Type.RESCUEHELO then ------------------------- -- RESCUE HELO Mission -- ------------------------- - + local DCStask={} - - DCStask.id="Formation" - + + DCStask.id=AUFTRAG.SpecialTask.FORMATION + -- We create a "fake" DCS task and pass the parameters to the FLIGHTGROUP. local param={} - param.unitname=self:GetTargetName() --self.carrier:GetName() + param.unitname=self:GetTargetName() param.offsetX=200 param.offsetZ=240 param.altitude=70 param.dtFollow=1.0 - + DCStask.params=param - + table.insert(DCStasks, DCStask) elseif self.type==AUFTRAG.Type.ARTY then @@ -124637,57 +133387,306 @@ function AUFTRAG:GetDCSMissionTask(TaskControllable) ------------------ -- ARTY Mission -- ------------------ - - local DCStask=CONTROLLABLE.TaskFireAtPoint(nil, self:GetTargetVec2(), self.artyRadius, self.artyShots, self.engageWeaponType) + - table.insert(DCStasks, DCStask) + if self.artyShots==1 or self.artyRadius<10 or true then + + local DCStask=CONTROLLABLE.TaskFireAtPoint(nil, self:GetTargetVec2(), self.artyRadius, self.artyShots, self.engageWeaponType, self.artyAltitude) + table.insert(DCStasks, DCStask) + + else + + local Vec2=self:GetTargetVec2() + + local zone=ZONE_RADIUS:New("temp", Vec2, self.artyRadius) + + for i=1,self.artyShots do + + local vec2=zone:GetRandomVec2() + + local DCStask=CONTROLLABLE.TaskFireAtPoint(nil, vec2, 0, 1, self.engageWeaponType, self.artyAltitude) + table.insert(DCStasks, DCStask) + + end + + end + + elseif self.type==AUFTRAG.Type.BARRAGE then + + --------------------- + -- BARRAGE Mission -- + --------------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.BARRAGE + + -- We create a "fake" DCS task and pass the parameters to the FLIGHTGROUP. + local param={} + param.zone=self:GetObjective() + param.altitude=self.artyAltitude + param.radius=self.artyRadius + param.heading=self.artyHeading + param.angle=self.artyAngle + param.shots=self.artyShots + param.weaponTypoe=self.engageWeaponType + + DCStask.params=param + + table.insert(DCStasks, DCStask) elseif self.type==AUFTRAG.Type.PATROLZONE then ------------------------- -- PATROL ZONE Mission -- ------------------------- - + local DCStask={} - - DCStask.id="PatrolZone" - + + DCStask.id=AUFTRAG.SpecialTask.PATROLZONE + -- We create a "fake" DCS task and pass the parameters to the FLIGHTGROUP. local param={} param.zone=self:GetObjective() param.altitude=self.missionAltitude param.speed=self.missionSpeed - + DCStask.params=param - + table.insert(DCStasks, DCStask) - + + elseif self.type==AUFTRAG.Type.CASENHANCED then + + ------------------------- + -- CAS ENHANCED Mission -- + ------------------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.PATROLZONE + + -- We create a "fake" DCS task and pass the parameters to the FLIGHTGROUP. + local param={} + param.zone=self:GetObjective() + param.altitude=self.missionAltitude + param.speed=self.missionSpeed + + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.GROUNDATTACK then + + --------------------------- + -- GROUND ATTACK Mission -- + --------------------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.GROUNDATTACK + + -- We create a "fake" DCS task and pass the parameters to the ARMYGROUP. + local param={} + param.target=self:GetTargetData() + param.action="Wedge" + param.speed=self.missionSpeed + + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.AMMOSUPPLY then + + ------------------------- + -- AMMO SUPPLY Mission -- + ------------------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.AMMOSUPPLY + + -- 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.FUELSUPPLY then + + ------------------------- + -- FUEL SUPPLY Mission -- + ------------------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.FUELSUPPLY + + -- 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.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 + + --------------------- + -- ALERT 5 Mission -- + --------------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.ALERT5 + + -- We create a "fake" DCS task and pass the parameters to the OPSGROUP. + local param={} + + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.NOTHING then + + --------------------- + -- NOTHING Mission -- + --------------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.NOTHING + + -- We create a "fake" DCS task and pass the parameters to the OPSGROUP. + local param={} + + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.HOVER then + + --------------------- + -- HOVER Mission -- + --------------------- + + local DCStask={} + DCStask.id=AUFTRAG.SpecialTask.HOVER + + local param={} + + param.hoverAltitude=self.hoverAltitude + param.hoverTime = self.hoverTime + param.missionSpeed = self.missionSpeed + param.missionAltitude = self.missionAltitude + + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.ONGUARD or self.type==AUFTRAG.Type.ARMOREDGUARD then + + ---------------------- + -- ON GUARD Mission -- + ---------------------- + + local DCStask={} + + DCStask.id= self.type==AUFTRAG.Type.ONGUARD and AUFTRAG.SpecialTask.ONGUARD or AUFTRAG.SpecialTask.ARMOREDGUARD + + -- We create a "fake" DCS task and pass the parameters to the OPSGROUP. + local param={} + param.coordinate=self:GetObjective() + + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.AIRDEFENSE then + + ------------------------ + -- AIRDEFENSE Mission -- + ------------------------ + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.AIRDEFENSE + + -- 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.EWR then + + ----------------- + -- EWR Mission -- + ----------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.EWR + + -- 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) + + -- EWR is an enroute task + local Enroutetask=CONTROLLABLE.EnRouteTaskEWR() + table.insert(self.enrouteTasks, Enroutetask) + else - self:E(self.lid..string.format("ERROR: Unknown mission task!")) + self:T(self.lid..string.format("ERROR: Unknown mission task!")) return nil end - - + + -- Set ORBIT task. Also applies to other missions: AWACS, TANKER, CAP, CAS. - if self.type==AUFTRAG.Type.ORBIT or + if self.type==AUFTRAG.Type.ORBIT or self.type==AUFTRAG.Type.CAP or self.type==AUFTRAG.Type.CAS or self.type==AUFTRAG.Type.GCICAP or - self.type==AUFTRAG.Type.AWACS or + self.type==AUFTRAG.Type.AWACS or self.type==AUFTRAG.Type.TANKER then ------------------- -- ORBIT Mission -- ------------------- - + local Coordinate=self:GetTargetCoordinate() - + local DCStask=CONTROLLABLE.TaskOrbit(nil, Coordinate, self.orbitAltitude, self.orbitSpeed, self.orbitRaceTrack) - + table.insert(DCStasks, DCStask) - + end - + -- Debug info. self:T3({missiontask=DCStasks}) @@ -124708,29 +133707,168 @@ end function AUFTRAG:_GetDCSAttackTask(Target, DCStasks) DCStasks=DCStasks or {} - + for _,_target in pairs(Target.targets) do local target=_target --Ops.Target#TARGET.Object if target.Type==TARGET.ObjectType.GROUP then - + local DCStask=CONTROLLABLE.TaskAttackGroup(nil, target.Object, self.engageWeaponType, self.engageWeaponExpend, self.engageQuantity, self.engageDirection, self.engageAltitude, self.engageAsGroup) - + table.insert(DCStasks, DCStask) - + elseif target.Type==TARGET.ObjectType.UNIT or target.Type==TARGET.ObjectType.STATIC then - + local DCStask=CONTROLLABLE.TaskAttackUnit(nil, target.Object, self.engageAsGroup, self.WeaponExpend, self.engageQuantity, self.engageDirection, self.engageAltitude, self.engageWeaponType) - + table.insert(DCStasks, DCStask) - + end - + end - + return DCStasks end +--- Get DCS task table for an attack group or unit task. +-- @param #AUFTRAG self +-- @param #string MissionType Mission (AUFTAG) type. +-- @return #string DCS mission task for the auftrag type. +function AUFTRAG:GetMissionTaskforMissionType(MissionType) + + local mtask=ENUMS.MissionTask.NOTHING + + if MissionType==AUFTRAG.Type.ANTISHIP then + mtask=ENUMS.MissionTask.ANTISHIPSTRIKE + elseif MissionType==AUFTRAG.Type.AWACS then + mtask=ENUMS.MissionTask.AWACS + elseif MissionType==AUFTRAG.Type.BAI then + mtask=ENUMS.MissionTask.GROUNDATTACK + elseif MissionType==AUFTRAG.Type.BOMBCARPET then + mtask=ENUMS.MissionTask.GROUNDATTACK + elseif MissionType==AUFTRAG.Type.BOMBING then + mtask=ENUMS.MissionTask.GROUNDATTACK + elseif MissionType==AUFTRAG.Type.BOMBRUNWAY then + mtask=ENUMS.MissionTask.RUNWAYATTACK + elseif MissionType==AUFTRAG.Type.CAP then + mtask=ENUMS.MissionTask.CAP + elseif MissionType==AUFTRAG.Type.GCICAP then + mtask=ENUMS.MissionTask.CAP + elseif MissionType==AUFTRAG.Type.CAS then + mtask=ENUMS.MissionTask.CAS + elseif MissionType==AUFTRAG.Type.PATROLZONE then + mtask=ENUMS.MissionTask.CAS + elseif MissionType==AUFTRAG.Type.CASENHANCED then + mtask=ENUMS.MissionTask.CAS + elseif MissionType==AUFTRAG.Type.ESCORT then + mtask=ENUMS.MissionTask.ESCORT + elseif MissionType==AUFTRAG.Type.FACA then + mtask=ENUMS.MissionTask.AFAC + elseif MissionType==AUFTRAG.Type.FERRY then + mtask=ENUMS.MissionTask.NOTHING + elseif MissionType==AUFTRAG.Type.INTERCEPT then + mtask=ENUMS.MissionTask.INTERCEPT + elseif MissionType==AUFTRAG.Type.RECON then + mtask=ENUMS.MissionTask.RECONNAISSANCE + elseif MissionType==AUFTRAG.Type.SEAD then + mtask=ENUMS.MissionTask.SEAD + elseif MissionType==AUFTRAG.Type.STRIKE then + mtask=ENUMS.MissionTask.GROUNDATTACK + elseif MissionType==AUFTRAG.Type.TANKER then + mtask=ENUMS.MissionTask.REFUELING + elseif MissionType==AUFTRAG.Type.TROOPTRANSPORT then + mtask=ENUMS.MissionTask.TRANSPORT + elseif MissionType==AUFTRAG.Type.CARGOTRANSPORT then + mtask=ENUMS.MissionTask.TRANSPORT + elseif MissionType==AUFTRAG.Type.ARMORATTACK then + mtask=ENUMS.MissionTask.NOTHING + elseif MissionType==AUFTRAG.Type.HOVER then + mtask=ENUMS.MissionTask.NOTHING + end + + return mtask +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Global Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Checks if a mission type is contained in a table of possible types. +-- @param #string MissionType The requested mission type. +-- @param #table PossibleTypes A table with possible mission types. +-- @return #boolean If true, the requested mission type is part of the possible mission types. +function AUFTRAG.CheckMissionType(MissionType, PossibleTypes) + + if type(PossibleTypes)=="string" then + PossibleTypes={PossibleTypes} + end + + for _,canmission in pairs(PossibleTypes) do + if canmission==MissionType then + return true + end + end + + return false +end + +--- Check if a mission type is contained in a list of possible capabilities. +-- @param #table MissionTypes The requested mission type. Can also be passed as a single mission type `#string`. +-- @param #table Capabilities A table with possible capabilities `Ops.Auftrag#AUFTRAG.Capability`. +-- @param #boolean All If `true`, given mission type must be includedin ALL capabilities. If `false` or `nil`, it must only match one. +-- @return #boolean If true, the requested mission type is part of the possible mission types. +function AUFTRAG.CheckMissionCapability(MissionTypes, Capabilities, All) + + -- Ensure table. + if type(MissionTypes)~="table" then + MissionTypes={MissionTypes} + end + + for _,cap in pairs(Capabilities) do + local capability=cap --Ops.Auftrag#AUFTRAG.Capability + for _,MissionType in pairs(MissionTypes) do + if All==true then + if capability.MissionType~=MissionType then + return false + end + else + if capability.MissionType==MissionType then + return true + end + end + end + end + + if All==true then + return true + else + return false + end +end + + +--- Check if a mission type is contained in a list of possible capabilities. +-- @param #table MissionTypes The requested mission type. Can also be passed as a single mission type `#string`. +-- @param #table Capabilities A table with possible capabilities `Ops.Auftrag#AUFTRAG.Capability`. +-- @return #boolean If true, the requested mission type is part of the possible mission types. +function AUFTRAG.CheckMissionCapabilityAny(MissionTypes, Capabilities) + + local res=AUFTRAG.CheckMissionCapability(MissionTypes, Capabilities, false) + + return res +end + + +--- Check if a mission type is contained in a list of possible capabilities. +-- @param #table MissionTypes The requested mission type. Can also be passed as a single mission type `#string`. +-- @param #table Capabilities A table with possible capabilities `Ops.Auftrag#AUFTRAG.Capability`. +-- @return #boolean If true, the requested mission type is part of the possible mission types. +function AUFTRAG.CheckMissionCapabilityAll(MissionTypes, Capabilities) + + local res=AUFTRAG.CheckMissionCapability(MissionTypes, Capabilities, true) + + return res +end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -124754,6 +133892,7 @@ end -- @type TARGET -- @field #string ClassName Name of the class. -- @field #number verbose Verbosity level. +-- @field #number uid Unique ID of the target. -- @field #string lid Class id string for output to DCS log file. -- @field #table targets Table of target objects. -- @field #number targetcounter Running number to generate target object IDs. @@ -124767,14 +133906,20 @@ end -- @field #number Ndead Number of target elements/units that are dead (destroyed or despawned). -- @field #table elements Table of target elements/units. -- @field #table casualties Table of dead element names. +-- @field #number prio Priority. +-- @field #number importance Importance. +-- @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 +--- **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 -- -- === -- --- ![Banner Image](..\Presentations\OPS\Target\_Main.pngs) --- -- # The TARGET Concept -- -- Define a target of your mission and monitor its status. Events are triggered when the target is damaged or destroyed. @@ -124797,7 +133942,8 @@ TARGET = { Ndead = 0, elements = {}, casualties = {}, - threatlevel0 = 0 + threatlevel0 = 0, + conditionStart = {}, } @@ -124846,6 +133992,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. @@ -124865,13 +134021,15 @@ _TARGETID=0 --- TARGET class version. -- @field #string version -TARGET.version="0.3.1" +TARGET.version="0.5.2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: A lot. +-- TODO: Had cases where target life was 0 but target was not dead. Need to figure out why! +-- TODO: Add pseudo functions. +-- DONE: Initial object can be nil. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor @@ -124888,26 +134046,23 @@ function TARGET:New(TargetObject) -- Increase counter. _TARGETID=_TARGETID+1 + + -- Set UID. + self.uid=_TARGETID + + if TargetObject then - -- Add object. - self:AddObject(TargetObject) - - -- Get first target. - local Target=self.targets[1] --#TARGET.Object - - if not Target then - self:E("ERROR: No valid TARGET!") - return nil - end + -- Add object. + self:AddObject(TargetObject) - -- Target Name. - self.name=self:GetTargetName(Target) + end - -- Target category. - self.category=self:GetTargetCategory(Target) + -- Defaults. + self:SetPriority() + self:SetImportance() -- Log ID. - self.lid=string.format("TARGET #%03d [%s] | ", _TARGETID, tostring(self.category)) + self.lid=string.format("TARGET #%03d | ", _TARGETID) -- Start state. self:SetStartState("Stopped") @@ -124924,7 +134079,7 @@ function TARGET:New(TargetObject) self:AddTransition("*", "Damaged", "*") -- Target was damaged. self:AddTransition("*", "Destroyed", "Dead") -- Target was completely destroyed. - self:AddTransition("*", "Dead", "Dead") -- Target was completely destroyed. + self:AddTransition("*", "Dead", "Dead") -- Target is dead. Could be destroyed or despawned. ------------------------ --- Pseudo Functions --- @@ -124957,7 +134112,6 @@ function TARGET:New(TargetObject) -- @param #number delay Delay in seconds. - -- Start. self:__Start(-1) @@ -124968,12 +134122,24 @@ end -- User functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Create target data from a given object. +--- Create target data from a given object. Valid objects are: +-- +-- * GROUP +-- * UNIT +-- * STATIC +-- * AIRBASE +-- * COORDINATE +-- * ZONE +-- * SET_GROUP +-- * SET_UNIT +-- * SET_STATIC +-- * SET_OPSGROUP +-- -- @param #TARGET self -- @param Wrapper.Positionable#POSITIONABLE Object The target GROUP, UNIT, STATIC, AIRBASE or COORDINATE. function TARGET:AddObject(Object) - if Object:IsInstanceOf("SET_GROUP") or Object:IsInstanceOf("SET_UNIT") then + if Object:IsInstanceOf("SET_GROUP") or Object:IsInstanceOf("SET_UNIT") or Object:IsInstanceOf("SET_STATIC") or Object:IsInstanceOf("SET_OPSGROUP") then --- -- Sets @@ -124984,31 +134150,217 @@ function TARGET:AddObject(Object) for _,object in pairs(set.Set) do self:AddObject(object) end - + + elseif Object:IsInstanceOf("SET_ZONE") then + + local set=Object --Core.Set#SET_ZONE + + set:SortByName() + + for index,ZoneName in pairs(set.Index) do + local zone=set.Set[ZoneName] --Core.Zone#ZONE + self:_AddObject(zone) + end + else --- -- Groups, Units, Statics, Airbases, Coordinates --- - self:_AddObject(Object) + if Object:IsInstanceOf("OPSGROUP") then + self:_AddObject(Object:GetGroup()) -- We add the MOOSE GROUP object not the OPSGROUP object. + else + self:_AddObject(Object) + end end end +--- Set priority of the target. +-- @param #TARGET self +-- @param #number Priority Priority of the target. Default 50. +-- @return #TARGET self +function TARGET:SetPriority(Priority) + self.prio=Priority or 50 + return self +end + +--- Set importance of the target. +-- @param #TARGET self +-- @param #number Importance Importance of the target. Default `nil`. +-- @return #TARGET self +function TARGET:SetImportance(Importance) + self.importance=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. function TARGET:IsAlive() - return self:Is("Alive") + + for _,_target in pairs(self.targets) do + local target=_target --Ops.Target#TARGET.Object + if target.Status==TARGET.ObjectStatus.ALIVE then + return true + end + end + + return false end +--- Check if TARGET is destroyed. +-- @param #TARGET self +-- @return #boolean If true, target is destroyed. +function TARGET:IsDestroyed() + return self.isDestroyed +end + + --- Check if TARGET is dead. -- @param #TARGET self -- @return #boolean If true, target is dead. function TARGET:IsDead() - return self:Is("Dead") + local is=self:Is("Dead") + return is end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -125059,6 +134411,11 @@ function TARGET:onafterStatus(From, Event, To) damaged=true end + if life==0 then + self:I(self.lid..string.format("FF life is zero but no object dead event fired ==> object dead now for target object %s!", tostring(target.Name))) + self:ObjectDead(target) + end + end -- Target was damaged. @@ -125159,6 +134516,8 @@ function TARGET:onafterObjectDead(From, Event, To, Target) if self.Ndestroyed==self.Ntargets0 then + self.isDestroyed=true + self:Destroyed() else @@ -125428,6 +134787,13 @@ function TARGET:_AddObject(Object) target.Object=Object table.insert(self.targets, target) + + if self.name==nil then + self.name=self:GetTargetName(target) + end + if self.category==nil then + self.category=self:GetTargetCategory(target) + end end @@ -125542,10 +134908,105 @@ function TARGET:GetLife() return N end +--- Get target threat level +-- @param #TARGET self +-- @param #TARGET.Object Target Target object. +-- @return #number Threat level of target. +function TARGET:GetTargetThreatLevelMax(Target) + + if Target.Type==TARGET.ObjectType.GROUP then + + local group=Target.Object --Wrapper.Group#GROUP + + if group and group:IsAlive() then + + local tl=group:GetThreatLevel() + + return tl + else + return 0 + end + + elseif Target.Type==TARGET.ObjectType.UNIT then + + local unit=Target.Object --Wrapper.Unit#UNIT + + if unit and unit:IsAlive() then + + -- Note! According to the profiler, there is a big difference if we "return unit:GetLife()" or "local life=unit:GetLife(); return life"! + local life=unit:GetThreatLevel() + return life + else + return 0 + end + + elseif Target.Type==TARGET.ObjectType.STATIC then + + return 0 + + elseif Target.Type==TARGET.ObjectType.SCENERY then + + return 0 + + elseif Target.Type==TARGET.ObjectType.AIRBASE then + + return 0 + + elseif Target.Type==TARGET.ObjectType.COORDINATE then + + return 0 + + elseif Target.Type==TARGET.ObjectType.ZONE then + + return 0 + + else + self:E("ERROR: unknown target object type in GetTargetThreatLevel!") + end + +end + + +--- Get threat level. +-- @param #TARGET self +-- @return #number Threat level. +function TARGET:GetThreatLevelMax() + + local N=0 + + for _,_target in pairs(self.targets) do + local Target=_target --#TARGET.Object + + local n=self:GetTargetThreatLevelMax(Target) + + if n>N then + N=n + end + + end + + return N +end + +--- Get target 2D position vector. +-- @param #TARGET self +-- @param #TARGET.Object Target Target object. +-- @return DCS#Vec2 Vector with x,y components. +function TARGET:GetTargetVec2(Target) + + local vec3=self:GetTargetVec3(Target) + + if vec3 then + return {x=vec3.x, y=vec3.z} + end + + return nil +end + --- Get target 3D position vector. -- @param #TARGET self -- @param #TARGET.Object Target Target object. --- @return DCS#Vec3 Vector with x,y,z components +-- @return DCS#Vec3 Vector with x,y,z components. function TARGET:GetTargetVec3(Target) if Target.Type==TARGET.ObjectType.GROUP then @@ -125554,8 +135015,12 @@ function TARGET:GetTargetVec3(Target) if object and object:IsAlive() then local vec3=object:GetVec3() - return vec3 + if vec3 then + return vec3 + else + return nil + end else return nil @@ -125692,6 +135157,12 @@ function TARGET:GetTargetName(Target) local coord=Target.Object --Core.Point#COORDINATE return coord:ToStringMGRS() + + elseif Target.Type==TARGET.ObjectType.ZONE then + + local Zone=Target.Object --Core.Zone#ZONE + + return Zone:GetName() end @@ -125702,7 +135173,48 @@ end -- @param #TARGET self -- @return #string Name of the target usually the first object. function TARGET:GetName() - return self.name + local name=self.name or "Unknown" + return name +end + +--- Get 2D vector. +-- @param #TARGET self +-- @return DCS#Vec2 2D vector of the target. +function TARGET:GetVec2() + + for _,_target in pairs(self.targets) do + local Target=_target --#TARGET.Object + + local coordinate=self:GetTargetVec2(Target) + + if coordinate then + return coordinate + end + + end + + self:E(self.lid..string.format("ERROR: Cannot get Vec2 of target %s", self.name)) + return nil +end + +--- Get 3D vector. +-- @param #TARGET self +-- @return DCS#Vec3 3D vector of the target. +function TARGET:GetVec3() + + for _,_target in pairs(self.targets) do + local Target=_target --#TARGET.Object + + local coordinate=self:GetTargetVec3(Target) + + if coordinate then + return coordinate + end + + end + + self:E(self.lid..string.format("ERROR: Cannot get Vec3 of target %s", self.name)) + return nil end --- Get coordinate. @@ -125721,10 +135233,16 @@ function TARGET:GetCoordinate() end - self:E(self.lid..string.format("ERROR: Cannot get coordinate of target %s", self.name)) + self:E(self.lid..string.format("ERROR: Cannot get coordinate of target %s", tostring(self.name))) return nil end +--- Get category. +-- @param #TARGET self +-- @return #string Target category. See `TARGET.Category.X`, where `X=AIRCRAFT, GROUND`. +function TARGET:GetCategory() + return self.category +end --- Get target category. -- @param #TARGET self @@ -125972,13 +135490,13 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- **Ops** - Generic group enhancement. --- +-- -- This class is **not** meant to be used itself by the end user. It contains common functionalities of derived classes for air, ground and sea. --- +-- -- === -- -- ### Author: **funkyfranky** --- +-- -- === -- @module Ops.OpsGroup -- @image OPS_OpsGroup.png @@ -125987,26 +135505,37 @@ end --- OPSGROUP class. -- @type OPSGROUP -- @field #string ClassName Name of the class. --- @field #boolean Debug Debug mode. Messages to all about status. -- @field #number verbose Verbosity level. 0=silent. -- @field #string lid Class id string for output to DCS log file. -- @field #string groupname Name of the group. -- @field Wrapper.Group#GROUP group Group object. --- @field #table template Template of the group. +-- @field DCS#Group dcsgroup The DCS group object. +-- @field DCS#Controller controller The DCS controller of the group. +-- @field DCS#Template template Template table of the group. +-- @field #table elements Table of elements, i.e. units of the group. -- @field #boolean isLateActivated Is the group late activated. -- @field #boolean isUncontrolled Is the group uncontrolled. -- @field #boolean isFlightgroup Is a FLIGHTGROUP. -- @field #boolean isArmygroup Is an ARMYGROUP. -- @field #boolean isNavygroup Is a NAVYGROUP. --- @field #table elements Table of elements, i.e. units of the group. +-- @field #boolean isHelo If true, this is a helicopter group. +-- @field #boolean isVTOL If true, this is capable of Vertical TakeOff and Landing (VTOL). +-- @field #boolean isSubmarine If true, this is a submarine group. -- @field #boolean isAI If true, group is purely AI. --- @field #boolean isAircraft If true, group is airplane or helicopter. --- @field #boolean isNaval If true, group is ships or submarine. --- @field #boolean isGround If true, group is some ground unit. +-- @field #boolean isDestroyed If true, the whole group was destroyed. +-- @field #boolean isDead If true, the whole group is dead. -- @field #table waypoints Table of waypoints. -- @field #table waypoints0 Table of initial waypoints. +-- @field #boolean useMEtasks If `true`, use tasks set in the ME. Default `false`. +-- @field Wrapper.Airbase#AIRBASE homebase The home base of the flight group. +-- @field Wrapper.Airbase#AIRBASE destbase The destination base of the flight group. +-- @field Wrapper.Airbase#AIRBASE currbase The current airbase of the flight group, i.e. where it is currently located or landing at. +-- @field Core.Zone#ZONE homezone The home zone of the flight group. Set when spawn happens in air. +-- @field Core.Zone#ZONE destzone The destination zone of the flight group. Set when final waypoint is in air. -- @field #number currentwp Current waypoint index. This is the index of the last passed waypoint. -- @field #boolean adinfinitum Resume route at first waypoint when final waypoint is reached. +-- @field #number Twaiting Abs. mission time stamp when the group was ordered to wait. +-- @field #number dTwait Time to wait in seconds. Default `nil` (for ever). -- @field #table taskqueue Queue of tasks. -- @field #number taskcounter Running number of task ids. -- @field #number taskcurrent ID of current task. If 0, there is no current task assigned. @@ -126020,21 +135549,28 @@ end -- @field #number speedMax Max speed in km/h. -- @field #number speedCruise Cruising speed in km/h. -- @field #number speedWp Speed to the next waypoint in m/s. +-- @field #boolean isMobile If `true`, group is mobile (speed > 1 m/s) -- @field #boolean passedfinalwp Group has passed the final waypoint. -- @field #number wpcounter Running number counting waypoints. --- @field #boolean respawning Group is being respawned. -- @field Core.Set#SET_ZONE checkzones Set of zones. -- @field Core.Set#SET_ZONE inzones Set of zones in which the group is currently in. +-- @field Core.Timer#TIMER timerStatus Timer for status update. -- @field Core.Timer#TIMER timerCheckZone Timer for check zones. -- @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. +-- +-- @field Ops.Legion#LEGION legion Legion the group belongs to. +-- @field Ops.Cohort#COHORT cohort Cohort the group belongs to. +-- -- @field Core.Point#COORDINATE coordinate Current coordinate. --- +-- -- @field DCS#Vec3 position Position of the group at last status check. -- @field DCS#Vec3 positionLast Backup of last position vec to monitor changes. -- @field #number heading Heading of the group at last status check. @@ -126043,51 +135579,72 @@ end -- @field DCS#Vec3 orientXLast Backup of last orientation to monitor changes. -- @field #number traveldist Distance traveled in meters. This is a lower bound. -- @field #number traveltime Time. --- +-- -- @field Core.Astar#ASTAR Astar path finding. -- @field #boolean ispathfinding If true, group is on pathfinding route. --- +-- +-- @field #boolean engagedetectedOn If `true`, auto engage detected targets. +-- @field #number engagedetectedRmax Max range in NM. Only detected targets within this radius from the group will be engaged. Default is 25 NM. +-- @field #table engagedetectedTypes Types of target attributes that will be engaged. See [DCS enum attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes). Default "All". +-- @field Core.Set#SET_ZONE engagedetectedEngageZones Set of zones in which targets are engaged. Default is anywhere. +-- @field Core.Set#SET_ZONE engagedetectedNoEngageZones Set of zones in which targets are *not* engaged. Default is nowhere. +-- -- @field #OPSGROUP.Radio radio Current radio settings. -- @field #OPSGROUP.Radio radioDefault Default radio settings. -- @field Core.RadioQueue#RADIOQUEUE radioQueue Radio queue. --- +-- -- @field #OPSGROUP.Beacon tacan Current TACAN settings. -- @field #OPSGROUP.Beacon tacanDefault Default TACAN settings. --- +-- -- @field #OPSGROUP.Beacon icls Current ICLS settings. -- @field #OPSGROUP.Beacon iclsDefault Default ICLS settings. --- +-- -- @field #OPSGROUP.Option option Current optional settings. -- @field #OPSGROUP.Option optionDefault Default option settings. --- +-- -- @field #OPSGROUP.Callsign callsign Current callsign settings. -- @field #OPSGROUP.Callsign callsignDefault Default callsign settings. --- +-- @field #string callsignName Callsign name. +-- @field #string callsignAlias Callsign alias. +-- -- @field #OPSGROUP.Spot spot Laser and IR spot. --- +-- -- @field #OPSGROUP.Ammo ammo Initial ammount of ammo. -- @field #OPSGROUP.WeaponData weaponData Weapon data table with key=BitType. --- +-- +-- @field #OPSGROUP.Element carrier Carrier the group is loaded into as cargo. +-- @field #OPSGROUP carrierGroup Carrier group transporting this group as cargo. +-- @field #OPSGROUP.MyCarrier mycarrier Carrier group for this group. +-- @field #table cargoqueue Table containing cargo groups to be transported. +-- @field #table cargoBay Table containing OPSGROUP loaded into this group. +-- @field Ops.OpsTransport#OPSTRANSPORT cargoTransport Current cargo transport assignment. +-- @field Ops.OpsTransport#OPSTRANSPORT.TransportZoneCombo cargoTZC Transport zone combo (pickup, deploy etc.) currently used. +-- @field #string cargoStatus Cargo status of this group acting as cargo. +-- @field #number cargoTransportUID Unique ID of the transport assignment this cargo group is associated with. +-- @field #string carrierStatus Carrier status of this group acting as cargo carrier. +-- @field #OPSGROUP.CarrierLoader carrierLoader Carrier loader parameters. +-- @field #OPSGROUP.CarrierLoader carrierUnloader Carrier unloader parameters. +-- +-- @field #boolean useSRS Use SRS for transmissions. +-- @field Sound.SRS#MSRS msrs MOOSE SRS wrapper. +-- -- @extends Core.Fsm#FSM ---- *A small group of determined and like-minded people can change the course of history.* --- Mahatma Gandhi +--- *A small group of determined and like-minded people can change the course of history.* -- Mahatma Gandhi -- -- === -- --- ![Banner Image](..\Presentations\OPS\OpsGroup\_Main.png) --- -- # The OPSGROUP Concept --- --- The OPSGROUP class contains common functions used by other classes such as FLIGHGROUP, NAVYGROUP and ARMYGROUP. --- Those classes inherit everything of this class and extend it with features specific to their unit category. --- +-- +-- The OPSGROUP class contains common functions used by other classes such as FLIGHTGROUP, NAVYGROUP and ARMYGROUP. +-- Those classes inherit everything of this class and extend it with features specific to their unit category. +-- -- This class is **NOT** meant to be used by the end user itself. --- --- +-- +-- -- @field #OPSGROUP OPSGROUP = { ClassName = "OPSGROUP", - Debug = false, verbose = 0, lid = nil, groupname = nil, @@ -126104,14 +135661,13 @@ OPSGROUP = { taskenroute = nil, taskpaused = {}, missionqueue = {}, - currentmission = nil, + currentmission = nil, detectedunits = {}, detectedgroups = {}, attribute = nil, checkzones = nil, inzones = nil, groupinitialized = nil, - respawning = nil, wpcounter = 1, radio = {}, option = {}, @@ -126121,21 +135677,68 @@ OPSGROUP = { callsign = {}, Ndestroyed = 0, Nkills = 0, + Nhit = 0, weaponData = {}, + cargoqueue = {}, + cargoBay = {}, + mycarrier = {}, + carrierLoader = {}, + carrierUnloader = {}, + useMEtasks = false, + pausedmissions = {}, } --- OPS group element. -- @type OPSGROUP.Element -- @field #string name Name of the element, i.e. the unit. +-- @field #string status The element status. See @{#OPSGROUP.ElementStatus}. -- @field Wrapper.Unit#UNIT unit The UNIT object. --- @field #string status The element status. +-- @field Wrapper.Group#GROUP group The GROUP object. +-- @field DCS#Unit DCSunit The DCS unit object. +-- @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. +-- @field Core.Zone#ZONE_POLYGON_BASE zoneUnload Unloading zone. +-- -- @field #string typename Type name. +-- @field #number category Aircraft category. +-- @field #string categoryname Aircraft category name. +-- +-- @field #number size Size (max of length, width, height) in meters. -- @field #number length Length of element in meters. -- @field #number width Width of element in meters. -- @field #number height Height of element in meters. +-- +-- @field DCS#Vec3 vec3 Last known 3D position vector. +-- @field DCS#Vec3 orientX Last known ordientation vector in the direction of the nose X. +-- @field #number heading Last known heading in degrees. +-- -- @field #number life0 Initial life points. -- @field #number life Life points when last updated. +-- @field #number damage Damage of element in percent. +-- +-- @field DCS#Object.Desc descriptors Descriptors table. +-- @field #number weightEmpty Empty weight in kg. +-- @field #number weightMaxTotal Max. total weight in kg. +-- @field #number weightMaxCargo Max. cargo weight in kg. +-- @field #number weightCargo Current cargo weight in kg. +-- @field #number weight Current weight including cargo in kg. +-- @field #table cargoBay Cargo bay. +-- +-- @field #string modex Tail number. +-- @field Wrapper.Client#CLIENT client The client if element is occupied by a human player. +-- @field #table pylons Table of pylons. +-- @field #number fuelmass Mass of fuel in kg. +-- @field #string callsign Call sign, e.g. "Uzi 1-1". +-- @field Wrapper.Airbase#AIRBASE.ParkingSpot parking The parking spot table the element is parking on. + --- Status of group element. -- @type OPSGROUP.ElementStatus @@ -126151,17 +135754,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. @@ -126189,6 +135814,7 @@ OPSGROUP.TaskType={ --- Task structure. -- @type OPSGROUP.Task -- @field #string type Type of task: either SCHEDULED or WAYPOINT. +-- @field #boolean ismission This is an AUFTRAG task. -- @field #number id Task ID. Running number to get the task. -- @field #number prio Priority. -- @field #number time Abs. mission time when to execute the task. @@ -126201,10 +135827,17 @@ 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. ---- Enroute task. --- @type OPSGROUP.EnrouteTask --- @field DCS#Task DCStask DCS task structure table. --- @field #number WaypointIndex Waypoint number at which the enroute task is added. +--- 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 @@ -126225,18 +135858,7 @@ OPSGROUP.TaskType={ -- @type OPSGROUP.Callsign -- @field #number NumberSquad Squadron number corresponding to a name like "Uzi". -- @field #number NumberGroup Group number. First number after name, e.g. "Uzi-**1**-1". --- @field #number NumberElement Element number.Second number after name, e.g. "Uzi-1-**1**" -- @field #string NameSquad Name of the squad, e.g. "Uzi". --- @field #string NameElement Name of group element, e.g. Uzi 11. - ---- 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. --- Weapon range data. -- @type OPSGROUP.WeaponData @@ -126279,6 +135901,13 @@ OPSGROUP.TaskType={ -- @field #number MissilesAS Amount of anti-ship missiles. -- @field #number MissilesCR Amount of cruise missiles. -- @field #number MissilesBM Amount of ballistic missiles. +-- @field #number MissilesSA Amount of surfe-to-air missiles. + +--- Spawn point data. +-- @type OPSGROUP.Spawnpoint +-- @field Core.Point#COORDINATE Coordinate Coordinate where to spawn +-- @field Wrapper.Airbase#AIRBASE Airport Airport where to spawn. +-- @field #table TerminalIDs Terminal IDs, where to spawn the group. It is a table of `#number`s because a group can consist of multiple units. --- Waypoint data. -- @type OPSGROUP.Waypoint @@ -126291,30 +135920,93 @@ OPSGROUP.TaskType={ -- @field #string name Waypoint description. Shown in the F10 map. -- @field #number x Waypoint x-coordinate. -- @field #number y Waypoint y-coordinate. --- @field #boolean detour If true, this waypoint is not part of the normal route. +-- @field #number detour Signifies that this waypoint is not part of the normal route: 0=Hold, 1=Resume Route. -- @field #boolean intowind If true, this waypoint is a turn into wind route point. -- @field #boolean astar If true, this waypint was found by A* pathfinding algorithm. +-- @field #boolean temp If true, this is a temporary waypoint and will be deleted when passed. Also the passing waypoint FSM event is not triggered. -- @field #number npassed Number of times a groups passed this waypoint. -- @field Core.Point#COORDINATE coordinate Waypoint coordinate. -- @field Core.Point#COORDINATE roadcoord Closest point to road. -- @field #number roaddist Distance to closest point on road. -- @field Wrapper.Marker#MARKER marker Marker on the F10 map. -- @field #string formation Ground formation. Similar to action but on/off road. +-- @field #number missionUID Mission UID (Auftragsnr) this waypoint belongs to. ---- NavyGroup version. +--- Cargo Carrier status. +-- @type OPSGROUP.CarrierStatus +-- @field #string NOTCARRIER This group is not a carrier yet. +-- @field #string PICKUP Carrier is on its way to pickup cargo. +-- @field #string LOADING Carrier is loading cargo. +-- @field #string LOADED Carrier has loaded cargo. +-- @field #string TRANSPORTING Carrier is transporting cargo. +-- @field #string UNLOADING Carrier is unloading cargo. +OPSGROUP.CarrierStatus={ + NOTCARRIER="not carrier", + PICKUP="pickup", + LOADING="loading", + LOADED="loaded", + TRANSPORTING="transporting", + UNLOADING="unloading", +} + +--- Cargo status. +-- @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. (Not used!) +-- @field #string BOARDING Cargo is boarding a carrier. +-- @field #string LOADED Cargo is loaded into a carrier. +OPSGROUP.CargoStatus={ + AWAITING="Awaiting carrier", + NOTCARGO="not cargo", + ASSIGNED="assigned to carrier", + BOARDING="boarding", + LOADED="loaded", +} + +--- Cargo carrier loader parameters. +-- @type OPSGROUP.CarrierLoader +-- @field #string type Loader type "Front", "Back", "Left", "Right", "All". +-- @field #number length Length of (un-)loading zone in meters. +-- @field #number width Width of (un-)loading zone in meters. + +--- Data of the carrier that has loaded this group. +-- @type OPSGROUP.MyCarrier +-- @field #OPSGROUP group The carrier group. +-- @field #OPSGROUP.Element element The carrier element. +-- @field #boolean reserved If `true`, the carrier has caro space reserved for me. + +--- Element cargo bay data. +-- @type OPSGROUP.MyCargo +-- @field #OPSGROUP group The cargo group. +-- @field #boolean reserved If `true`, the cargo bay space is reserved but cargo has not actually been loaded yet. + +--- Cargo group data. +-- @type OPSGROUP.CargoGroup +-- @field #OPSGROUP opsgroup The cargo opsgroup. +-- @field #boolean delivered If `true`, group was delivered. +-- @field #boolean disembarkActivation If `true`, group is activated. If `false`, group is late activated. +-- @field #string status Status of the cargo group. Not used yet. + +--- OpsGroup version. -- @field #string version -OPSGROUP.version="0.7.1" +OPSGROUP.version="0.7.9" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - + -- TODO: AI on/off. --- TODO: Invisible/immortal. -- TODO: F10 menu. -- TODO: Add pseudo function. --- TODO: EPLRS datalink. --- TODO: Emission on/off. +-- 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 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor @@ -126322,70 +136014,146 @@ OPSGROUP.version="0.7.1" --- Create a new OPSGROUP class object. -- @param #OPSGROUP self --- @param Wrapper.Group#GROUP Group The group object. Can also be given by its group name as `#string`. +-- @param Wrapper.Group#GROUP group The GROUP object. Can also be given by its group name as `#string`. -- @return #OPSGROUP self -function OPSGROUP:New(Group) +function OPSGROUP:New(group) -- Inherit everything from FSM class. local self=BASE:Inherit(self, FSM:New()) -- #OPSGROUP - + -- Get group and group name. - if type(Group)=="string" then - self.groupname=Group + if type(group)=="string" then + self.groupname=group self.group=GROUP:FindByName(self.groupname) else - self.group=Group - self.groupname=Group:GetName() + self.group=group + self.groupname=group:GetName() end - + -- Set some string id for output to DCS.log file. self.lid=string.format("OPSGROUP %s | ", tostring(self.groupname)) - + + -- Check if group exists. if self.group then if not self:IsExist() then - self:E(self.lid.."ERROR: GROUP does not exist! Returning nil") + self:T(self.lid.."ERROR: GROUP does not exist! Returning nil") return nil end end + + -- Set the template. + self:_SetTemplate() + + -- Set DCS group and controller. + self.dcsgroup=self:GetDCSGroup() + self.controller=self.dcsgroup:getController() + + -- Category. + self.category=self.dcsgroup:getCategory() + if self.category==Group.Category.GROUND then + self.isArmygroup=true + elseif self.category==Group.Category.TRAIN then + self.isArmygroup=true + self.isTrain=true + elseif self.category==Group.Category.SHIP then + self.isNavygroup=true + elseif self.category==Group.Category.AIRPLANE then + self.isFlightgroup=true + elseif self.category==Group.Category.HELICOPTER then + self.isFlightgroup=true + self.isHelo=true + else + + end + -- Set gen attribute. + self.attribute=self.group:GetAttribute() + + local units=self.group:GetUnits() + + if units then + local masterunit=units[1] --Wrapper.Unit#UNIT + + -- Get Descriptors. + self.descriptors=masterunit:GetDesc() + + -- Set type name. + self.actype=masterunit:GetTypeName() + + -- Is this a submarine. + self.isSubmarine=masterunit:HasAttribute("Submarines") + + -- Has this a datalink? + self.isEPLRS=masterunit:HasAttribute("Datalink") + + if self:IsFlightgroup() then + + self.rangemax=self.descriptors.range and self.descriptors.range*1000 or 500*1000 + + self.ceiling=self.descriptors.Hmax + + self.tankertype=select(2, masterunit:IsTanker()) + self.refueltype=select(2, masterunit:IsRefuelable()) + + --env.info("DCS Unit BOOM_AND_RECEPTACLE="..tostring(Unit.RefuelingSystem.BOOM_AND_RECEPTACLE)) + --env.info("DCS Unit PROBE_AND_DROGUE="..tostring(Unit.RefuelingSystem.PROBE_AND_DROGUE)) + + end + + end + -- Init set of detected units. self.detectedunits=SET_UNIT:New() - + -- Init set of detected groups. self.detectedgroups=SET_GROUP:New() - + -- Init inzone set. self.inzones=SET_ZONE:New() - + + -- Set Default altitude. + self:SetDefaultAltitude() + -- Laser. self.spot={} self.spot.On=false self.spot.timer=TIMER:New(self._UpdateLaser, self) self.spot.Coordinate=COORDINATE:New(0, 0, 0) self:SetLaser(1688, true, false, 0.5) - + + -- Cargo. + self.cargoStatus=OPSGROUP.CargoStatus.NOTCARGO + self.carrierStatus=OPSGROUP.CarrierStatus.NOTCARRIER + self:SetCarrierLoaderAllAspect() + self:SetCarrierUnloaderAllAspect() + -- Init task counter. self.taskcurrent=0 self.taskcounter=0 - + -- Start state. self:SetStartState("InUtero") -- Add FSM transitions. -- From State --> Event --> To State self:AddTransition("InUtero", "Spawned", "Spawned") -- The whole group was spawned. - self:AddTransition("*", "Dead", "Dead") -- The whole group is dead. + self:AddTransition("*", "Respawn", "InUtero") -- Respawn group. + self:AddTransition("*", "Dead", "InUtero") -- The whole group is dead and goes back to mummy. + self:AddTransition("*", "InUtero", "InUtero") -- Deactivated group goes back to mummy. self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. - self:AddTransition("*", "Status", "*") -- Status update. - - 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. + + self:AddTransition("*", "PassingWaypoint", "*") -- Group passed a waypoint. + self:AddTransition("*", "PassedFinalWaypoint", "*") -- Group passed the waypoint. + self:AddTransition("*", "GotoWaypoint", "*") -- Group switches to a specific waypoint. + + self:AddTransition("*", "Wait", "*") -- Group will wait for further orders. - self:AddTransition("*", "UpdateRoute", "*") -- Update route of group. Only if airborne. - self:AddTransition("*", "Respawn", "*") -- Respawn group. - self:AddTransition("*", "PassingWaypoint", "*") -- Passing waypoint. - self:AddTransition("*", "DetectedUnit", "*") -- Unit was detected (again) in this detection cycle. self:AddTransition("*", "DetectedUnitNew", "*") -- Add a newly detected unit to the detected units set. self:AddTransition("*", "DetectedUnitKnown", "*") -- A known unit is still detected. @@ -126394,16 +136162,18 @@ function OPSGROUP:New(Group) self:AddTransition("*", "DetectedGroup", "*") -- Unit was detected (again) in this detection cycle. self:AddTransition("*", "DetectedGroupNew", "*") -- Add a newly detected unit to the detected units set. self:AddTransition("*", "DetectedGroupKnown", "*") -- A known unit is still detected. - self:AddTransition("*", "DetectedGroupLost", "*") -- Group lost a detected target group. - - self:AddTransition("*", "PassingWaypoint", "*") -- Group passed a waypoint. - self:AddTransition("*", "GotoWaypoint", "*") -- Group switches to a specific waypoint. + self:AddTransition("*", "DetectedGroupLost", "*") -- Group lost a detected target group. self:AddTransition("*", "OutOfAmmo", "*") -- Group is completely out of ammo. self:AddTransition("*", "OutOfGuns", "*") -- Group is out of gun shells. self:AddTransition("*", "OutOfRockets", "*") -- Group is out of rockets. self:AddTransition("*", "OutOfBombs", "*") -- Group is out of bombs. self:AddTransition("*", "OutOfMissiles", "*") -- Group is out of missiles. + self:AddTransition("*", "OutOfTorpedos", "*") -- Group is out of torpedos. + + self:AddTransition("*", "OutOfMissilesAA", "*") -- Group is out of A2A (air) missiles. + self:AddTransition("*", "OutOfMissilesAG", "*") -- Group is out of A2G (ground) missiles. + self:AddTransition("*", "OutOfMissilesAS", "*") -- Group is out of A2S (ship) missiles. self:AddTransition("*", "EnterZone", "*") -- Group entered a certain zone. self:AddTransition("*", "LeaveZone", "*") -- Group leaves a certain zone. @@ -126420,24 +136190,47 @@ function OPSGROUP:New(Group) self:AddTransition("*", "TaskPause", "*") -- Pause current task. Not implemented yet! self:AddTransition("*", "TaskCancel", "*") -- Cancel current task. self:AddTransition("*", "TaskDone", "*") -- Task is over. - + self:AddTransition("*", "MissionStart", "*") -- Mission is started. self:AddTransition("*", "MissionExecute", "*") -- Mission execution began. - self:AddTransition("*", "MissionCancel", "*") -- Cancel current mission. + self:AddTransition("*", "MissionCancel", "*") -- Cancel current mission. self:AddTransition("*", "PauseMission", "*") -- Pause the current mission. self:AddTransition("*", "UnpauseMission", "*") -- Unpause the the paused mission. self:AddTransition("*", "MissionDone", "*") -- Mission is over. + self:AddTransition("*", "ElementInUtero", "*") -- An element is in utero again. self:AddTransition("*", "ElementSpawned", "*") -- An element was spawned. 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. + self:AddTransition("*", "Disembarked", "*") -- Group was unloaded from a cargo carrier. + + self:AddTransition("*", "Pickup", "*") -- Carrier and is on route to pick up cargo. + self:AddTransition("*", "Loading", "*") -- Carrier is loading cargo. + self:AddTransition("*", "Load", "*") -- Carrier loads cargo into carrier. + self:AddTransition("*", "Loaded", "*") -- Carrier loaded cargo into carrier. + self:AddTransition("*", "LoadingDone", "*") -- Carrier loaded all assigned/possible cargo into carrier. + self:AddTransition("*", "Transport", "*") -- Carrier is transporting cargo. + self:AddTransition("*", "Unloading", "*") -- Carrier is unloading the cargo. + self:AddTransition("*", "Unload", "*") -- Carrier unloads a cargo group. + self:AddTransition("*", "Unloaded", "*") -- Carrier unloaded a cargo group. + self:AddTransition("*", "UnloadingDone", "*") -- Carrier unloaded all its current cargo. + self:AddTransition("*", "Delivered", "*") -- Carrier delivered ALL cargo of the transport assignment. + + self:AddTransition("*", "TransportCancel", "*") -- Cancel (current) transport. + + self:AddTransition("*", "HoverStart", "*") -- Helo group is hovering + self:AddTransition("*", "HoverEnd", "*") -- Helo group is flying on ------------------------ --- Pseudo Functions --- ------------------------ --- 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. @@ -126454,9 +136247,122 @@ function OPSGROUP:New(Group) -- @param #OPSGROUP self -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "MissionStart". + -- @function [parent=#OPSGROUP] MissionStart + -- @param #OPSGROUP self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "MissionStart" after a delay. + -- @function [parent=#OPSGROUP] __MissionStart + -- @param #OPSGROUP self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "MissionStart" event. + -- @function [parent=#OPSGROUP] OnAfterMissionStart + -- @param #OPSGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "MissionExecute". + -- @function [parent=#OPSGROUP] MissionExecute + -- @param #OPSGROUP self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "MissionExecute" after a delay. + -- @function [parent=#OPSGROUP] __MissionExecute + -- @param #OPSGROUP self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "MissionExecute" event. + -- @function [parent=#OPSGROUP] OnAfterMissionExecute + -- @param #OPSGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "MissionCancel". + -- @function [parent=#OPSGROUP] MissionCancel + -- @param #OPSGROUP self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "MissionCancel" after a delay. + -- @function [parent=#OPSGROUP] __MissionCancel + -- @param #OPSGROUP self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "MissionCancel" event. + -- @function [parent=#OPSGROUP] OnAfterMissionCancel + -- @param #OPSGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "MissionDone". + -- @function [parent=#OPSGROUP] MissionDone + -- @param #OPSGROUP self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "MissionDone" after a delay. + -- @function [parent=#OPSGROUP] __MissionDone + -- @param #OPSGROUP self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "MissionDone" event. + -- @function [parent=#OPSGROUP] OnAfterMissionDone + -- @param #OPSGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "HoverStart" event. + -- @function [parent=#OPSGROUP] OnAfterHoverStart + -- @param #OPSGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On after "HoverEnd" event. + -- @function [parent=#OPSGROUP] OnAfterHoverEnd + -- @param #OPSGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- Triggers the FSM event "TransportCancel". + -- @function [parent=#OPSGROUP] TransportCancel + -- @param #OPSGROUP self + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- Triggers the FSM event "TransportCancel" after a delay. + -- @function [parent=#OPSGROUP] __TransportCancel + -- @param #OPSGROUP self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- On after "TransportCancel" event. + -- @function [parent=#OPSGROUP] OnAfterTransportCancel + -- @param #OPSGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + -- TODO: Add pseudo functions. - return self + return self end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -126472,14 +136378,43 @@ end --- Returns the absolute (average) life points of the group. -- @param #OPSGROUP self +-- @param #OPSGROUP.Element Element (Optional) Only get life points of this element. -- @return #number Life points. If group contains more than one element, the average is given. -- @return #number Initial life points. -function OPSGROUP:GetLifePoints() - if self.group then - return self.group:GetLife(), self.group:GetLife0() +function OPSGROUP:GetLifePoints(Element) + + local life=0 + local life0=0 + + if Element then + + local unit=Element.unit + + if unit then + life=unit:GetLife() + life0=unit:GetLife0() + life=math.min(life, life0) -- Some units have more life than life0 returns! + end + + else + + for _,element in pairs(self.elements) do + local l,l0=self:GetLifePoints(element) + life=life+l + life0=life+l0 + end + end + + return life, life0 end +--- Get generalized attribute. +-- @param #OPSGROUP self +-- @return #string Generalized attribute. +function OPSGROUP:GetAttribute() + return self.attribute +end --- Set verbosity level. -- @param #OPSGROUP self @@ -126490,6 +136425,16 @@ function OPSGROUP:SetVerbosity(VerbosityLevel) return self end +--- Set legion this ops group belongs to. +-- @param #OPSGROUP self +-- @param Ops.Legion#LEGION Legion The Legion. +-- @return #OPSGROUP self +function OPSGROUP:_SetLegion(Legion) + self:T2(self.lid..string.format("Adding opsgroup to legion %s", Legion.alias)) + self.legion=Legion + return self +end + --- Set default cruise speed. -- @param #OPSGROUP self -- @param #number Speed Speed in knots. @@ -126505,20 +136450,330 @@ end -- @param #OPSGROUP self -- @return #number Cruise speed (>0) in knots. function OPSGROUP:GetSpeedCruise() - return UTILS.KmphToKnots(self.speedCruise or self.speedMax*0.7) + local speed=UTILS.KmphToKnots(self.speedCruise or self.speedMax*0.7) + return speed +end + +--- Set default cruise altitude. +-- @param #OPSGROUP self +-- @param #number Altitude Altitude in feet. Default is 10,000 ft for airplanes and 1,500 feet for helicopters. +-- @return #OPSGROUP self +function OPSGROUP:SetDefaultAltitude(Altitude) + if Altitude then + self.altitudeCruise=UTILS.FeetToMeters(Altitude) + else + if self:IsFlightgroup() then + if self.isHelo then + self.altitudeCruise=UTILS.FeetToMeters(1500) + else + self.altitudeCruise=UTILS.FeetToMeters(10000) + end + else + self.altitudeCruise=0 + end + end + return self +end + +--- Get default cruise speed. +-- @param #OPSGROUP self +-- @return #number Cruise altitude in feet. +function OPSGROUP:GetCruiseAltitude() + local alt=UTILS.MetersToFeet(self.altitudeCruise) + return alt +end + +--- Set current altitude. +-- @param #OPSGROUP self +-- @param #number Altitude Altitude in feet. Default is 10,000 ft for airplanes and 1,500 feet for helicopters. +-- @param #boolean Keep If `true` the group will maintain that speed on passing waypoints. If `nil` or `false` the group will return to the speed as defined by their route. +-- @return #OPSGROUP self +function OPSGROUP:SetAltitude(Altitude, Keep, RadarAlt) + if Altitude then + Altitude=UTILS.FeetToMeters(Altitude) + else + if self:IsFlightgroup() then + if self.isHelo then + Altitude=UTILS.FeetToMeters(1500) + else + Altitude=UTILS.FeetToMeters(10000) + end + else + Altitude=0 + end + end + + local AltType="BARO" + if RadarAlt then + AltType="RADIO" + end + + if self.controller then + self.controller:setAltitude(Altitude, Keep, AltType) + end + + return self +end + +--- Set current altitude. +-- @param #OPSGROUP self +-- @return #number Altitude in feet. +function OPSGROUP:GetAltitude() + + local alt=0 + + if self.group then + + alt=self.group:GetAltitude() + + alt=UTILS.MetersToFeet(alt) + + end + + return alt +end + +--- Set current speed. +-- @param #OPSGROUP self +-- @param #number Speed Speed in knots. Default is 70% of max speed. +-- @param #boolean Keep If `true` the group will maintain that speed on passing waypoints. If `nil` or `false` the group will return to the speed as defined by their route. +-- @param #boolean AltCorrected If `true`, use altitude corrected indicated air speed. +-- @return #OPSGROUP self +function OPSGROUP:SetSpeed(Speed, Keep, AltCorrected) + if Speed then + + else + Speed=UTILS.KmphToKnots(self.speedMax) + end + + + if AltCorrected then + local altitude=self:GetAltitude() + Speed=UTILS.KnotsToAltKIAS(Speed, altitude) + end + + Speed=UTILS.KnotsToMps(Speed) + + if self.controller then + self.controller:setSpeed(Speed, Keep) + end + + return self end --- Set detection on or off. --- If detection is on, detected targets of the group will be evaluated and FSM events triggered. +-- If detection is on, detected targets of the group will be evaluated and FSM events triggered. -- @param #OPSGROUP self -- @param #boolean Switch If `true`, detection is on. If `false` or `nil`, detection is off. Default is off. -- @return #OPSGROUP self function OPSGROUP:SetDetection(Switch) + self:T(self.lid..string.format("Detection is %s", tostring(Switch))) self.detectionOn=Switch return self end ---- Set LASER parameters. +--- Get DCS group object. +-- @param #OPSGROUP self +-- @return DCS#Group DCS group object. +function OPSGROUP:GetDCSObject() + return self.dcsgroup +end + +--- Set detection on or off. +-- If detection is on, detected targets of the group will be evaluated and FSM events triggered. +-- @param #OPSGROUP self +-- @param Wrapper.Positionable#POSITIONABLE TargetObject The target object. +-- @param #boolean KnowType Make type known. +-- @param #boolean KnowDist Make distance known. +-- @param #number Delay Delay in seconds before the target is known. +-- @return #OPSGROUP self +function OPSGROUP:KnowTarget(TargetObject, KnowType, KnowDist, Delay) + + if Delay and Delay>0 then + -- Delayed call. + self:ScheduleOnce(Delay, OPSGROUP.KnowTarget, self, TargetObject, KnowType, KnowDist, 0) + else + + if TargetObject:IsInstanceOf("GROUP") then + TargetObject=TargetObject:GetUnit(1) + elseif TargetObject:IsInstanceOf("OPSGROUP") then + TargetObject=TargetObject.group:GetUnit(1) + end + + -- Get the DCS object. + local object=TargetObject:GetDCSObject() + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if element.controller then + element.controller:knowTarget(object, true, true) + --self:T(self.lid..string.format("Element %s should now know target %s", element.name, TargetObject:GetName())) + end + end + + -- Debug info. + self:T(self.lid..string.format("We should now know target %s", TargetObject:GetName())) + + end + + return self +end + +--- Check if target is detected. +-- @param #OPSGROUP self +-- @param Wrapper.Positionable#POSITIONABLE TargetObject The target object. +-- @return #boolean If `true`, target was detected. +function OPSGROUP:IsTargetDetected(TargetObject) + + local objects={} + + if TargetObject:IsInstanceOf("GROUP") then + for _,unit in pairs(TargetObject:GetUnits()) do + table.insert(objects, unit:GetDCSObject()) + end + elseif TargetObject:IsInstanceOf("OPSGROUP") then + for _,unit in pairs(TargetObject.group:GetUnits()) do + table.insert(objects, unit:GetDCSObject()) + end + elseif TargetObject:IsInstanceOf("UNIT") or TargetObject:IsInstanceOf("STATIC") then + table.insert(objects, TargetObject:GetDCSObject()) + end + + for _,object in pairs(objects or {}) do + + -- Check group controller. + local detected, visible, lastTime, type, distance, lastPos, lastVel = self.controller:isTargetDetected(object, 1, 2, 4, 8, 16, 32) + + --env.info(self.lid..string.format("Detected target %s: %s", TargetObject:GetName(), tostring(detected))) + + if detected then + return true + end + + + -- Check all elements. + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if element.controller then + + -- Check. + local detected, visible, lastTime, type, distance, lastPos, lastVel= + element.controller:isTargetDetected(object, 1, 2, 4, 8, 16, 32) + + --env.info(self.lid..string.format("Element %s detected target %s: %s", element.name, TargetObject:GetName(), tostring(detected))) + + if detected then + return true + end + + end + end + + end + + 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 dist=ThreatLevelMin and threatlevel<=ThreatLevelMax then - + if threatlevellevelmax then threat=unit levelmax=threatlevel end - + end return threat, levelmax end +--- Enable to automatically engage detected targets. +-- @param #OPSGROUP self +-- @param #number RangeMax Max range in NM. Only detected targets within this radius from the group will be engaged. Default is 25 NM. +-- @param #table TargetTypes Types of target attributes that will be engaged. See [DCS enum attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes). Default "All". +-- @param Core.Set#SET_ZONE EngageZoneSet Set of zones in which targets are engaged. Default is anywhere. +-- @param Core.Set#SET_ZONE NoEngageZoneSet Set of zones in which targets are *not* engaged. Default is nowhere. +-- @return #OPSGROUP self +function OPSGROUP:SetEngageDetectedOn(RangeMax, TargetTypes, EngageZoneSet, NoEngageZoneSet) + + -- Ensure table. + if TargetTypes then + if type(TargetTypes)~="table" then + TargetTypes={TargetTypes} + end + else + TargetTypes={"All"} + end + + -- Ensure SET_ZONE if ZONE is provided. + if EngageZoneSet and EngageZoneSet:IsInstanceOf("ZONE_BASE") then + local zoneset=SET_ZONE:New():AddZone(EngageZoneSet) + EngageZoneSet=zoneset + end + if NoEngageZoneSet and NoEngageZoneSet:IsInstanceOf("ZONE_BASE") then + local zoneset=SET_ZONE:New():AddZone(NoEngageZoneSet) + NoEngageZoneSet=zoneset + end + + -- Set parameters. + self.engagedetectedOn=true + self.engagedetectedRmax=UTILS.NMToMeters(RangeMax or 25) + self.engagedetectedTypes=TargetTypes + self.engagedetectedEngageZones=EngageZoneSet + self.engagedetectedNoEngageZones=NoEngageZoneSet + + -- Debug info. + self:T(self.lid..string.format("Engage detected ON: Rmax=%d NM", UTILS.MetersToNM(self.engagedetectedRmax))) + + -- Ensure detection is ON or it does not make any sense. + self:SetDetection(true) + + return self +end + +--- Disable to automatically engage detected targets. +-- @param #OPSGROUP self +-- @return #OPSGROUP self +function OPSGROUP:SetEngageDetectedOff() + self:T(self.lid..string.format("Engage detected OFF")) + self.engagedetectedOn=false + return self +end + +--- Set that group is going to rearm once it runs out of ammo. +-- @param #OPSGROUP self +-- @return #OPSGROUP self +function OPSGROUP:SetRearmOnOutOfAmmo() + self.rearmOnOutOfAmmo=true + return self +end + +--- Set that group is retreating once it runs out of ammo. +-- @param #OPSGROUP self +-- @return #OPSGROUP self +function OPSGROUP:SetRetreatOnOutOfAmmo() + self.retreatOnOutOfAmmo=true + return self +end + +--- Set that group is return to legion once it runs out of ammo. +-- @param #OPSGROUP self +-- @return #OPSGROUP self +function OPSGROUP:SetReturnOnOutOfAmmo() + self.rtzOnOutOfAmmo=true + return self +end + + --- Check if an element of the group has line of sight to a coordinate. -- @param #OPSGROUP self -- @param Core.Point#COORDINATE Coordinate The position to which we check the LoS. @@ -126715,8 +137048,8 @@ function OPSGROUP:HasLoS(Coordinate, Element, OffsetElement, OffsetCoordinate) end --- Function to check LoS for an element of the group. - local function checklos(element) - local vec3=element.unit:GetVec3() + local function checklos(element) + local vec3=element.unit:GetVec3() if OffsetElement then vec3=UTILS.VecAdd(vec3, OffsetElement) end @@ -126725,19 +137058,19 @@ function OPSGROUP:HasLoS(Coordinate, Element, OffsetElement, OffsetCoordinate) return _los end - if Element then + if Element then local los=checklos(Element) return los else - + for _,element in pairs(self.elements) do -- Get LoS of this element. - local los=checklos(element) + local los=checklos(element) if los then return true end end - + return false end @@ -126773,12 +137106,12 @@ end function OPSGROUP:GetUnit(UnitNumber) local DCSUnit=self:GetDCSUnit(UnitNumber) - + if DCSUnit then local unit=UNIT:Find(DCSUnit) return unit end - + return nil end @@ -126789,12 +137122,12 @@ end function OPSGROUP:GetDCSUnit(UnitNumber) local DCSGroup=self:GetDCSGroup() - + if DCSGroup then local unit=DCSGroup:getUnit(UnitNumber or 1) return unit end - + return nil end @@ -126804,132 +137137,24 @@ end function OPSGROUP:GetDCSUnits() local DCSGroup=self:GetDCSGroup() - + if DCSGroup then local units=DCSGroup:getUnits() return units end - + return nil end ---- Despawn the group. The whole group is despawned and (optionally) a "Remove Unit" event is generated for all current units of the group. --- @param #OPSGROUP self --- @param #number Delay Delay in seconds before the group will be despawned. Default immediately. --- @param #boolean NoEventRemoveUnit If true, no event "Remove Unit" is generated. --- @return #OPSGROUP self -function OPSGROUP:Despawn(Delay, NoEventRemoveUnit) - - if Delay and Delay>0 then - self:ScheduleOnce(Delay, OPSGROUP.Despawn, self, 0, NoEventRemoveUnit) - else - - local DCSGroup=self:GetDCSGroup() - - if DCSGroup then - - -- Destroy DCS group. - DCSGroup:destroy() - - if not NoEventRemoveUnit then - - -- Get all units. - local units=self:GetDCSUnits() - - -- Create a "Remove Unit" event. - local EventTime=timer.getTime() - for i=1,#units do - self:CreateEventRemoveUnit(EventTime, units[i]) - end - - end - end - end - - return self -end - ---- Destroy group. The whole group is despawned and a *Unit Lost* for aircraft or *Dead* event for ground/naval units is generated for all current units. --- @param #OPSGROUP self --- @param #number Delay Delay in seconds before the group will be destroyed. Default immediately. --- @return #OPSGROUP self -function OPSGROUP:Destroy(Delay) - - if Delay and Delay>0 then - self:ScheduleOnce(Delay, OPSGROUP.Destroy, self) - else - - local DCSGroup=self:GetDCSGroup() - - if DCSGroup then - - self:T(self.lid.."Destroying group") - - -- Destroy DCS group. - DCSGroup:destroy() - - -- Get all units. - local units=self:GetDCSUnits() - - -- Create a "Unit Lost" event. - local EventTime=timer.getTime() - for i=1,#units do - if self.isAircraft then - self:CreateEventUnitLost(EventTime, units[i]) - else - self:CreateEventDead(EventTime, units[i]) - end - end - end - - end - - return self -end - ---- Despawn an element/unit of the group. --- @param #OPSGROUP self --- @param #OPSGROUP.Element Element The element that will be despawned. --- @param #number Delay Delay in seconds before the element will be despawned. Default immediately. --- @param #boolean NoEventRemoveUnit If true, no event "Remove Unit" is generated. --- @return #OPSGROUP self -function OPSGROUP:DespawnElement(Element, Delay, NoEventRemoveUnit) - - if Delay and Delay>0 then - self:ScheduleOnce(Delay, OPSGROUP.DespawnElement, self, Element, 0, NoEventRemoveUnit) - else - - if Element then - - -- Get DCS unit object. - local DCSunit=Unit.getByName(Element.name) - - if DCSunit then - - -- Destroy object. - DCSunit:destroy() - - -- Create a remove unit event. - if not NoEventRemoveUnit then - self:CreateEventRemoveUnit(timer.getTime(), DCSunit) - end - - end - - end - - end - - return self -end --- Get current 2D position vector of the group. -- @param #OPSGROUP self +-- @param #string UnitName (Optional) Get position of a specifc unit of the group. Default is the first existing unit in the group. -- @return DCS#Vec2 Vector with x,y components. -function OPSGROUP:GetVec2() +function OPSGROUP:GetVec2(UnitName) + + local vec3=self:GetVec3(UnitName) - local vec3=self:GetVec3() - if vec3 then local vec2={x=vec3.x, y=vec3.z} return vec2 @@ -126941,34 +137166,60 @@ end --- Get current 3D position vector of the group. -- @param #OPSGROUP self +-- @param #string UnitName (Optional) Get position of a specifc unit of the group. Default is the first existing unit in the group. -- @return DCS#Vec3 Vector with x,y,z components. -function OPSGROUP:GetVec3() - if self:IsExist() then - - local unit=self:GetDCSUnit() - - if unit then - local vec3=unit:getPoint() - +function OPSGROUP:GetVec3(UnitName) + + local vec3=nil --DCS#Vec3 + + -- First check if this group is loaded into a carrier + local carrier=self:_GetMyCarrierElement() + if carrier and carrier.status~=OPSGROUP.ElementStatus.DEAD and self:IsLoaded() then + local unit=carrier.unit + if unit and unit:IsAlive()~=nil then + vec3=unit:GetVec3() return vec3 end - end + + if self:IsExist() then + + local unit=nil --DCS#Unit + if UnitName then + unit=Unit.getByName(UnitName) + else + unit=self:GetDCSUnit() + end + + + if unit then + local vec3=unit:getPoint() + return vec3 + end + + end + + -- Return last known position. + if self.position then + return self.position + end + return nil end ---- Get current coordinate of the group. +--- Get current coordinate of the group. If the current position cannot be determined, the last known position is returned. -- @param #OPSGROUP self -- @param #boolean NewObject Create a new coordiante object. +-- @param #string UnitName (Optional) Get position of a specifc unit of the group. Default is the first existing unit in the group. -- @return Core.Point#COORDINATE The coordinate (of the first unit) of the group. -function OPSGROUP:GetCoordinate(NewObject) +function OPSGROUP:GetCoordinate(NewObject, UnitName) - local vec3=self:GetVec3() + local vec3=self:GetVec3(UnitName) or self.position --DCS#Vec3 if vec3 then - + self.coordinate=self.coordinate or COORDINATE:New(0,0,0) - + self.coordinate.x=vec3.x self.coordinate.y=vec3.y self.coordinate.z=vec3.z @@ -126978,100 +137229,126 @@ function OPSGROUP:GetCoordinate(NewObject) return coord else return self.coordinate - end + end else - self:E(self.lid.."WARNING: Group is not alive. Cannot get coordinate!") + self:T(self.lid.."WARNING: Cannot get coordinate!") end - + return nil end --- Get current velocity of the group. -- @param #OPSGROUP self +-- @param #string UnitName (Optional) Get heading of a specific unit of the group. Default is from the first existing unit in the group. -- @return #number Velocity in m/s. -function OPSGROUP:GetVelocity() +function OPSGROUP:GetVelocity(UnitName) + if self:IsExist() then - - local unit=self:GetDCSUnit(1) - - if unit then - - local velvec3=unit:getVelocity() - - local vel=UTILS.VecNorm(velvec3) - - return vel - + + local unit=nil --DCS#Unit + + if UnitName then + unit=Unit.getByName(UnitName) + else + unit=self:GetDCSUnit() end + + if unit then + + local velvec3=unit:getVelocity() + + local vel=UTILS.VecNorm(velvec3) + + return vel + + else + self:T(self.lid.."WARNING: Unit does not exist. Cannot get velocity!") + end + else - self:E(self.lid.."WARNING: Group does not exist. Cannot get velocity!") + self:T(self.lid.."WARNING: Group does not exist. Cannot get velocity!") end + return nil end ---- Get current heading of the group. +--- Get current heading of the group or (optionally) of a specific unit of the group. -- @param #OPSGROUP self +-- @param #string UnitName (Optional) Get heading of a specific unit of the group. Default is from the first existing unit in the group. -- @return #number Current heading of the group in degrees. -function OPSGROUP:GetHeading() +function OPSGROUP:GetHeading(UnitName) if self:IsExist() then - - local unit=self:GetDCSUnit() - + + local unit=nil --DCS#Unit + if UnitName then + unit=Unit.getByName(UnitName) + else + unit=self:GetDCSUnit() + end + if unit then - + local pos=unit:getPosition() - + local heading=math.atan2(pos.x.z, pos.x.x) - + if heading<0 then heading=heading+ 2*math.pi end - + heading=math.deg(heading) - + return heading end - + else - self:E(self.lid.."WARNING: Group does not exist. Cannot get heading!") + self:T(self.lid.."WARNING: Group does not exist. Cannot get heading!") end - + return nil end ---- Get current orientation of the first unit in the group. +--- Get current orientation of the group. -- @param #OPSGROUP self +-- @param #string UnitName (Optional) Get orientation of a specific unit of the group. Default is the first existing unit of the group. -- @return DCS#Vec3 Orientation X parallel to where the "nose" is pointing. -- @return DCS#Vec3 Orientation Y pointing "upwards". -- @return DCS#Vec3 Orientation Z perpendicular to the "nose". -function OPSGROUP:GetOrientation() +function OPSGROUP:GetOrientation(UnitName) if self:IsExist() then - - local unit=self:GetDCSUnit() - + + local unit=nil --DCS#Unit + + if UnitName then + unit=Unit.getByName(UnitName) + else + unit=self:GetDCSUnit() + end + if unit then - + local pos=unit:getPosition() - + return pos.x, pos.y, pos.z end - + else - self:E(self.lid.."WARNING: Group does not exist. Cannot get orientation!") + self:T(self.lid.."WARNING: Group does not exist. Cannot get orientation!") end - + return nil end ---- Get current orientation of the first unit in the group. +--- Get current "X" orientation of the first unit in the group. -- @param #OPSGROUP self +-- @param #string UnitName (Optional) Get orientation of a specific unit of the group. Default is the first existing unit of the group. -- @return DCS#Vec3 Orientation X parallel to where the "nose" is pointing. -function OPSGROUP:GetOrientationX() +function OPSGROUP:GetOrientationX(UnitName) + + local X,Y,Z=self:GetOrientation(UnitName) - local X,Y,Z=self:GetOrientation() - return X end @@ -127095,6 +137372,209 @@ function OPSGROUP:CheckTaskDescriptionUnique(description) end +--- Despawn a unit of the group. A "Remove Unit" event is generated by default. +-- @param #OPSGROUP self +-- @param #string UnitName Name of the unit +-- @param #number Delay Delay in seconds before the group will be despawned. Default immediately. +-- @param #boolean NoEventRemoveUnit If true, no event "Remove Unit" is generated. +-- @return #OPSGROUP self +function OPSGROUP:DespawnUnit(UnitName, Delay, NoEventRemoveUnit) + + -- Debug info. + self:T(self.lid.."Despawn element "..tostring(UnitName)) + + -- Get element. + local element=self:GetElementByName(UnitName) + + if element then + + -- Get DCS unit object. + local DCSunit=Unit.getByName(UnitName) + + if DCSunit then + + -- Despawn unit. + DCSunit:destroy() + + -- Element goes back in utero. + self:ElementInUtero(element) + + if not NoEventRemoveUnit then + self:CreateEventRemoveUnit(timer.getTime(), DCSunit) + end + + end + + end + +end + +--- Despawn an element/unit of the group. +-- @param #OPSGROUP self +-- @param #OPSGROUP.Element Element The element that will be despawned. +-- @param #number Delay Delay in seconds before the element will be despawned. Default immediately. +-- @param #boolean NoEventRemoveUnit If true, no event "Remove Unit" is generated. +-- @return #OPSGROUP self +function OPSGROUP:DespawnElement(Element, Delay, NoEventRemoveUnit) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP.DespawnElement, self, Element, 0, NoEventRemoveUnit) + else + + if Element then + + -- Get DCS unit object. + local DCSunit=Unit.getByName(Element.name) + + if DCSunit then + + -- Destroy object. + DCSunit:destroy() + + -- Create a remove unit event. + if not NoEventRemoveUnit then + self:CreateEventRemoveUnit(timer.getTime(), DCSunit) + end + + end + + end + + end + + return self +end + +--- Despawn the group. The whole group is despawned and a "`Remove Unit`" event is generated for all current units of the group. +-- If no `Remove Unit` event should be generated, the second optional parameter needs to be set to `true`. +-- If this group belongs to an AIRWING, BRIGADE or FLEET, it will be added to the warehouse stock if the `NoEventRemoveUnit` parameter is `false` or `nil`. +-- @param #OPSGROUP self +-- @param #number Delay Delay in seconds before the group will be despawned. Default immediately. +-- @param #boolean NoEventRemoveUnit If `true`, **no** event "Remove Unit" is generated. +-- @return #OPSGROUP self +function OPSGROUP:Despawn(Delay, NoEventRemoveUnit) + + if Delay and Delay>0 then + self.scheduleIDDespawn=self:ScheduleOnce(Delay, OPSGROUP.Despawn, self, 0, NoEventRemoveUnit) + else + + -- Debug info. + self:T(self.lid..string.format("Despawning Group!")) + + -- DCS group obejct. + local DCSGroup=self:GetDCSGroup() + + if DCSGroup then + + -- Clear any task ==> makes DCS crash! + --self.group:ClearTasks() + + -- Get all units. + local units=self:GetDCSUnits() + + for i=1,#units do + local unit=units[i] + if unit then + local name=unit:getName() + if name then + -- Despawn the unit. + self:DespawnUnit(name, 0, NoEventRemoveUnit) + end + end + end + + end + end + + return self +end + +--- Return group back to the legion it belongs to. +-- Group is despawned and added back to the stock. +-- @param #OPSGROUP self +-- @param #number Delay Delay in seconds before the group will be despawned. Default immediately +-- @return #OPSGROUP self +function OPSGROUP:ReturnToLegion(Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP.ReturnToLegion, self) + else + + if self.legion then + -- Add asset back. + self:T(self.lid..string.format("Adding asset back to LEGION")) + self.legion:AddAsset(self.group, 1) + else + self:E(self.lid..string.format("ERROR: Group does not belong to a LEGION!")) + end + + end + + return self +end + +--- Destroy a unit of the group. A *Unit Lost* for aircraft or *Dead* event for ground/naval units is generated. +-- @param #OPSGROUP self +-- @param #string UnitName Name of the unit which should be destroyed. +-- @param #number Delay Delay in seconds before the group will be destroyed. Default immediately. +-- @return #OPSGROUP self +function OPSGROUP:DestroyUnit(UnitName, Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP.DestroyUnit, self, UnitName, 0) + else + + local unit=Unit.getByName(UnitName) + + if unit then + + -- Create a "Unit Lost" event. + local EventTime=timer.getTime() + + if self:IsFlightgroup() then + self:CreateEventUnitLost(EventTime, unit) + else + self:CreateEventDead(EventTime, unit) + end + + -- Despawn unit. + unit:destroy() + + end + + end + +end + +--- Destroy group. The whole group is despawned and a *Unit Lost* for aircraft or *Dead* event for ground/naval units is generated for all current units. +-- @param #OPSGROUP self +-- @param #number Delay Delay in seconds before the group will be destroyed. Default immediately. +-- @return #OPSGROUP self +function OPSGROUP:Destroy(Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP.Destroy, self, 0) + else + + -- Get all units. + local units=self:GetDCSUnits() + + if units then + + -- Create a "Unit Lost" event. + for _,unit in pairs(units) do + if unit then + self:DestroyUnit(unit:getName()) + end + end + + end + + end + + return self +end + --- Activate a *late activated* group. -- @param #OPSGROUP self -- @param #number delay (Optional) Delay in seconds before the group is activated. Default is immediately. @@ -127102,22 +137582,47 @@ end function OPSGROUP:Activate(delay) if delay and delay>0 then - self:T2(self.lid..string.format("Activating late activated group in %d seconds", delay)) - self:ScheduleOnce(delay, OPSGROUP.Activate, self) + self:T2(self.lid..string.format("Activating late activated group in %d seconds", delay)) + self:ScheduleOnce(delay, OPSGROUP.Activate, self) else - + if self:IsAlive()==false then - + self:T(self.lid.."Activating late activated group") self.group:Activate() self.isLateActivated=false - + elseif self:IsAlive()==true then - self:E(self.lid.."WARNING: Activating group that is already activated") + self:T(self.lid.."WARNING: Activating group that is already activated") else - self:E(self.lid.."ERROR: Activating group that is does not exist!") + self:T(self.lid.."ERROR: Activating group that is does not exist!") end - + + end + + return self +end + +--- Deactivate the group. Group will be respawned in late activated state. +-- @param #OPSGROUP self +-- @param #number delay (Optional) Delay in seconds before the group is deactivated. Default is immediately. +-- @return #OPSGROUP self +function OPSGROUP:Deactivate(delay) + + if delay and delay>0 then + self:ScheduleOnce(delay, OPSGROUP.Deactivate, self) + else + + if self:IsAlive()==true then + + self.template.lateActivation=true + + local template=UTILS.DeepCopy(self.template) + + self:_Respawn(0, template) + + end + end return self @@ -127126,26 +137631,272 @@ end --- Self destruction of group. An explosion is created at the position of each element. -- @param #OPSGROUP self -- @param #number Delay Delay in seconds. Default now. --- @param #number ExplosionPower (Optional) Explosion power in kg TNT. Default 500 kg. --- @return #number Relative fuel in percent. +-- @param #number ExplosionPower (Optional) Explosion power in kg TNT. Default 100 kg. +-- @return #OPSGROUP self function OPSGROUP:SelfDestruction(Delay, ExplosionPower) if Delay and Delay>0 then self:ScheduleOnce(Delay, OPSGROUP.SelfDestruction, self, 0, ExplosionPower) else - + -- Loop over all elements. for i,_element in pairs(self.elements) do local element=_element --#OPSGROUP.Element - + local unit=element.unit - + if unit and unit:IsAlive() then - unit:Explode(ExplosionPower) + unit:Explode(ExplosionPower or 100) end end end + return self +end + +--- Use SRS Simple-Text-To-Speech for transmissions. +-- @param #OPSGROUP self +-- @param #string PathToSRS Path to SRS directory. +-- @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 Port SRS port. Default 5002. +-- @param #string PathToGoogleKey Full path to the google credentials JSON file, e.g. `"C:\Users\myUsername\Downloads\key.json"`. +-- @param #string Label Label of the SRS comms for the SRS Radio overlay. Defaults to "ROBOT". No spaces allowed! +-- @param #number Volume Volume to be set, 0.0 = silent, 1.0 = loudest. Defaults to 1.0 +-- @return #OPSGROUP self +function OPSGROUP:SetSRS(PathToSRS, Gender, Culture, Voice, Port, PathToGoogleKey, Label, Volume) + 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:SetLabel(Label) + if PathToGoogleKey then + self.msrs:SetGoogle(PathToGoogleKey) + end + self.msrs:SetCoalition(self:GetCoalition()) + self.msrs:SetVolume(Volume) + return self +end + +--- Send a radio transmission via SRS Text-To-Speech. +-- @param #OPSGROUP self +-- @param #string Text Text of transmission. +-- @param #number Delay Delay in seconds before the transmission is started. +-- @param #boolean SayCallsign If `true`, the callsign is prepended to the given text. Default `false`. +-- @param #number Frequency Override sender frequency, helpful when you need multiple radios from the same sender. Default is the frequency set for the OpsGroup. +-- @return #OPSGROUP self +function OPSGROUP:RadioTransmission(Text, Delay, SayCallsign, Frequency) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP.RadioTransmission, self, Text, 0, SayCallsign) + else + + if self.useSRS and self.msrs then + + local freq, modu, radioon=self:GetRadio() + + if Frequency then + self.msrs:SetFrequencies(Frequency) + else + self.msrs:SetFrequencies(freq) + end + self.msrs:SetModulations(modu) + + if SayCallsign then + local callsign=self:GetCallsignName() + Text=string.format("%s, %s", callsign, Text) + end + + -- Debug info. + self:T(self.lid..string.format("Radio transmission on %.3f MHz %s: %s", freq, UTILS.GetModulationName(modu), Text)) + + self.msrs:PlayText(Text) + end + + end + + return self +end + +--- Set that this carrier is an all aspect loader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierLoaderAllAspect(Length, Width) + self.carrierLoader.type="front" + self.carrierLoader.length=Length or 50 + self.carrierLoader.width=Width or 20 + return self +end + +--- Set that this carrier is a front loader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierLoaderFront(Length, Width) + self.carrierLoader.type="front" + self.carrierLoader.length=Length or 50 + self.carrierLoader.width=Width or 20 + return self +end + +--- Set that this carrier is a back loader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierLoaderBack(Length, Width) + self.carrierLoader.type="back" + self.carrierLoader.length=Length or 50 + self.carrierLoader.width=Width or 20 + return self +end + +--- Set that this carrier is a starboard (right side) loader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierLoaderStarboard(Length, Width) + self.carrierLoader.type="right" + self.carrierLoader.length=Length or 50 + self.carrierLoader.width=Width or 20 + return self +end + +--- Set that this carrier is a port (left side) loader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierLoaderPort(Length, Width) + self.carrierLoader.type="left" + self.carrierLoader.length=Length or 50 + self.carrierLoader.width=Width or 20 + return self +end + + +--- Set that this carrier is an all aspect unloader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierUnloaderAllAspect(Length, Width) + self.carrierUnloader.type="front" + self.carrierUnloader.length=Length or 50 + self.carrierUnloader.width=Width or 20 + return self +end + +--- Set that this carrier is a front unloader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierUnloaderFront(Length, Width) + self.carrierUnloader.type="front" + self.carrierUnloader.length=Length or 50 + self.carrierUnloader.width=Width or 20 + return self +end + +--- Set that this carrier is a back unloader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierUnloaderBack(Length, Width) + self.carrierUnloader.type="back" + self.carrierUnloader.length=Length or 50 + self.carrierUnloader.width=Width or 20 + return self +end + +--- Set that this carrier is a starboard (right side) unloader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierUnloaderStarboard(Length, Width) + self.carrierUnloader.type="right" + self.carrierUnloader.length=Length or 50 + self.carrierUnloader.width=Width or 20 + return self +end + +--- Set that this carrier is a port (left side) unloader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierUnloaderPort(Length, Width) + self.carrierUnloader.type="left" + self.carrierUnloader.length=Length or 50 + self.carrierUnloader.width=Width or 20 + return self +end + +--- Check if group is currently inside a zone. +-- @param #OPSGROUP self +-- @param Core.Zone#ZONE Zone The zone. +-- @return #boolean If true, group is in this zone +function OPSGROUP:IsInZone(Zone) + local vec2=self:GetVec2() + local is=false + if vec2 then + is=Zone:IsVec2InZone(vec2) + else + self:T3(self.lid.."WARNING: Cannot get vec2 at IsInZone()!") + end + return is +end + +--- Get 2D distance to a coordinate. +-- @param #OPSGROUP self +-- @param Core.Point#COORDINATE Coordinate. Can also be a DCS#Vec2 or DCS#Vec3. +-- @return #number Distance in meters. +function OPSGROUP:Get2DDistance(Coordinate) + + local a=self:GetVec2() + local b={} + if Coordinate.z then + b.x=Coordinate.x + b.y=Coordinate.z + else + b.x=Coordinate.x + b.y=Coordinate.y + end + + local dist=UTILS.VecDist2D(a, b) + + return dist +end + +--- Check if this is a FLIGHTGROUP. +-- @param #OPSGROUP self +-- @return #boolean If true, this is an airplane or helo group. +function OPSGROUP:IsFlightgroup() + return self.isFlightgroup +end + +--- Check if this is a ARMYGROUP. +-- @param #OPSGROUP self +-- @return #boolean If true, this is a ground group. +function OPSGROUP:IsArmygroup() + return self.isArmygroup +end + +--- Check if this is a NAVYGROUP. +-- @param #OPSGROUP self +-- @return #boolean If true, this is a ship group. +function OPSGROUP:IsNavygroup() + return self.isNavygroup end @@ -127155,7 +137906,7 @@ end function OPSGROUP:IsExist() local DCSGroup=self:GetDCSGroup() - + if DCSGroup then local exists=DCSGroup:isExist() return exists @@ -127169,6 +137920,12 @@ end -- @return #boolean If true, the group exists or false if the group does not exist. If nil, the DCS group could not be found. function OPSGROUP:IsActive() + if self.group then + local active=self.group:IsActive() + return active + end + + return nil end --- Check if group is alive. @@ -127191,32 +137948,42 @@ function OPSGROUP:IsLateActivated() return self.isLateActivated end ---- Check if group is in state in utero. +--- Check if group is in state in utero. Note that dead groups are also in utero but will return `false` here. -- @param #OPSGROUP self -- @return #boolean If true, group is not spawned yet. function OPSGROUP:IsInUtero() - return self:Is("InUtero") + local is=self:Is("InUtero") and not self:IsDead() + return is end --- Check if group is in state spawned. -- @param #OPSGROUP self -- @return #boolean If true, group is spawned. function OPSGROUP:IsSpawned() - return self:Is("Spawned") + local is=self:Is("Spawned") + return is end ---- Check if group is dead. +--- Check if group is dead. Could be destroyed or despawned. FSM state of dead group is `InUtero` though. -- @param #OPSGROUP self -- @return #boolean If true, all units/elements of the group are dead. function OPSGROUP:IsDead() - return self:Is("Dead") + return self.isDead +end + +--- Check if group was destroyed. +-- @param #OPSGROUP self +-- @return #boolean If true, all units/elements of the group were destroyed. +function OPSGROUP:IsDestroyed() + return self.isDestroyed end --- Check if FSM is stopped. -- @param #OPSGROUP self -- @return #boolean If true, FSM state is stopped. function OPSGROUP:IsStopped() - return self:Is("Stopped") + local is=self:Is("Stopped") + return is end --- Check if this group is currently "uncontrolled" and needs to be "started" to begin its route. @@ -127233,7 +138000,7 @@ function OPSGROUP:HasPassedFinalWaypoint() return self.passedfinalwp end ---- Check if the group is currently rearming. +--- Check if the group is currently rearming or on its way to the rearming place. -- @param #OPSGROUP self -- @return #boolean If true, group is rearming. function OPSGROUP:IsRearming() @@ -127241,6 +138008,42 @@ function OPSGROUP:IsRearming() return rearming end +--- Check if the group is completely out of ammo. +-- @param #OPSGROUP self +-- @return #boolean If `true`, group is out-of-ammo. +function OPSGROUP:IsOutOfAmmo() + return self.outofAmmo +end + +--- Check if the group is out of bombs. +-- @param #OPSGROUP self +-- @return #boolean If `true`, group is out of bombs. +function OPSGROUP:IsOutOfBombs() + return self.outofBombs +end + +--- Check if the group is out of guns. +-- @param #OPSGROUP self +-- @return #boolean If `true`, group is out of guns. +function OPSGROUP:IsOutOfGuns() + return self.outofGuns +end + +--- Check if the group is out of missiles. +-- @param #OPSGROUP self +-- @return #boolean If `true`, group is out of missiles. +function OPSGROUP:IsOutOfMissiles() + return self.outofMissiles +end + +--- Check if the group is out of torpedos. +-- @param #OPSGROUP self +-- @return #boolean If `true`, group is out of torpedos. +function OPSGROUP:IsOutOfTorpedos() + return self.outofTorpedos +end + + --- Check if the group has currently switched a LASER on. -- @param #OPSGROUP self -- @return #boolean If true, LASER of the group is on. @@ -127248,18 +138051,299 @@ function OPSGROUP:IsLasing() return self.spot.On end ---- Check if the group is currently retreating. +--- Check if the group is currently retreating or retreated. -- @param #OPSGROUP self --- @return #boolean If true, group is retreating. +-- @return #boolean If true, group is retreating or retreated. function OPSGROUP:IsRetreating() - return self:is("Retreating") + local is=self:is("Retreating") or self:is("Retreated") + return is +end + +--- Check if the group is retreated (has reached its retreat zone). +-- @param #OPSGROUP self +-- @return #boolean If true, group is retreated. +function OPSGROUP:IsRetreated() + local is=self:is("Retreated") + return is +end + + +--- Check if the group is currently returning to a zone. +-- @param #OPSGROUP self +-- @return #boolean If true, group is returning. +function OPSGROUP:IsReturning() + local is=self:is("Returning") + return is end --- Check if the group is engaging another unit or group. -- @param #OPSGROUP self -- @return #boolean If true, group is engaging. function OPSGROUP:IsEngaging() - return self:is("Engaging") + local is=self:is("Engaging") + return is +end + +--- Check if group is currently waiting. +-- @param #OPSGROUP self +-- @return #boolean If true, group is currently waiting. +function OPSGROUP:IsWaiting() + if self.Twaiting then + return true + end + return false +end + +--- Check if the group is not a carrier yet. +-- @param #OPSGROUP self +-- @return #boolean If true, group is not a carrier. +function OPSGROUP:IsNotCarrier() + return self.carrierStatus==OPSGROUP.CarrierStatus.NOTCARRIER +end + +--- Check if the group is a carrier. +-- @param #OPSGROUP self +-- @return #boolean If true, group is a carrier. +function OPSGROUP:IsCarrier() + return not self:IsNotCarrier() +end + +--- Check if the group is picking up cargo. +-- @param #OPSGROUP self +-- @return #boolean If true, group is picking up. +function OPSGROUP:IsPickingup() + return self.carrierStatus==OPSGROUP.CarrierStatus.PICKUP +end + +--- Check if the group is loading cargo. +-- @param #OPSGROUP self +-- @return #boolean If true, group is loading. +function OPSGROUP:IsLoading() + return self.carrierStatus==OPSGROUP.CarrierStatus.LOADING +end + +--- Check if the group is transporting cargo. +-- @param #OPSGROUP self +-- @return #boolean If true, group is transporting. +function OPSGROUP:IsTransporting() + return self.carrierStatus==OPSGROUP.CarrierStatus.TRANSPORTING +end + +--- Check if the group is unloading cargo. +-- @param #OPSGROUP self +-- @return #boolean If true, group is unloading. +function OPSGROUP:IsUnloading() + return self.carrierStatus==OPSGROUP.CarrierStatus.UNLOADING +end + + +--- Check if the group is assigned as cargo. +-- @param #OPSGROUP self +-- @param #boolean CheckTransport If `true` or `nil`, also check if cargo is associated with a transport assignment. If not, we consider it not cargo. +-- @return #boolean If true, group is cargo. +function OPSGROUP:IsCargo(CheckTransport) + return not self:IsNotCargo(CheckTransport) +end + +--- Check if the group is **not** cargo. +-- @param #OPSGROUP self +-- @param #boolean CheckTransport If `true` or `nil`, also check if cargo is associated with a transport assignment. If not, we consider it not cargo. +-- @return #boolean If true, group is *not* cargo. +function OPSGROUP:IsNotCargo(CheckTransport) + local notcargo=self.cargoStatus==OPSGROUP.CargoStatus.NOTCARGO + + if notcargo then + -- Not cargo. + return true + else + -- Is cargo (e.g. loaded or boarding) + + if CheckTransport then + -- Check if transport UID was set. + if self.cargoTransportUID==nil then + return true + else + -- Some transport UID was assigned. + return false + end + else + -- Is cargo. + return false + end + + end + + + return notcargo +end + +--- Check if awaiting a transport. +-- @param #OPSGROUP self +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. +-- @return #OPSGROUP self +function OPSGROUP:_AddMyLift(Transport) + self.mylifts=self.mylifts or {} + self.mylifts[Transport.uid]=true + return self +end + +--- Remove my lift. +-- @param #OPSGROUP self +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. +-- @return #OPSGROUP self +function OPSGROUP:_DelMyLift(Transport) + if self.mylifts then + self.mylifts[Transport.uid]=nil + end + return self +end + + +--- Check if awaiting a transport lift. +-- @param #OPSGROUP self +-- @param Ops.OpsTransport#OPSTRANSPORT Transport (Optional) The transport. +-- @return #boolean If true, group is awaiting transport lift.. +function OPSGROUP:IsAwaitingLift(Transport) + + if self.mylifts then + + for uid,iswaiting in pairs(self.mylifts) do + if Transport==nil or Transport.uid==uid then + if iswaiting==true then + return true + end + end + end + + end + + return false +end + +--- Get paused mission. +-- @param #OPSGROUP self +-- @return Ops.Auftrag#AUFTRAG Paused mission or nil. +function OPSGROUP:_GetPausedMission() + + if self.pausedmissions and #self.pausedmissions>0 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. +-- @return #boolean If true, group is boarding. +function OPSGROUP:IsBoarding(CarrierGroupName) + if CarrierGroupName then + local carrierGroup=self:_GetMyCarrierGroup() + if carrierGroup and carrierGroup.groupname~=CarrierGroupName then + return false + end + end + return self.cargoStatus==OPSGROUP.CargoStatus.BOARDING +end + +--- Check if the group is currently loaded into a carrier. +-- @param #OPSGROUP self +-- @param #string CarrierGroupName (Optional) Additionally check if group is loaded into a particular carrier group(s). +-- @return #boolean If true, group is loaded. +function OPSGROUP:IsLoaded(CarrierGroupName) + if CarrierGroupName then + if type(CarrierGroupName)~="table" then + CarrierGroupName={CarrierGroupName} + end + for _,CarrierName in pairs(CarrierGroupName) do + local carrierGroup=self:_GetMyCarrierGroup() + if carrierGroup and carrierGroup.groupname==CarrierName then + return true + end + end + return false + end + return self.cargoStatus==OPSGROUP.CargoStatus.LOADED +end + +--- Check if the group is currently busy doing something. +-- +-- * Boarding +-- * Rearming +-- * Returning +-- * Pickingup, Loading, Transporting, Unloading +-- * Engageing +-- +-- @param #OPSGROUP self +-- @return #boolean If `true`, group is busy. +function OPSGROUP:IsBusy() + + if self:IsBoarding() then + return true + end + + if self:IsRearming() then + return true + end + + if self:IsReturning() then + return true + end + + -- Busy as carrier? + if self:IsPickingup() or self:IsLoading() or self:IsTransporting() or self:IsUnloading() then + return true + end + + if self:IsEngaging() then + return true + end + + + return false end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -127281,21 +138365,21 @@ function OPSGROUP:MarkWaypoints(Duration) for i,_waypoint in pairs(self.waypoints or {}) do local waypoint=_waypoint --#OPSGROUP.Waypoint - + local text=string.format("Waypoint ID=%d of %s", waypoint.uid, self.groupname) text=text..string.format("\nSpeed=%.1f kts, Alt=%d ft (%s)", UTILS.MpsToKnots(waypoint.speed), UTILS.MetersToFeet(waypoint.alt), "BARO") - + if waypoint.marker then if waypoint.marker.text~=text then waypoint.marker.text=text end - + else waypoint.marker=MARKER:New(waypoint.coordinate, text):ToCoalition(self:GetCoalition()) end end - - + + if Duration then self:RemoveWaypointMarkers(Duration) end @@ -127315,14 +138399,14 @@ function OPSGROUP:RemoveWaypointMarkers(Delay) for i,_waypoint in pairs(self.waypoints or {}) do local waypoint=_waypoint --#OPSGROUP.Waypoint - + if waypoint.marker then waypoint.marker:Remove() end end - + end - + return self end @@ -127395,32 +138479,37 @@ end --- Get next waypoint index. -- @param #OPSGROUP self --- @param #boolean cyclic If true, return first waypoint if last waypoint was reached. Default is patrol ad infinitum value set. +-- @param #boolean cyclic If `true`, return first waypoint if last waypoint was reached. Default is patrol ad infinitum value set. -- @param #number i Waypoint index from which the next index is returned. Default is the last waypoint passed. -- @return #number Next waypoint index. function OPSGROUP:GetWaypointIndexNext(cyclic, i) + -- If not specified, we take the adinititum value. if cyclic==nil then cyclic=self.adinfinitum end - + + -- Total number of waypoints. local N=#self.waypoints - + + -- Default is currentwp. i=i or self.currentwp + -- If no next waypoint exists, because the final waypoint was reached, we return the last waypoint. local n=math.min(i+1, N) - + + -- If last waypoint was reached, the first waypoint is the next in line. if cyclic and i==N then n=1 end - + return n end --- Get current waypoint index. This is the index of the last passed waypoint. -- @param #OPSGROUP self -- @return #number Current waypoint index. -function OPSGROUP:GetWaypointIndexCurrent() +function OPSGROUP:GetWaypointIndexCurrent() return self.currentwp or 1 end @@ -127435,8 +138524,8 @@ function OPSGROUP:GetWaypointIndexAfterID(uid) return index+1 else return #self.waypoints+1 - end - + end + end --- Get waypoint. @@ -127461,7 +138550,7 @@ end function OPSGROUP:GetWaypointNext(cyclic) local n=self:GetWaypointIndexNext(cyclic) - + return self.waypoints[n] end @@ -127472,13 +138561,24 @@ function OPSGROUP:GetWaypointCurrent() return self.waypoints[self.currentwp] end +--- Get current waypoint UID. +-- @param #OPSGROUP self +-- @return #number Current waypoint UID. +function OPSGROUP:GetWaypointCurrentUID() + local wp=self:GetWaypointCurrent() + if wp then + return wp.uid + end + return nil +end + --- Get coordinate of next waypoint of the group. -- @param #OPSGROUP self -- @param #boolean cyclic If true, return first waypoint if last waypoint was reached. -- @return Core.Point#COORDINATE Coordinate of the next waypoint. function OPSGROUP:GetNextWaypointCoordinate(cyclic) - -- Get next waypoint + -- Get next waypoint local waypoint=self:GetWaypointNext(cyclic) return waypoint.coordinate @@ -127503,7 +138603,7 @@ end function OPSGROUP:GetWaypointSpeed(indx) local waypoint=self:GetWaypoint(indx) - + if waypoint then return UTILS.MpsToKnots(waypoint.speed) end @@ -127526,7 +138626,7 @@ end function OPSGROUP:GetWaypointID(indx) local waypoint=self:GetWaypoint(indx) - + if waypoint then return waypoint.uid end @@ -127542,8 +138642,8 @@ end function OPSGROUP:GetSpeedToWaypoint(indx) local speed=self:GetWaypointSpeed(indx) - - if speed<=0.1 then + + if speed<=0.01 then speed=self:GetSpeedCruise() end @@ -127556,22 +138656,22 @@ end -- @return #number Distance in meters. function OPSGROUP:GetDistanceToWaypoint(indx) local dist=0 - + if #self.waypoints>0 then indx=indx or self:GetWaypointIndexNext() - + local wp=self:GetWaypoint(indx) - + if wp then - + local coord=self:GetCoordinate() - + dist=coord:Get2DDistance(wp.coordinate) end - + end - + return dist end @@ -127580,21 +138680,21 @@ end -- @param #number indx Waypoint index. Default is the next waypoint. -- @return #number Time in seconds. If velocity is 0 function OPSGROUP:GetTimeToWaypoint(indx) - + local s=self:GetDistanceToWaypoint(indx) - + local v=self:GetVelocity() - + local t=s/v - + if t==math.inf then return 365*24*60*60 elseif t==math.nan then return 0 - else + else return t end - + end --- Returns the currently expected speed. @@ -127602,12 +138702,12 @@ end -- @return #number Expected speed in m/s. function OPSGROUP:GetExpectedSpeed() - if self:IsHolding() then + if self:IsHolding() or self:Is("Rearming") or self:IsWaiting() or self:IsRetreated() then return 0 else return self.speedWp or 0 end - + end --- Remove a waypoint with a ceratin UID. @@ -127617,9 +138717,9 @@ end function OPSGROUP:RemoveWaypointByID(uid) local index=self:GetWaypointIndex(uid) - + if index then - self:RemoveWaypoint(index) + self:RemoveWaypoint(index) end return self @@ -127632,69 +138732,285 @@ end function OPSGROUP:RemoveWaypoint(wpindex) if self.waypoints then - + + -- The waypoitn to be removed. + local wp=self:GetWaypoint(wpindex) + + -- Is this a temporary waypoint. + local istemp=wp.temp or wp.detour or wp.astar or wp.missionUID + -- Number of waypoints before delete. local N=#self.waypoints - + + -- Always keep at least one waypoint. + if N==1 then + self:T(self.lid..string.format("ERROR: Cannot remove waypoint with index=%d! It is the only waypoint and a group needs at least ONE waypoint", wpindex)) + return self + end + + -- Check that wpindex is not larger than the number of waypoints in the table. + if wpindex>N then + self:T(self.lid..string.format("ERROR: Cannot remove waypoint with index=%d as there are only N=%d waypoints!", wpindex, N)) + return self + end + -- Remove waypoint marker. - local wp=self:GetWaypoint(wpindex) if wp and wp.marker then wp.marker:Remove() end -- Remove waypoint. table.remove(self.waypoints, wpindex) - + -- Number of waypoints after delete. local n=#self.waypoints - + -- Debug info. - self:T(self.lid..string.format("Removing waypoint index %d, current wp index %d. N %d-->%d", wpindex, self.currentwp, N, n)) - + self:T(self.lid..string.format("Removing waypoint UID=%d [temp=%s]: index=%d [currentwp=%d]. N %d-->%d", wp.uid, tostring(istemp), wpindex, self.currentwp, N, n)) + -- Waypoint was not reached yet. if wpindex > self.currentwp then - + --- -- Removed a FUTURE waypoint --- - - -- TODO: patrol adinfinitum. - - if self.currentwp>=n then - self.passedfinalwp=true + + -- TODO: patrol adinfinitum. Not sure this is handled correctly. If patrol adinfinitum and we have now only one WP left, we should at least go back. + + -- Could be that the waypoint we are currently moving to was the LAST waypoint. Then we now passed the final waypoint. + if self.currentwp>=n and not (self.adinfinitum or istemp) then + self:_PassedFinalWaypoint(true, "Removed FUTURE waypoint we are currently moving to and that was the LAST waypoint") end + -- Check if group is done. self:_CheckGroupDone(1) else - + --- -- Removed a waypoint ALREADY PASSED --- - + -- If an already passed waypoint was deleted, we do not need to update the route. - + -- If current wp = 1 it stays 1. Otherwise decrease current wp. - + if self.currentwp==1 then - + if self.adinfinitum then self.currentwp=#self.waypoints else self.currentwp=1 end - + else self.currentwp=self.currentwp-1 end - + end - + end return self end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- DCS Events +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Event function handling the birth of a unit. +-- @param #OPSGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function OPSGROUP:OnEventBirth(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 + + -- Set homebase if not already set. + if self.isFlightgroup then + + if EventData.Place then + self.homebase=self.homebase or EventData.Place + self.currbase=EventData.Place + else + self.currbase=nil + end + + if self.homebase and not self.destbase then + self.destbase=self.homebase + end + + self:T(self.lid..string.format("EVENT: Element %s born at airbase %s ==> spawned", unitname, self.currbase and self.currbase:GetName() or "unknown")) + else + self:T3(self.lid..string.format("EVENT: Element %s born ==> spawned", unitname)) + end + + -- Get element. + local element=self:GetElementByName(unitname) + + if element and element.status~=OPSGROUP.ElementStatus.SPAWNED then + + -- Debug info. + self:T(self.lid..string.format("EVENT: Element %s born ==> spawned", unitname)) + + -- Set element to spawned state. + self:ElementSpawned(element) + + end + + end + +end + +--- 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) + + -- 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: Unit %s dead!", 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: Element %s dead ==> destroyed", element.name)) + self:ElementDestroyed(element) + end + + end + +end + +--- Event function handling when a unit is removed from the game. +-- @param #OPSGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function OPSGROUP:OnEventRemoveUnit(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: Unit %s removed!", 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: Element %s removed ==> dead", element.name)) + self:ElementDead(element) + end + + end + +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 + + -- Target name + local targetname=tostring(EventData.TgtUnitName) + + -- Debug info. + self:T2(self.lid..string.format("EVENT: Unit %s killed object %s!", tostring(EventData.IniUnitName), targetname)) + + -- Check if this was a UNIT or STATIC object. + local target=UNIT:FindByName(targetname) + if not target then + target=STATIC:FindByName(targetname, false) + end + + -- Only count UNITS and STATICs (not SCENERY) + if target then + + -- Debug info. + self:T(self.lid..string.format("EVENT: Unit %s killed unit/static %s!", tostring(EventData.IniUnitName), targetname)) + + -- Kill counter. + self.Nkills=self.Nkills+1 + + -- Check if on a mission. + local mission=self:GetMissionCurrent() + if mission then + mission.Nkills=mission.Nkills+1 -- Increase mission kill counter. + end + + end + + end + +end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Task Functions @@ -127707,16 +139023,7 @@ end function OPSGROUP:SetTask(DCSTask) if self:IsAlive() then - - if self.taskcurrent>0 then - - -- TODO: Why the hell did I do this? It breaks scheduled tasks. I comment it out for now to see where it fails. - --local task=self:GetTaskCurrent() - --self:RemoveTask(task) - --self.taskcurrent=0 - - end - + -- Inject enroute tasks. if self.taskenroute and #self.taskenroute>0 then if tostring(DCSTask.id)=="ComboTask" then @@ -127726,14 +139033,14 @@ function OPSGROUP:SetTask(DCSTask) else local tasks=UTILS.DeepCopy(self.taskenroute) table.insert(tasks, DCSTask) - + DCSTask=self.group.TaskCombo(self, tasks) end end - + -- Set task. - self.group:SetTask(DCSTask) - + self.controller:setTask(DCSTask) + -- Debug info. local text=string.format("SETTING Task %s", tostring(DCSTask.id)) if tostring(DCSTask.id)=="ComboTask" then @@ -127741,9 +139048,9 @@ function OPSGROUP:SetTask(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 end @@ -127754,10 +139061,24 @@ end function OPSGROUP:PushTask(DCSTask) if self:IsAlive() then - + + -- Inject enroute tasks. + if self.taskenroute and #self.taskenroute>0 then + if tostring(DCSTask.id)=="ComboTask" then + for _,task in pairs(self.taskenroute) do + table.insert(DCSTask.params.tasks, 1, task) + end + else + local tasks=UTILS.DeepCopy(self.taskenroute) + table.insert(tasks, DCSTask) + + DCSTask=self.group.TaskCombo(self, tasks) + end + end + -- Push task. - self.group:PushTask(DCSTask) - + self.controller:pushTask(DCSTask) + -- Debug info. local text=string.format("PUSHING Task %s", tostring(DCSTask.id)) if tostring(DCSTask.id)=="ComboTask" then @@ -127767,22 +139088,36 @@ function OPSGROUP:PushTask(DCSTask) end self:T(self.lid..text) end - + return self end +--- Returns true if the DCS controller currently has a task. +-- @param #OPSGROUP self +-- @return #boolean True or false if the controller has a task. Nil if no controller. +function OPSGROUP:HasTaskController() + local hastask=nil + if self.controller then + hastask=self.controller:hasTask() + end + self:T3(self.lid..string.format("Controller hasTask=%s", tostring(hastask))) + return hastask +end + --- Clear DCS tasks. -- @param #OPSGROUP self --- @param #table DCSTask DCS task structure. -- @return #OPSGROUP self function OPSGROUP:ClearTasks() - if self:IsAlive() then - self.group:ClearTasks() - self:I(self.lid..string.format("CLEARING Tasks")) + local hastask=self:HasTaskController() + if self:IsAlive() and self.controller and self:HasTaskController() then + self:T(self.lid..string.format("CLEARING Tasks")) + self.controller:resetTask() end return self end + + --- Add a *scheduled* task. -- @param #OPSGROUP self -- @param #table task DCS task table structure. @@ -127797,7 +139132,7 @@ function OPSGROUP:AddTask(task, clock, description, prio, duration) -- Add to table. table.insert(self.taskqueue, newtask) - + -- Info. self:T(self.lid..string.format("Adding SCHEDULED task %s starting at %s", newtask.description, UTILS.SecondsToClock(newtask.time, true))) self:T3({newtask=newtask}) @@ -127832,14 +139167,14 @@ function OPSGROUP:NewTaskScheduled(task, clock, description, prio, duration) local newtask={} --#OPSGROUP.Task newtask.status=OPSGROUP.TaskStatus.SCHEDULED newtask.dcstask=task - newtask.description=description or task.id + newtask.description=description or task.id newtask.prio=prio or 50 newtask.time=time newtask.id=self.taskcounter newtask.duration=duration newtask.waypoint=-1 newtask.type=OPSGROUP.TaskType.SCHEDULED - newtask.stopflag=USERFLAG:New(string.format("%s StopTaskFlag %d", self.groupname, newtask.id)) + newtask.stopflag=USERFLAG:New(string.format("%s StopTaskFlag %d", self.groupname, newtask.id)) newtask.stopflag:Set(0) return newtask @@ -127849,15 +139184,15 @@ end -- @param #OPSGROUP self -- @param #table task DCS task table structure. -- @param #OPSGROUP.Waypoint Waypoint where the task is executed. Default is the at *next* waypoint. --- @param #string description Brief text describing the task, e.g. "Attack SAM". +-- @param #string description Brief text describing the task, e.g. "Attack SAM". -- @param #number prio Priority of the task. Number between 1 and 100. Default is 50. -- @param #number duration Duration before task is cancelled in seconds counted after task started. Default never. -- @return #OPSGROUP.Task The task structure. function OPSGROUP:AddTaskWaypoint(task, Waypoint, description, prio, duration) - + -- Waypoint of task. Waypoint=Waypoint or self:GetWaypointNext() - + if Waypoint then -- Increase counter. @@ -127874,22 +139209,22 @@ function OPSGROUP:AddTaskWaypoint(task, Waypoint, description, prio, duration) newtask.time=0 newtask.waypoint=Waypoint.uid newtask.type=OPSGROUP.TaskType.WAYPOINT - newtask.stopflag=USERFLAG:New(string.format("%s StopTaskFlag %d", self.groupname, newtask.id)) + newtask.stopflag=USERFLAG:New(string.format("%s StopTaskFlag %d", self.groupname, newtask.id)) newtask.stopflag:Set(0) - + -- Add to table. table.insert(self.taskqueue, newtask) - + -- Info. self:T(self.lid..string.format("Adding WAYPOINT task %s at WP ID=%d", newtask.description, newtask.waypoint)) self:T3({newtask=newtask}) - + -- Update route. - self:__UpdateRoute(-1) - - return newtask + --self:__UpdateRoute(-1) + + return newtask end - + return nil end @@ -127901,7 +139236,7 @@ function OPSGROUP:AddTaskEnroute(task) if not self.taskenroute then self.taskenroute={} end - + -- Check not to add the same task twice! local gotit=false for _,Task in pairs(self.taskenroute) do @@ -127910,11 +139245,12 @@ function OPSGROUP:AddTaskEnroute(task) break end end - + if not gotit then + self:T(self.lid..string.format("Adding enroute task")) table.insert(self.taskenroute, task) end - + end --- Get the unfinished waypoint tasks @@ -127923,7 +139259,7 @@ end -- @return #table Table of tasks. Table could also be empty {}. function OPSGROUP:GetTasksWaypoint(id) - -- Tasks table. + -- Tasks table. local tasks={} -- Sort queue. @@ -127936,7 +139272,7 @@ function OPSGROUP:GetTasksWaypoint(id) table.insert(tasks, task) end end - + return tasks end @@ -127946,7 +139282,7 @@ end -- @return #number Number of waypoint tasks. function OPSGROUP:CountTasksWaypoint(id) - -- Tasks table. + -- Tasks table. local n=0 -- Look for first task that SCHEDULED. @@ -127956,7 +139292,7 @@ function OPSGROUP:CountTasksWaypoint(id) n=n+1 end end - + return n end @@ -127970,7 +139306,7 @@ function OPSGROUP:_SortTaskQueue() local taskB=b --#OPSGROUP.Task return (taskA.prio0) then + + if Mission:IsReadyToPush() then + + --- + -- READY to push yet + --- + + -- Group is currently waiting. + if self:IsWaiting() then + + -- Not waiting any more. + self.Twaiting=nil + self.dTwait=nil + + -- For a flight group, we must cancel the wait/orbit task. + if self:IsFlightgroup() then + + -- Set hold flag to 1. This is a condition in the wait/orbit task. + self.flaghold:Set(1) + + -- Reexecute task in 1 sec to allow to flag to take effect. + --self:__TaskExecute(-1, Task) + + -- Deny transition for now. + --return false + end + end + + else + + --- + -- NOT READY to push yet + --- + + if self:IsWaiting() then + -- Group is already waiting + else + -- Wait indefinately. + local alt=Mission.missionAltitude and UTILS.MetersToFeet(Mission.missionAltitude) or nil + self:Wait(nil, alt) + end + + -- Time to for the next try. Best guess is when push time is reached or 20 sec when push conditions are not true yet. + local dt=Mission.Tpush and Mission.Tpush-timer.getAbsTime() or 20 + + -- Debug info. + self:T(self.lid..string.format("Mission %s task execute suspended for %d seconds", Mission.name, dt)) + + -- Reexecute task. + self:__TaskExecute(-dt, Task) + + -- Deny transition. + return false + end + + end + + if Mission and Mission.opstransport then + + local delivered=Mission.opstransport:IsCargoDelivered(self.groupname) + + if not delivered then + + local dt=30 + + -- Debug info. + self:T(self.lid..string.format("Mission %s task execute suspended for %d seconds because we were not delivered", Mission.name, dt)) + + -- Reexecute task. + self:__TaskExecute(-dt, Task) + + if (self:IsArmygroup() or self:IsNavygroup()) and self:IsCruising() then + self:FullStop() + end + + -- Deny transition. + return false + end + end + + return true +end + --- On after "TaskExecute" event. -- @param #OPSGROUP self -- @param #string From From state. @@ -128103,114 +139534,445 @@ end -- @param #string To To state. -- @param Ops.OpsGroup#OPSGROUP.Task Task The task. function OPSGROUP:onafterTaskExecute(From, Event, To, Task) - + self:T({Task}) -- Debug message. local text=string.format("Task %s ID=%d execute", tostring(Task.description), Task.id) self:T(self.lid..text) - + self:T({Task}) -- Cancel current task if there is any. if self.taskcurrent>0 then self:TaskCancel() end - + -- Set current task. self.taskcurrent=Task.id - + -- Set time stamp. Task.timestamp=timer.getAbsTime() -- Task status executing. Task.status=OPSGROUP.TaskStatus.EXECUTING - - if Task.dcstask.id=="Formation" then - -- Set of group(s) to follow Mother. - local followSet=SET_GROUP:New():AddGroup(self.group) - - local param=Task.dcstask.params - - local followUnit=UNIT:FindByName(param.unitname) - - -- Define AI Formation object. - Task.formation=AI_FORMATION:New(followUnit, followSet, "Formation", "Follow X at given parameters.") - - -- Formation parameters. - Task.formation:FormationCenterWing(-param.offsetX, 50, math.abs(param.altitude), 50, param.offsetZ, 50) - - -- Set follow time interval. - Task.formation:SetFollowTimeInterval(param.dtFollow) - - -- Formation mode. - Task.formation:SetFlightModeFormation(self.group) - - -- Start formation FSM. - Task.formation:Start() - - elseif Task.dcstask.id=="PatrolZone" then - - --- - -- Task patrol zone. - --- - - -- Parameters. - local zone=Task.dcstask.params.zone --Core.Zone#ZONE - local Coordinate=zone:GetRandomCoordinate() - local Speed=UTILS.KmphToKnots(Task.dcstask.params.speed or self.speedCruise) - local Altitude=Task.dcstask.params.altitude and UTILS.MetersToFeet(Task.dcstask.params.altitude) or nil - - -- New waypoint. - if self.isFlightgroup then - FLIGHTGROUP.AddWaypoint(self, Coordinate, Speed, AfterWaypointWithID, Altitude) - elseif self.isNavygroup then - ARMYGROUP.AddWaypoint(self, Coordinate, Speed, AfterWaypointWithID, Formation) - elseif self.isArmygroup then - NAVYGROUP.AddWaypoint(self, Coordinate, Speed, AfterWaypointWithID, Altitude) - end - - else - - -- If task is scheduled (not waypoint) set task. - if Task.type==OPSGROUP.TaskType.SCHEDULED then - - local DCStasks={} - if Task.dcstask.id=='ComboTask' then - -- Loop over all combo tasks. - for TaskID, Task in ipairs(Task.dcstask.params.tasks) do - table.insert(DCStasks, Task) - end - else - table.insert(DCStasks, Task.dcstask) - end - - -- Combo task. - local TaskCombo=self.group:TaskCombo(DCStasks) - - -- Stop condition! - local TaskCondition=self.group:TaskCondition(nil, Task.stopflag:GetName(), 1, nil, Task.duration) - - -- Controlled task. - local TaskControlled=self.group:TaskControlled(TaskCombo, TaskCondition) - - -- Task done. - local TaskDone=self.group:TaskFunction("OPSGROUP._TaskDone", self, Task) - - -- Final task. - local TaskFinal=self.group:TaskCombo({TaskControlled, TaskDone}) - - -- Set task for group. - self:SetTask(TaskFinal) - - end - + -- Insert into task queue. Not sure any more, why I added this. But probably if a task is just executed without having been put into the queue. + if self:GetTaskCurrent()==nil then + table.insert(self.taskqueue, Task) end -- Get mission of this task (if any). local Mission=self:GetMissionByTaskID(self.taskcurrent) + + if Task.dcstask.id==AUFTRAG.SpecialTask.FORMATION then + + -- Set of group(s) to follow Mother. + local followSet=SET_GROUP:New():AddGroup(self.group) + + local param=Task.dcstask.params + + local followUnit=UNIT:FindByName(param.unitname) + + -- Define AI Formation object. + Task.formation=AI_FORMATION:New(followUnit, followSet, AUFTRAG.SpecialTask.FORMATION, "Follow X at given parameters.") + + -- Formation parameters. + Task.formation:FormationCenterWing(-param.offsetX, 50, math.abs(param.altitude), 50, param.offsetZ, 50) + + -- Set follow time interval. + Task.formation:SetFollowTimeInterval(param.dtFollow) + + -- Formation mode. + Task.formation:SetFlightModeFormation(self.group) + + -- Start formation FSM. + Task.formation:Start() + + elseif Task.dcstask.id==AUFTRAG.SpecialTask.PATROLZONE then + + --- + -- Task patrol zone. + --- + + -- Parameters. + local zone=Task.dcstask.params.zone --Core.Zone#ZONE + + local surfacetypes=nil + if self:IsArmygroup() then + surfacetypes={land.SurfaceType.LAND, land.SurfaceType.ROAD} + elseif self:IsNavygroup() then + surfacetypes={land.SurfaceType.WATER, land.SurfaceType.SHALLOW_WATER} + end + + -- Random coordinate in zone. + local Coordinate=zone:GetRandomCoordinate(nil, nil, surfacetypes) + + --Coordinate:MarkToAll("Random Patrol Zone Coordinate") + + -- Speed and altitude. + local Speed=Task.dcstask.params.speed and UTILS.MpsToKnots(Task.dcstask.params.speed) or UTILS.KmphToKnots(self.speedCruise) + local Altitude=Task.dcstask.params.altitude and UTILS.MetersToFeet(Task.dcstask.params.altitude) or nil + + local currUID=self:GetWaypointCurrent().uid + + -- New waypoint. + local wp=nil --#OPSGROUP.Waypoint + if self.isFlightgroup then + wp=FLIGHTGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude) + elseif self.isArmygroup then + wp=ARMYGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Task.dcstask.params.formation) + elseif self.isNavygroup then + wp=NAVYGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude) + end + + -- Set mission UID. + wp.missionUID=Mission and Mission.auftragsnummer or nil + + elseif Task.dcstask.id==AUFTRAG.SpecialTask.RECON then + + --- + -- Task recon. + --- + + -- Target + local target=Task.dcstask.params.target --Ops.Target#TARGET + + -- Init a table. + self.reconindecies={} + for i=1,#target.targets do + table.insert(self.reconindecies, i) + end + + local n=1 + if Task.dcstask.params.randomly then + n=UTILS.GetRandomTableElement(self.reconindecies) + else + table.remove(self.reconindecies, n) + end + + -- Target object and zone. + local object=target.targets[n] --Ops.Target#TARGET.Object + local zone=object.Object --Core.Zone#ZONE + + -- Random coordinate in zone. + local Coordinate=zone:GetRandomCoordinate() + + -- Speed and altitude. + local Speed=Task.dcstask.params.speed and UTILS.MpsToKnots(Task.dcstask.params.speed) or UTILS.KmphToKnots(self.speedCruise) + local Altitude=Task.dcstask.params.altitude and UTILS.MetersToFeet(Task.dcstask.params.altitude) or nil + + --Coordinate:MarkToAll("Recon Waypoint Execute") + + local currUID=self:GetWaypointCurrent().uid + + -- New waypoint. + local wp=nil --#OPSGROUP.Waypoint + if self.isFlightgroup then + wp=FLIGHTGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude) + elseif self.isArmygroup then + wp=ARMYGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Task.dcstask.params.formation) + elseif self.isNavygroup then + wp=NAVYGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude) + end + + -- Set mission UID. + wp.missionUID=Mission and Mission.auftragsnummer or nil + + elseif Task.dcstask.id==AUFTRAG.SpecialTask.AMMOSUPPLY or Task.dcstask.id==AUFTRAG.SpecialTask.FUELSUPPLY then + + --- + -- Task "Ammo Supply" or "Fuel Supply" mission. + --- + + -- 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 + + --- + -- Task "Alert 5" mission. + --- + + -- Just stay put on the airfield and wait until something happens. + + elseif Task.dcstask.id==AUFTRAG.SpecialTask.ONGUARD or Task.dcstask.id==AUFTRAG.SpecialTask.ARMOREDGUARD then + + --- + -- Task "On Guard" Mission. + --- + + -- Just stay put. + --TODO: Change ALARM STATE + + if self:IsArmygroup() or self:IsNavygroup() then + -- Especially NAVYGROUP needs a full stop as patrol ad infinitum + self:FullStop() + else + -- FLIGHTGROUP not implemented (intended!) for this AUFTRAG type. + end + + elseif Task.dcstask.id==AUFTRAG.SpecialTask.NOTHING then + + --- + -- Task "Nothing" Mission. + --- + + -- Just stay put. + --TODO: Change ALARM STATE + + if self:IsArmygroup() or self:IsNavygroup() then + -- Especially NAVYGROUP needs a full stop as patrol ad infinitum + self:FullStop() + else + -- FLIGHTGROUP not implemented (intended!) for this AUFTRAG type. + end + + elseif Task.dcstask.id==AUFTRAG.SpecialTask.AIRDEFENSE or Task.dcstask.id==AUFTRAG.SpecialTask.EWR then + + --- + -- Task "AIRDEFENSE" or "EWR" Mission. + --- + + -- Just stay put. + --TODO: Change ALARM STATE + + if self:IsArmygroup() or self:IsNavygroup() then + self:FullStop() + else + -- FLIGHTGROUP not implemented (intended!) for this AUFTRAG type. + end + + elseif Task.dcstask.id==AUFTRAG.SpecialTask.GROUNDATTACK or Task.dcstask.id==AUFTRAG.SpecialTask.ARMORATTACK then + + --- + -- Task "Ground Attack" Mission. + --- + + -- Engage target. + local target=Task.dcstask.params.target --Ops.Target#TARGET + + -- Set speed. Default max. + local speed=self.speedMax and UTILS.KmphToKnots(self.speedMax) or nil + if Task.dcstask.params.speed then + speed=Task.dcstask.params.speed + end + + if target then + self:EngageTarget(target, speed, Task.dcstask.params.formation) + end + + elseif Task.dcstask.id==AUFTRAG.SpecialTask.HOVER then + + --- + -- Task "Hover" Mission. + --- + + if self.isFlightgroup then + self:T("We are Special Auftrag HOVER, hovering now ...") + --self:I({Task.dcstask.params}) + local alt = Task.dcstask.params.hoverAltitude + local time =Task.dcstask.params.hoverTime + local Speed=UTILS.MpsToKnots(Task.dcstask.params.missionSpeed) or UTILS.KmphToKnots(self.speedCruise) + local CruiseAlt = UTILS.FeetToMeters(Task.dcstask.params.missionAltitude) + local helo = self:GetGroup() + helo:SetSpeed(0.01,true) + helo:SetAltitude(alt,true,"BARO") + self:HoverStart() + local function FlyOn(Helo,Speed,CruiseAlt,Task) + if Helo then + Helo:SetSpeed(Speed,true) + Helo:SetAltitude(CruiseAlt,true,"BARO") + self:T("We are Special Auftrag HOVER, end of hovering now ...") + self:TaskDone(Task) + self:HoverEnd() + end + end + local timer = TIMER:New(FlyOn,helo,Speed,CruiseAlt,Task) + timer:Start(time) + end + + elseif Task.dcstask.id==AUFTRAG.SpecialTask.RELOCATECOHORT then + + --- + -- Task "RelocateCohort" Mission. + --- + + -- Debug mission. + self:T(self.lid.."Executing task for relocation mission") + + -- The new legion. + local legion=Task.dcstask.params.legion --Ops.Legion#LEGION + + -- Get random coordinate in spawn zone of new legion. + local Coordinate=legion.spawnzone:GetRandomCoordinate() + + -- Get current waypoint ID. + local currUID=self:GetWaypointCurrent().uid + + local wp=nil --#OPSGROUP.Waypoint + if self.isArmygroup then + self:T2(self.lid.."Routing group to spawn zone of new legion") + wp=ARMYGROUP.AddWaypoint(self, Coordinate, UTILS.KmphToKnots(self.speedCruise), currUID, Mission.optionFormation) + elseif self.isFlightgroup then + self:T2(self.lid.."Routing group to intermediate point near new legion") + Coordinate=self:GetCoordinate():GetIntermediateCoordinate(Coordinate, 0.8) + wp=FLIGHTGROUP.AddWaypoint(self, Coordinate, UTILS.KmphToKnots(self.speedCruise), currUID, UTILS.MetersToFeet(self.altitudeCruise)) + elseif self.isNavygroup then + self:T2(self.lid.."Routing group to spawn zone of new legion") + wp=NAVYGROUP.AddWaypoint(self, Coordinate, UTILS.KmphToKnots(self.speedCruise), currUID) + else + + end + + wp.missionUID=Mission and Mission.auftragsnummer or nil + + else + + -- If task is scheduled (not waypoint) set task. + if Task.type==OPSGROUP.TaskType.SCHEDULED or Task.ismission then + + -- DCS task. + local DCSTask=nil + + -- BARRAGE is special! + if Task.dcstask.id==AUFTRAG.SpecialTask.BARRAGE then + --- + -- BARRAGE + + -- Current vec2. + local vec2=self:GetVec2() + + -- Task parameters. + local param=Task.dcstask.params + + -- Set heading and altitude. + local heading=param.heading or math.random(1, 360) + local Altitude=param.altitude or 500 + local Alpha=param.angle or math.random(45, 85) + local distance=Altitude/math.tan(math.rad(Alpha)) + local tvec2=UTILS.Vec2Translate(vec2, distance, heading) + + -- Debug info. + self:T(self.lid..string.format("Barrage: Shots=%s, Altitude=%d m, Angle=%d°, heading=%03d°, distance=%d m", tostring(param.shots), Altitude, Alpha, heading, distance)) + + -- Set fire at point task. + DCSTask=CONTROLLABLE.TaskFireAtPoint(nil, tvec2, param.radius, param.shots, param.weaponType, Altitude) + + elseif Task.ismission and Task.dcstask.id=='FireAtPoint' then + + -- Copy DCS task. + DCSTask=UTILS.DeepCopy(Task.dcstask) + + -- Get current ammo. + local ammo=self:GetAmmoTot() + + -- Number of ammo avail. + local nAmmo=ammo.Total + + local weaponType=DCSTask.params.weaponType or -1 + + -- Adjust max number of ammo for specific weapon types requested. + if weaponType==ENUMS.WeaponFlag.CruiseMissile then + nAmmo=ammo.MissilesCR + elseif weaponType==ENUMS.WeaponFlag.AnyRocket then + nAmmo=ammo.Rockets + elseif weaponType==ENUMS.WeaponFlag.Cannons then + nAmmo=ammo.Guns + end + + --TODO: Update target location while we're at it anyway. + --TODO: Adjust mission result evaluation time? E.g. cruise missiles can fly a long time depending on target distance. + + -- Number of shots to be fired. + local nShots=DCSTask.params.expendQty or 1 + + -- Debug info. + self:T(self.lid..string.format("Fire at point with nshots=%d of %d", nShots, nAmmo)) + + if nShots==-1 then + -- The -1 is for using all available ammo. + nShots=nAmmo + self:T(self.lid..string.format("Fire at point taking max amount of ammo = %d", nShots)) + elseif nShots<1 then + local p=nShots + nShots=UTILS.Round(p*nAmmo, 0) + self:T(self.lid..string.format("Fire at point taking %.1f percent amount of ammo = %d", p, nShots)) + else + -- Fire nShots but at most nAmmo. + nShots=math.min(nShots, nAmmo) + end + + -- Set quantity of task. + DCSTask.params.expendQty=nShots + + elseif Mission and Mission.type==AUFTRAG.Type.RECOVERYTANKER then + + env.info("FF recoverytanker setting DCS task") + + -- Update DCS task with the current carrier parameters. + DCSTask=Mission:GetDCSMissionTask() + + else + --- + -- Take DCS task + --- + DCSTask=Task.dcstask + end + + local DCStasks={} + if DCSTask.id=='ComboTask' then + -- Loop over all combo tasks. + for TaskID, Task in ipairs(DCSTask.params.tasks) do + table.insert(DCStasks, Task) + end + else + table.insert(DCStasks, DCSTask) + end + + -- Combo task. + local TaskCombo=self.group:TaskCombo(DCStasks) + + -- Stop condition! + local TaskCondition=self.group:TaskCondition(nil, Task.stopflag:GetName(), 1, nil, Task.duration) + + -- Controlled task. + local TaskControlled=self.group:TaskControlled(TaskCombo, TaskCondition) + + -- Task done. + local TaskDone=self.group:TaskFunction("OPSGROUP._TaskDone", self, Task) + + -- Final task. + local TaskFinal=self.group:TaskCombo({TaskControlled, TaskDone}) + + -- Set task for group. + -- NOTE: I am pushing the task instead of setting it as it seems to keep the mission task alive. + -- There were issues that flights did not proceed to a later waypoint because the task did not finish until the fired missiles + -- impacted (took rather long). Then the flight flew to the nearest airbase and one lost completely the control over the group. + self:PushTask(TaskFinal) + --self:SetTask(TaskFinal) + + + elseif Task.type==OPSGROUP.TaskType.WAYPOINT then + -- Waypoint tasks are executed elsewhere! + else + self:T(self.lid.."ERROR: Unknown task type: ") + end + + end + + + -- Set AUFTRAG status. if Mission then - -- Set AUFTRAG status. self:MissionExecute(Mission) end - + end --- On after "TaskCancel" event. Cancels the current task or simply sets the status to DONE if the task is not the current one. @@ -128220,60 +139982,76 @@ end -- @param #string To To state. -- @param #OPSGROUP.Task Task The task to cancel. Default is the current task (if any). function OPSGROUP:onafterTaskCancel(From, Event, To, Task) - + -- Get current task. local currenttask=self:GetTaskCurrent() - + -- If no task, we take the current task. But this could also be *nil*! Task=Task or currenttask - + if Task then - + -- Check if the task is the current task? if currenttask and Task.id==currenttask.id then - + -- Current stop flag value. I noticed cases, where setting the flag to 1 would not cancel the task, e.g. when firing HARMS on a dead ship. local stopflag=Task.stopflag:Get() - + -- Debug info. local text=string.format("Current task %s ID=%d cancelled (flag %s=%d)", Task.description, Task.id, Task.stopflag:GetName(), stopflag) self:T(self.lid..text) - + -- Set stop flag. When the flag is true, the _TaskDone function is executed and calls :TaskDone() Task.stopflag:Set(1) - + local done=false - if Task.dcstask.id=="Formation" then + if Task.dcstask.id==AUFTRAG.SpecialTask.FORMATION then Task.formation:Stop() done=true - elseif Task.dcstask.id=="PatrolZone" then + elseif Task.dcstask.id==AUFTRAG.SpecialTask.PATROLZONE then done=true + elseif Task.dcstask.id==AUFTRAG.SpecialTask.RECON then + done=true + elseif Task.dcstask.id==AUFTRAG.SpecialTask.AMMOSUPPLY then + done=true + 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 + done=true + elseif Task.dcstask.id==AUFTRAG.SpecialTask.GROUNDATTACK or Task.dcstask.id==AUFTRAG.SpecialTask.ARMORATTACK then + done=true + elseif Task.dcstask.id==AUFTRAG.SpecialTask.NOTHING then + done=true elseif stopflag==1 or (not self:IsAlive()) or self:IsDead() or self:IsStopped() then -- Manual call TaskDone if setting flag to one was not successful. done=true end - + if done then self:TaskDone(Task) end - + else - + -- Debug info. self:T(self.lid..string.format("TaskCancel: Setting task %s ID=%d to DONE", Task.description, Task.id)) - - -- Call task done function. + + -- Call task done function. self:TaskDone(Task) end - + else - + local text=string.format("WARNING: No (current) task to cancel!") - self:E(self.lid..text) - + self:T(self.lid..text) + end - + end --- On before "TaskDone" event. Deny transition if task status is PAUSED. @@ -128309,38 +140087,75 @@ function OPSGROUP:onafterTaskDone(From, Event, To, Task) if Task.id==self.taskcurrent then self.taskcurrent=0 end - + -- Task status done. Task.status=OPSGROUP.TaskStatus.DONE - + -- Restore old ROE. if Task.backupROE then self:SwitchROE(Task.backupROE) end - + -- Check if this task was the task of the current mission ==> Mission Done! local Mission=self:GetMissionByTaskID(Task.id) - + if Mission and Mission:IsNotOver() then - local status=Mission:GetGroupStatus(self) - + -- Get mission status of this group. + local status=Mission:GetGroupStatus(self) + + -- Check if mission is paused. if status~=AUFTRAG.GroupStatus.PAUSED then - self:T(self.lid.."Task Done ==> Mission Done!") - self:MissionDone(Mission) + --- + -- Mission is NOT over ==> trigger done + --- + + -- Get egress waypoint uid. + local EgressUID=Mission:GetGroupEgressWaypointUID(self) + + if EgressUID then + -- Egress coordinate given ==> wait until we pass that waypoint. + self:T(self.lid..string.format("Task Done but Egress waypoint defined ==> Will call Mission Done once group passed waypoint UID=%d!", EgressUID)) + else + -- Mission done! + self:T(self.lid.."Task Done ==> Mission Done!") + self:MissionDone(Mission) + end else - --Mission paused. Do nothing! + --- + -- Mission Paused: Do nothing! Just set the current mission to nil so we can launch a new one. + --- + if self:IsOnMission(Mission.auftragsnummer) then + self.currentmission=nil + end + -- Remove mission waypoints. + self:T(self.lid.."Remove mission waypoints") + self:_RemoveMissionWaypoints(Mission, false) end + else - + if Task.description=="Engage_Target" then + self:T(self.lid.."Task DONE Engage_Target ==> Cruise") self:Disengage() - end - - self:T(self.lid.."Task Done but NO mission found ==> _CheckGroupDone in 1 sec") - self:_CheckGroupDone(1) + end + + if Task.description==AUFTRAG.SpecialTask.ONGUARD or Task.description==AUFTRAG.SpecialTask.ARMOREDGUARD or Task.description==AUFTRAG.SpecialTask.NOTHING then + self:T(self.lid.."Task DONE OnGuard ==> Cruise") + self:Cruise() + end + + if Task.description=="Task_Land_At" then + self:T(self.lid.."Taske DONE Task_Land_At ==> Wait") + self:Cruise() + self:Wait(20, 100) + else + self:T(self.lid.."Task Done but NO mission found ==> _CheckGroupDone in 1 sec") + self:_CheckGroupDone(1) + end + end - + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -128352,27 +140167,33 @@ end -- @param Ops.Auftrag#AUFTRAG Mission Mission for this group. -- @return #OPSGROUP self function OPSGROUP:AddMission(Mission) - + -- Add group to mission. Mission:AddOpsGroup(self) - + -- Set group status to SCHEDULED.. Mission:SetGroupStatus(self, AUFTRAG.GroupStatus.SCHEDULED) - + -- Set mission status to SCHEDULED. Mission:Scheduled() - + -- 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) - + + -- ad infinitum? + self.adinfinitum = Mission.DCStask.params.adinfinitum and Mission.DCStask.params.adinfinitum or false + -- Info text. - local text=string.format("Added %s mission %s starting at %s, stopping at %s", + local text=string.format("Added %s mission %s starting at %s, stopping at %s", tostring(Mission.type), tostring(Mission.name), UTILS.SecondsToClock(Mission.Tstart, true), Mission.Tstop and UTILS.SecondsToClock(Mission.Tstop, true) or "INF") self:T(self.lid..text) - + return self end @@ -128382,24 +140203,36 @@ 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. local Task=Mission:GetGroupWaypointTask(self) - + if Task then self:RemoveTask(Task) end - + + -- Take care of a paused mission. + 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. table.remove(self.missionqueue, i) - + return self end - + end return self @@ -128415,19 +140248,52 @@ function OPSGROUP:CountRemainingMissison() -- Loop over mission queue. for _,_mission in pairs(self.missionqueue) do local mission=_mission --Ops.Auftrag#AUFTRAG - + if mission and mission:IsNotOver() then - + -- Get group status. local status=mission:GetGroupStatus(self) - + if status~=AUFTRAG.GroupStatus.DONE and status~=AUFTRAG.GroupStatus.CANCELLED then N=N+1 end - + end end - + + return N +end + +--- Count remaining cargo transport assignments. +-- @param #OPSGROUP self +-- @return #number Number of unfinished transports in the queue. +function OPSGROUP:CountRemainingTransports() + + local N=0 + + -- Loop over mission queue. + for _,_transport in pairs(self.cargoqueue) do + local transport=_transport --Ops.OpsTransport#OPSTRANSPORT + + local mystatus=transport:GetCarrierTransportStatus(self) + local status=transport:GetState() + + -- Debug info. + self:T(self.lid..string.format("Transport my status=%s [%s]", mystatus, status)) + + -- Count not delivered (executing or scheduled) assignments. + if transport and mystatus==OPSTRANSPORT.Status.SCHEDULED and status~=OPSTRANSPORT.Status.DELIVERED and status~=OPSTRANSPORT.Status.CANCELLED then + N=N+1 + end + end + + -- In case we directly set the cargo transport (not in queue). + if N==0 and self.cargoTransport and + self.cargoTransport:GetState()~=OPSTRANSPORT.Status.DELIVERED and self.cargoTransport:GetCarrierTransportStatus(self)~=OPSTRANSPORT.Status.DELIVERED and + self.cargoTransport:GetState()~=OPSTRANSPORT.Status.CANCELLED and self.cargoTransport:GetCarrierTransportStatus(self)~=OPSTRANSPORT.Status.CANCELLED then + N=1 + end + return N end @@ -128436,6 +140302,11 @@ end -- @return Ops.Auftrag#AUFTRAG Next mission or *nil*. function OPSGROUP:_GetNextMission() + -- Check if group is acting as carrier or cargo at the moment. + if self:IsPickingup() or self:IsLoading() or self:IsTransporting() or self:IsUnloading() or self:IsLoaded() then + return nil + end + -- Number of missions. local Nmissions=#self.missionqueue @@ -128451,14 +140322,11 @@ function OPSGROUP:_GetNextMission() return (taskA.prio3.6 or true then + + self:RouteToMission(Mission, 3) + + else + --- + -- IMMOBILE Group + --- + + env.info(self.lid.."FF Immobile GROUP") + + -- Add waypoint task. UpdateRoute is called inside. + local Clock=Mission.Tpush and UTILS.SecondsToClock(Mission.Tpush) or 5 + + -- Add mission task. + local Task=self:AddTask(Mission.DCStask, Clock, Mission.name, Mission.prio, Mission.duration) + Task.ismission=true + + -- Set waypoint task. + Mission:SetGroupWaypointTask(self, Task) + + -- Execute task. This calls mission execute. + self:__TaskExecute(3, Task) + end + end --- On after "MissionExecute" event. Mission execution began. @@ -128596,13 +140557,23 @@ function OPSGROUP:onafterMissionExecute(From, Event, To, Mission) local text=string.format("Executing %s Mission %s, target %s", Mission.type, tostring(Mission.name), Mission:GetTargetName()) self:T(self.lid..text) - + -- Set group mission status to EXECUTING. Mission:SetGroupStatus(self, AUFTRAG.GroupStatus.EXECUTING) - + -- Set mission status to EXECUTING. Mission:Executing() - + + -- Group is holding but has waypoints ==> Cruise. + if self:IsHolding() and not self:HasPassedFinalWaypoint() then + self:Cruise() + end + + -- Set auto engage detected targets. + if Mission.engagedetectedOn then + self:SetEngageDetectedOn(UTILS.MetersToNM(Mission.engagedetectedRmax), Mission.engagedetectedTypes, Mission.engagedetectedEngageZones, Mission.engagedetectedNoEngageZones) + end + end --- On after "PauseMission" event. @@ -128613,24 +140584,26 @@ end function OPSGROUP:onafterPauseMission(From, Event, To) local Mission=self:GetMissionCurrent() - + if Mission then -- Set group mission status to PAUSED. Mission:SetGroupStatus(self, AUFTRAG.GroupStatus.PAUSED) - + -- Get mission waypoint task. local Task=Mission:GetGroupWaypointTask(self) - + -- Debug message. self:T(self.lid..string.format("Pausing current mission %s. Task=%s", tostring(Mission.name), tostring(Task and Task.description or "WTF"))) - + -- Cancelling the mission is actually cancelling the current task. self:TaskCancel(Task) + self:_RemoveMissionWaypoints(Mission) + -- Set mission to pause so we can unpause it later. - self.missionpaused=Mission - + table.insert(self.pausedmissions, 1, Mission.auftragsnummer) + end end @@ -128641,21 +140614,32 @@ 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 mission then - if self.missionpaused then - - local mission=self:GetMissionByID(self.missionpaused.auftragsnummer) + -- Debug info. + self:T(self.lid..string.format("Unpausing mission %s [%s]", mission:GetName(), mission:GetType())) + -- Start mission. self:MissionStart(mission) - self.missionpaused=nil + -- 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 + else - self:E(self.lid.."ERROR: No mission to unpause!") + self:T(self.lid.."ERROR: No mission to unpause!") end - + end @@ -128667,42 +140651,88 @@ end -- @param Ops.Auftrag#AUFTRAG Mission The mission to be cancelled. function OPSGROUP:onafterMissionCancel(From, Event, To, Mission) - if self.currentmission and Mission.auftragsnummer==self.currentmission then + if self:IsOnMission(Mission.auftragsnummer) then --- -- Current Mission --- + -- Some missions dont have a task set, which could be cancelled. + --[[ + if Mission.type==AUFTRAG.Type.ALERT5 or + Mission.type==AUFTRAG.Type.ONGUARD or + Mission.type==AUFTRAG.Type.ARMOREDGUARD or + --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) - -- Debug info. - self:T(self.lid..string.format("Cancel current mission %s. Task=%s", tostring(Mission.name), tostring(Task and Task.description or "WTF"))) + if Task then - -- 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 + -- 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. + + -- Trigger mission don task. + self:MissionDone(Mission) + + end + + else + --- -- NOT the current mission --- - + -- Set mission group status. Mission:SetGroupStatus(self, AUFTRAG.GroupStatus.CANCELLED) - + -- Remove mission from queue self:RemoveMission(Mission) - + -- Send group RTB or WAIT if nothing left to do. self:_CheckGroupDone(1) - + end - + +end + +--- On after "MissionDone" event. +-- @param #OPSGROUP self +-- @param Ops.Auftrag#AUFTRAG Mission +-- @param #boolean Silently Remove waypoints by `table.remove()` and do not update the route. +function OPSGROUP:_RemoveMissionWaypoints(Mission, Silently) + + for i=#self.waypoints,1,-1 do + local wp=self.waypoints[i] --#OPSGROUP.Waypoint + if wp.missionUID==Mission.auftragsnummer then + if Silently then + table.remove(self.waypoints, i) + else + self:RemoveWaypoint(i) + end + end + end + end --- On after "MissionDone" event. @@ -128710,47 +140740,65 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param Ops.Auftrag#AUFTRAG Mission +-- @param Ops.Auftrag#AUFTRAG Mission The mission that is done. function OPSGROUP:onafterMissionDone(From, Event, To, Mission) -- Debug info. local text=string.format("Mission %s DONE!", Mission.name) self:T(self.lid..text) - + -- Set group status. Mission:SetGroupStatus(self, AUFTRAG.GroupStatus.DONE) - + -- Set current mission to nil. - if self.currentmission and Mission.auftragsnummer==self.currentmission then + if self:IsOnMission(Mission.auftragsnummer) then self.currentmission=nil end - - -- Remove mission waypoint. - local wpidx=Mission:GetGroupWaypointIndex(self) - if wpidx then - self:RemoveWaypointByID(wpidx) - end - + + -- Remove mission waypoints. + self:_RemoveMissionWaypoints(Mission) + -- Decrease patrol data. if Mission.patroldata then Mission.patroldata.noccupied=Mission.patroldata.noccupied-1 AIRWING.UpdatePatrolPointMarker(Mission.patroldata) end - + + -- Switch auto engage detected off. This IGNORES that engage detected had been activated for the group! + if Mission.engagedetectedOn then + self:SetEngageDetectedOff() + end + -- ROE to default. if Mission.optionROE then self:SwitchROE() end -- ROT to default - if Mission.optionROT then + if self:IsFlightgroup() and Mission.optionROT then self:SwitchROT() end -- Alarm state to default. if Mission.optionAlarm then self:SwitchAlarmstate() - end + end + -- EPLRS to default. + if Mission.optionEPLRS then + self:SwitchEPLRS() + end + -- Emission to default. + 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 then + if Mission.optionFormation and self:IsFlightgroup() then self:SwitchFormation() end -- Radio freq and modu to default. @@ -128761,29 +140809,64 @@ function OPSGROUP:onafterMissionDone(From, Event, To, Mission) -- TACAN beacon. if Mission.tacan then - -- Switch to default. + -- Switch to default. self:_SwitchTACAN() - - -- Return Squadron TACAN channel. - local squadron=self.squadron --Ops.Squadron#SQUADRON - if squadron then - squadron:ReturnTacan(Mission.tacan.Channel) + + -- Return Cohort's TACAN channel. + local cohort=self.cohort --Ops.Cohort#COHORT + if cohort then + cohort:ReturnTacan(Mission.tacan.Channel) end - + -- Set asset TACAN to nil. local asset=Mission:GetAssetByName(self.groupname) if asset then asset.tacan=nil - end + end end - + -- ICLS beacon to default. if Mission.icls then - self:_SwitchICLS() + self:_SwitchICLS() + end + + -- Delay before check if group is done. + local delay=1 + + -- Special mission cases. + if Mission.type==AUFTRAG.Type.ARTY then + -- We add a 10 sec delay for ARTY. Found that they need some time to readjust the barrel of their gun. Not sure if necessary for all. Needs some more testing! + delay=60 + elseif Mission.type==AUFTRAG.Type.RELOCATECOHORT then + + -- New legion. + local legion=Mission.DCStask.params.legion --Ops.Legion#LEGION + + -- Debug message. + self:T(self.lid..string.format("Asset relocated to new legion=%s",tostring(legion.alias))) + + -- Get asset and change its warehouse id. + local asset=Mission:GetAssetByName(self.groupname) + if asset then + asset.wid=legion.uid + end + + -- Set new legion. + self.legion=legion + + if self.isArmygroup then + self:T2(self.lid.."Adding asset via ReturnToLegion()") + self:ReturnToLegion() + elseif self.isFlightgroup then + self:T2(self.lid.."Adding asset via RTB to new legion airbase") + self:RTB(self.legion.airbase) + end + + return end -- Check if group is done. - self:_CheckGroupDone(1) + self:_CheckGroupDone(delay) end @@ -128797,139 +140880,404 @@ function OPSGROUP:RouteToMission(mission, delay) -- Delayed call. self:ScheduleOnce(delay, OPSGROUP.RouteToMission, self, mission) else - - if self:IsDead() then + + -- Debug info. + self:T(self.lid..string.format("Route To Mission")) + + -- Catch dead or stopped groups. + if self:IsDead() or self:IsStopped() then + 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 + self:T(self.lid..string.format("Route To Mission: I am OPSTRANSPORT! Add transport and return...")) + self:AddOpsTransport(mission.opstransport) + return + end + + -- ALERT5: Just set the mission to executing. + if mission.type==AUFTRAG.Type.ALERT5 then + self:T(self.lid..string.format("Route To Mission: I am ALERT5! Go right to MissionExecute()...")) + self:MissionExecute(mission) + return + 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 - -- Get coordinate where the mission is executed. - local waypointcoord=mission:GetMissionWaypointCoord(self.group) + -- Current coordinate of the group. + local currentcoord=self:GetCoordinate() + -- Road connection. + local roadcoord=currentcoord:GetClosestPointToRoad() + + local roaddist=nil + if roadcoord then + roaddist=currentcoord:Get2DDistance(roadcoord) + end + + -- Target zone. + local targetzone=nil --Core.Zone#ZONE + + -- Random radius of 1000 meters. + local randomradius=mission.missionWaypointRadius or 1000 + + -- Surface types. + local surfacetypes=nil + if self:IsArmygroup() then + surfacetypes={land.SurfaceType.LAND, land.SurfaceType.ROAD} + elseif self:IsNavygroup() then + surfacetypes={land.SurfaceType.WATER, land.SurfaceType.SHALLOW_WATER} + end + + -- Get ingress waypoint. + if mission.opstransport and not mission.opstransport:IsCargoDelivered(self.groupname) then + + -- Get transport zone combo. + local tzc=mission.opstransport:GetTZCofCargo(self.groupname) + + local pickupzone=tzc.PickupZone + + if self:IsInZone(pickupzone) then + -- We are already in the pickup zone. + self:PauseMission() + self:FullStop() + return + else + -- Get a random coordinate inside the pickup zone. + waypointcoord=pickupzone:GetRandomCoordinate() + 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.REARMING or + mission.type==AUFTRAG.Type.AIRDEFENSE or + mission.type==AUFTRAG.Type.EWR then + --- + -- Missions with ZONE as target + --- + + -- Get the zone. + targetzone=mission.engageTarget:GetObject() --Core.Zone#ZONE + + -- Random coordinate. + waypointcoord=targetzone:GetRandomCoordinate(nil , nil, surfacetypes) + + elseif mission.type==AUFTRAG.Type.ONGUARD or mission.type==AUFTRAG.Type.ARMOREDGUARD then + --- + -- Guard + --- + + -- Mission waypoint + waypointcoord=mission:GetMissionWaypointCoord(self.group, nil, surfacetypes) + + elseif mission.type==AUFTRAG.Type.NOTHING then + --- + -- Nothing + --- + + -- Get the zone. + targetzone=mission.engageTarget:GetObject() --Core.Zone#ZONE + + -- Random coordinate. + waypointcoord=targetzone:GetRandomCoordinate(nil , nil, surfacetypes) + + elseif mission.type==AUFTRAG.Type.HOVER then + --- + -- Hover + --- + + local zone=mission.engageTarget:GetObject() --Core.Zone#ZONE + waypointcoord=zone:GetCoordinate() + + elseif mission.type==AUFTRAG.Type.RELOCATECOHORT then + --- + -- Relocation + --- + + -- Roughly go to the new legion. + local ToCoordinate=mission.DCStask.params.legion:GetCoordinate() + + if self.isFlightgroup then + -- Get mission waypoint coord in direction of the + waypointcoord=currentcoord:GetIntermediateCoordinate(ToCoordinate, 0.2):SetAltitude(self.altitudeCruise) + elseif self.isArmygroup then + -- Army group: check for road connection. + if roadcoord then + waypointcoord=roadcoord + else + waypointcoord=currentcoord:GetIntermediateCoordinate(ToCoordinate, 100) + end + else + -- Navy group: Route into direction of the target. + waypointcoord=currentcoord:GetIntermediateCoordinate(ToCoordinate, 0.05) + end + + elseif mission.type==AUFTRAG.Type.RECOVERYTANKER then + --- + -- Recoverytanker + --- + + local carrier=mission.DCStask.params.carrier --Wrapper.Unit#UNIT + + -- Roughly go to the new legion. + local CarrierCoordinate=carrier:GetCoordinate() + + local heading=carrier:GetHeading() + + waypointcoord=CarrierCoordinate:Translate(10000, heading-180):SetAltitude(2000) + + waypointcoord:MarkToAll("Recoverytanker") + + else + --- + -- Default case + --- + + waypointcoord=mission:GetMissionWaypointCoord(self.group, randomradius, surfacetypes) + end + -- Add enroute tasks. for _,task in pairs(mission.enrouteTasks) do self:AddTaskEnroute(task) end - + -- Speed to mission waypoint. - local SpeedToMission=UTILS.KmphToKnots(self.speedCruise) - + local SpeedToMission=mission.missionSpeed and UTILS.KmphToKnots(mission.missionSpeed) or self:GetSpeedCruise() + -- Special for Troop transport. if mission.type==AUFTRAG.Type.TROOPTRANSPORT then - - -- Refresh DCS task with the known controllable. + + --- + -- TROOP TRANSPORT + --- + + -- Refresh DCS task with the known controllable. mission.DCStask=mission:GetDCSMissionTask(self.group) - + + -- Create a pickup zone around the pickup coordinate. The troops will go to a random point inside the zone. + -- This is necessary so the helos do not try to land at the exact same location where the troops wait. + local pradius=mission.transportPickupRadius + local pickupZone=ZONE_RADIUS:New("Pickup Zone", mission.transportPickup:GetVec2(), pradius) + -- Add task to embark for the troops. for _,_group in pairs(mission.transportGroupSet.Set) do local group=_group --Wrapper.Group#GROUP - + if group and group:IsAlive() then - local DCSTask=group:TaskEmbarkToTransport(mission.transportPickup, 500) + -- Get random coordinate inside the zone. + local pcoord=pickupZone:GetRandomCoordinate(20, pradius, {land.SurfaceType.LAND, land.SurfaceType.ROAD}) + + -- Let the troops embark the transport. + local DCSTask=group:TaskEmbarkToTransport(pcoord, pradius) group:SetTask(DCSTask, 5) end - + end - + elseif mission.type==AUFTRAG.Type.ARTY then - - -- Get weapon range. - local weapondata=self:GetWeaponData(mission.engageWeaponType) + + --- + -- ARTY + --- + + -- Target Coord. + local targetcoord=mission:GetTargetCoordinate() - if weapondata then - -- Get target coordinate. - local targetcoord=mission:GetTargetCoordinate() + -- In range already? + local inRange=self:InWeaponRange(targetcoord, mission.engageWeaponType) + + if inRange then + + waypointcoord=self:GetCoordinate(true) + + else + + local coordInRange=self:GetCoordinateInRange(targetcoord, mission.engageWeaponType, waypointcoord) - -- Heading to target. - local heading=self:GetCoordinate():HeadingTo(targetcoord) - - -- Distance to target. - local dist=self:GetCoordinate():Get2DDistance(targetcoord) - - -- Check if we are within range. - if dist>weapondata.RangeMax then - - local d=(dist-weapondata.RangeMax)*1.1 + if coordInRange then + + -- Add waypoint at + local waypoint=nil --#OPSGROUP.Waypoint + if self:IsFlightgroup() then + waypoint=FLIGHTGROUP.AddWaypoint(self, waypointcoord, SpeedToMission, uid, UTILS.MetersToFeet(mission.missionAltitude or self.altitudeCruise), false) + elseif self:IsArmygroup() then + waypoint=ARMYGROUP.AddWaypoint(self, waypointcoord, SpeedToMission, uid, mission.optionFormation, false) + elseif self:IsNavygroup() then + waypoint=NAVYGROUP.AddWaypoint(self, waypointcoord, SpeedToMission, uid, UTILS.MetersToFeet(mission.missionAltitude or self.altitudeCruise), false) + end + waypoint.missionUID=mission.auftragsnummer + + -- Set waypoint coord to be the one in range. Take care of proper waypoint uid. + waypointcoord=coordInRange + uid=waypoint.uid - -- New waypoint coord. - waypointcoord=self:GetCoordinate():Translate(d, heading) - - self:T(self.lid..string.format("Out of max range = %.1f km for weapon %d", weapondata.RangeMax/1000, mission.engageWeaponType)) - elseif dist TaskExecute()") + self:TaskExecute(waypointtask) + return + elseif d<25 then + self:T(self.lid.."Already within 25 meters of mission waypoint ==> TaskExecute()") + self:TaskExecute(waypointtask) + return + end + + -- Check if group is mobile. Note that some immobile units report a speed of 1 m/s = 3.6 km/h. + if self.speedMax<=3.6 or mission.teleport then + + -- Teleport to waypoint coordinate. Mission will not be paused. + self:Teleport(waypointcoord, nil, true) + + -- Execute task in one second. + self:__TaskExecute(-1, waypointtask) + + else + + -- Give cruise command/update route. + if self:IsArmygroup() then + self:Cruise(SpeedToMission) + elseif self:IsNavygroup() then + self:Cruise(SpeedToMission) + elseif self:IsFlightgroup() then + self:UpdateRoute() + end + + end --- -- Mission Specific Settings --- - - -- ROE - if mission.optionROE then - self:SwitchROE(mission.optionROE) - end - -- ROT - if mission.optionROT then - self:SwitchROT(mission.optionROT) - end - -- Alarm state. - if mission.optionAlarm then - self:SwitchAlarmstate(mission.optionAlarm) - end - -- Formation - if mission.optionFormation and self.isAircraft then - self:SwitchFormation(mission.optionFormation) - end - -- Radio frequency and modulation. - if mission.radio then - self:SwitchRadio(mission.radio.Freq, mission.radio.Modu) - end - -- TACAN settings. - if mission.tacan then - self:SwitchTACAN(mission.tacan.Channel, mission.tacan.Morse, mission.tacan.BeaconName, mission.tacan.Band) - end - -- ICLS settings. - if mission.icls then - self:SwitchICLS(mission.icls.Channel, mission.icls.Morse, mission.icls.UnitName) - end - + self:_SetMissionOptions(mission) + end end +--- Set mission specific options for ROE, Alarm state, etc. +-- @param #OPSGROUP self +-- @param Ops.Auftrag#AUFTRAG mission The mission table. +function OPSGROUP:_SetMissionOptions(mission) + + -- ROE + if mission.optionROE then + self:SwitchROE(mission.optionROE) + end + -- ROT + if mission.optionROT then + self:SwitchROT(mission.optionROT) + end + -- Alarm state + if mission.optionAlarm then + self:SwitchAlarmstate(mission.optionAlarm) + end + -- EPLRS + if mission.optionEPLRS then + self:SwitchEPLRS(mission.optionEPLRS) + end + -- Emission + if mission.optionEmission then + self:SwitchEmission(mission.optionEmission) + end + -- Invisible + if mission.optionInvisible then + self:SwitchInvisible(mission.optionInvisible) + end + -- Immortal + if mission.optionImmortal then + self:SwitchImmortal(mission.optionImmortal) + end + -- Formation + if mission.optionFormation and self:IsFlightgroup() then + self:SwitchFormation(mission.optionFormation) + end + -- Radio frequency and modulation. + if mission.radio then + self:SwitchRadio(mission.radio.Freq, mission.radio.Modu) + end + -- TACAN settings. + if mission.tacan then + self:SwitchTACAN(mission.tacan.Channel, mission.tacan.Morse, mission.tacan.BeaconName, mission.tacan.Band) + end + -- ICLS settings. + if mission.icls then + self:SwitchICLS(mission.icls.Channel, mission.icls.Morse, mission.icls.UnitName) + end + +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Queue Update: Missions & Tasks ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- On after "QueueUpdate" event. +--- On after "QueueUpdate" event. -- @param #OPSGROUP self function OPSGROUP:_QueueUpdate() @@ -128937,26 +141285,27 @@ function OPSGROUP:_QueueUpdate() -- Mission --- - -- First check if group is alive? Late activated groups are activated and uncontrolled units are started automatically. + -- First check if group is alive? Late activated groups are activated and uncontrolled units are started automatically. if self:IsExist() then - + local mission=self:_GetNextMission() - + if mission then - + local currentmission=self:GetMissionCurrent() - + if currentmission then - + -- Current mission but new mission is urgent with higher prio. if mission.urgent and mission.prio0 then + self:T(self.lid..string.format("WARNING: Got current task ==> WAIT event is suspended for 30 sec!")) + Tsuspend=-30 + allowed=false + end + + -- Check for a current transport assignment. + if self.cargoTransport then + self:T(self.lid..string.format("WARNING: Got current TRANSPORT assignment ==> WAIT event is suspended for 30 sec!")) + Tsuspend=-30 + allowed=false + end + + -- Call wait again. + if Tsuspend and not allowed then + self:__Wait(Tsuspend, Duration) + end + + return allowed +end + +--- On after "Wait" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number Duration Duration in seconds how long the group will be waiting. Default `nil` (for ever). +function OPSGROUP:onafterWait(From, Event, To, Duration) + + -- Order Group to hold. + self:FullStop() + + -- Set time stamp. + self.Twaiting=timer.getAbsTime() + + -- Max waiting + self.dTwait=Duration + +end + + --- On after "PassingWaypoint" event. -- @param #OPSGROUP self -- @param #string From From state. @@ -129001,57 +141409,264 @@ function OPSGROUP:onafterPassingWaypoint(From, Event, To, Waypoint) -- Get the current task. local task=self:GetTaskCurrent() - - if task and task.dcstask.id=="PatrolZone" then - - -- Remove old waypoint. + + -- Get the corresponding mission. + local mission=nil --Ops.Auftrag#AUFTRAG + if task then + mission=self:GetMissionByTaskID(task.id) + end + + if task and task.dcstask.id==AUFTRAG.SpecialTask.PATROLZONE then + + --- + -- SPECIAL TASK: Patrol Zone + --- + + -- Remove old waypoint. self:RemoveWaypointByID(Waypoint.uid) - local zone=task.dcstask.params.zone --Core.Zone#ZONE - local Coordinate=zone:GetRandomCoordinate() - local Speed=UTILS.KmphToKnots(task.dcstask.params.speed or self.speedCruise) - local Altitude=task.dcstask.params.altitude and UTILS.MetersToFeet(task.dcstask.params.altitude) or nil - - if self.isFlightgroup then - FLIGHTGROUP.AddWaypoint(self, Coordinate, Speed, AfterWaypointWithID, Altitude) - elseif self.isNavygroup then - ARMYGROUP.AddWaypoint(self, Coordinate, Speed, AfterWaypointWithID, Formation) - elseif self.isArmygroup then - NAVYGROUP.AddWaypoint(self, Coordinate, Speed, AfterWaypointWithID, Altitude) + -- Zone object. + local zone=task.dcstask.params.zone --Core.Zone#ZONE + + -- Surface types. + local surfacetypes=nil + if self:IsArmygroup() then + surfacetypes={land.SurfaceType.LAND, land.SurfaceType.ROAD} + elseif self:IsNavygroup() then + surfacetypes={land.SurfaceType.WATER, land.SurfaceType.SHALLOW_WATER} end + -- Random coordinate in zone. + local Coordinate=zone:GetRandomCoordinate(nil, nil, surfacetypes) + + -- Speed and altitude. + local Speed=task.dcstask.params.speed and UTILS.MpsToKnots(task.dcstask.params.speed) or UTILS.KmphToKnots(self.speedCruise) + local Altitude=UTILS.MetersToFeet(task.dcstask.params.altitude or self.altitudeCruise) + + local currUID=self:GetWaypointCurrent().uid + + local wp=nil --#OPSGROUP.Waypoint + if self.isFlightgroup then + wp=FLIGHTGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude) + elseif self.isArmygroup then + wp=ARMYGROUP.AddWaypoint(self, Coordinate, Speed, currUID, task.dcstask.params.formation) + elseif self.isNavygroup then + wp=NAVYGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude) + end + wp.missionUID=mission and mission.auftragsnummer or nil + + elseif task and task.dcstask.id==AUFTRAG.SpecialTask.RECON then + + --- + -- SPECIAL TASK: Recon Mission + --- + + -- TARGET. + local target=task.dcstask.params.target --Ops.Target#TARGET - else + -- Init a table. + if self.adinfinitum and #self.reconindecies==0 then -- all targets done once + self.reconindecies={} + for i=1,#target.targets do + table.insert(self.reconindecies, i) + end + end - -- Apply tasks of this waypoint. - local ntasks=self:_SetWaypointTasks(Waypoint) + if #self.reconindecies>0 then - -- Get waypoint index. - local wpindex=self:GetWaypointIndex(Waypoint.uid) - - -- Final waypoint reached? - if wpindex==nil or wpindex==#self.waypoints then - - -- Set switch to true. - if not self.adinfinitum or #self.waypoints<=1 then - self.passedfinalwp=true + local n=1 + if task.dcstask.params.randomly then + n=UTILS.GetRandomTableElement(self.reconindecies) + else + n=self.reconindecies[1] + table.remove(self.reconindecies, 1) end + -- Zone object. + local object=target.targets[n] --Ops.Target#TARGET.Object + local zone=object.Object --Core.Zone#ZONE + + -- Random coordinate in zone. + local Coordinate=zone:GetRandomCoordinate() + + -- Speed and altitude. + local Speed=task.dcstask.params.speed and UTILS.MpsToKnots(task.dcstask.params.speed) or UTILS.KmphToKnots(self.speedCruise) + local Altitude=task.dcstask.params.altitude and UTILS.MetersToFeet(task.dcstask.params.altitude) or nil + + -- Debug. + --Coordinate:MarkToAll("Recon Waypoint n="..tostring(n)) + + local currUID=self:GetWaypointCurrent().uid + + local wp=nil --#OPSGROUP.Waypoint + if self.isFlightgroup then + wp=FLIGHTGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude) + elseif self.isArmygroup then + wp=ARMYGROUP.AddWaypoint(self, Coordinate, Speed, currUID, task.dcstask.params.formation) + elseif self.isNavygroup then + wp=NAVYGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude) + end + wp.missionUID=mission and mission.auftragsnummer or nil + + else + + -- Get waypoint index. + local wpindex=self:GetWaypointIndex(Waypoint.uid) + + -- Final waypoint reached? + if wpindex==nil or wpindex==#self.waypoints then + + -- Set switch to true. + if not self.adinfinitum or #self.waypoints<=1 then + self:_PassedFinalWaypoint(true, "Passing waypoint and NOT adinfinitum and #self.waypoints<=1") + end + + end + + -- Final zone reached ==> task done. + self:TaskDone(task) + end - + + elseif task and task.dcstask.id==AUFTRAG.SpecialTask.RELOCATECOHORT then + + --- + -- SPECIAL TASK: Relocate Mission + --- + + -- TARGET. + local legion=task.dcstask.params.legion --Ops.Legion#LEGION + + self:T(self.lid..string.format("Asset arrived at relocation task waypoint ==> Task Done!")) + + -- Final zone reached ==> task done. + self:TaskDone(task) + + else + + --- + -- No special task active + --- + + -- Apply tasks of this waypoint. + local ntasks=self:_SetWaypointTasks(Waypoint) + + -- Get waypoint index. + local wpindex=self:GetWaypointIndex(Waypoint.uid) + + -- Final waypoint reached? + if wpindex==nil or wpindex==#self.waypoints then + + -- Ad infinitum and not mission waypoint? + if self.adinfinitum then + --- + -- Ad Infinitum + --- + + if Waypoint.missionUID then + --- + -- Last waypoint was a mission waypoint ==> Do nothing (when mission is over, it should take care of this) + --- + else + + --- + -- Last waypoint reached. + --- + + if #self.waypoints<=1 then + -- Only one waypoint. Ad infinitum does not really make sense. However, another waypoint could be added later... + self:_PassedFinalWaypoint(true, "PassingWaypoint: adinfinitum but only ONE WAYPOINT left") + else + + --[[ Solved now! + + -- Looks like the passing waypoint function is triggered over and over again if the group is near the final waypoint. + -- So the only good solution is to guide the group away from that waypoint and then update the route. + + -- Get first waypoint. + local wp1=self:GetWaypointByIndex(1) + + -- Get a waypoint + local Coordinate=Waypoint.coordinate:GetIntermediateCoordinate(wp1.coordinate, 0.1) + + local formation=nil + if self.isArmygroup then + formation=ENUMS.Formation.Vehicle.OffRoad + end + + self:Detour(Coordinate, self.speedCruise, formation, true) + + ]] + + + -- Send + self:__UpdateRoute(-0.01, 1, 1) + + end + end + else + --- + -- NOT Ad Infinitum + --- + + -- Final waypoint reached. + self:_PassedFinalWaypoint(true, "PassingWaypoint: wpindex=#self.waypoints (or wpindex=nil)") + end + + elseif wpindex==1 then + + -- Ad infinitum and not mission waypoint? + if self.adinfinitum then + --- + -- Ad Infinitum + --- + + if #self.waypoints<=1 then + -- Only one waypoint. Ad infinitum does not really make sense. However, another waypoint could be added later... + self:_PassedFinalWaypoint(true, "PassingWaypoint: adinfinitum but only ONE WAYPOINT left") + + else + + if not Waypoint.missionUID then + -- Redo the route until the end. + self:__UpdateRoute(-0.01, 2) + end + end + end + + end + + -- Passing mission waypoint? + local isEgress=false + if Waypoint.missionUID then + + -- Debug info. + self:T2(self.lid..string.format("Passing mission waypoint UID=%s", tostring(Waypoint.uid))) + + -- Get the mission. + local mission=self:GetMissionByID(Waypoint.missionUID) + + -- Check if this was an Egress waypoint of the mission. If so, call Mission Done! This will call CheckGroupDone. + local EgressUID=mission and mission:GetGroupEgressWaypointUID(self) or nil + isEgress=EgressUID and Waypoint.uid==EgressUID + if isEgress and mission:GetGroupStatus(self)~=AUFTRAG.GroupStatus.DONE then + self:MissionDone(mission) + end + end + -- Check if all tasks/mission are done? -- Note, we delay it for a second to let the OnAfterPassingwaypoint function to be executed in case someone wants to add another waypoint there. - if ntasks==0 then - self:_CheckGroupDone(0.1) + if ntasks==0 and self:HasPassedFinalWaypoint() and not isEgress then + self:_CheckGroupDone(0.01) end - + -- Debug info. - local text=string.format("Group passed waypoint %s/%d ID=%d: final=%s detour=%s astar=%s", + local text=string.format("Group passed waypoint %s/%d ID=%d: final=%s detour=%s astar=%s", tostring(wpindex), #self.waypoints, Waypoint.uid, tostring(self.passedfinalwp), tostring(Waypoint.detour), tostring(Waypoint.astar)) self:T(self.lid..text) - + end - + end --- Set tasks at this waypoint @@ -129065,37 +141680,54 @@ function OPSGROUP:_SetWaypointTasks(Waypoint) -- Debug info. local text=string.format("WP uid=%d tasks:", Waypoint.uid) + local missiontask=nil --Ops.OpsGroup#OPSGROUP.Task if #tasks>0 then for i,_task in pairs(tasks) do local task=_task --#OPSGROUP.Task text=text..string.format("\n[%d] %s", i, task.description) + if task.ismission then + missiontask=task + end end else text=text.." None" end self:T(self.lid..text) - + + -- Check if there is mission task + if missiontask then + self:T(self.lid.."Executing mission task") + local mission=self:GetMissionByTaskID(missiontask.id) + if mission then + if mission.opstransport and not mission.opstransport:IsCargoDelivered(self.groupname) then + self:PauseMission() + return + end + end + self:TaskExecute(missiontask) + return 1 + end + + -- TODO: maybe set waypoint enroute tasks? -- Tasks at this waypoints. local taskswp={} - - -- TODO: maybe set waypoint enroute tasks? - + for _,task in pairs(tasks) do - local Task=task --Ops.OpsGroup#OPSGROUP.Task - + local Task=task --Ops.OpsGroup#OPSGROUP.Task + -- Task execute. table.insert(taskswp, self.group:TaskFunction("OPSGROUP._TaskExecute", self, Task)) -- Stop condition if userflag is set to 1 or task duration over. local TaskCondition=self.group:TaskCondition(nil, Task.stopflag:GetName(), 1, nil, Task.duration) - - -- Controlled task. + + -- Controlled task. table.insert(taskswp, self.group:TaskControlled(Task.dcstask, TaskCondition)) - + -- Task done. table.insert(taskswp, self.group:TaskFunction("OPSGROUP._TaskDone", self, Task)) - + end -- Execute waypoint tasks. @@ -129106,36 +141738,43 @@ function OPSGROUP:_SetWaypointTasks(Waypoint) return #taskswp end +--- On after "PassedFinalWaypoint" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterPassedFinalWaypoint(From, Event, To) + self:T(self.lid..string.format("Group passed final waypoint")) + + -- Check if group is done? No tasks mission running. + --self:_CheckGroupDone() + +end + --- On after "GotoWaypoint" event. Group will got to the given waypoint and execute its route from there. -- @param #OPSGROUP self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -- @param #number UID The goto waypoint unique ID. -function OPSGROUP:onafterGotoWaypoint(From, Event, To, UID) +-- @param #number Speed (Optional) Speed to waypoint in knots. +function OPSGROUP:onafterGotoWaypoint(From, Event, To, UID, Speed) local n=self:GetWaypointIndex(UID) - + if n then - - -- TODO: switch to re-enable waypoint tasks. - if false then - local tasks=self:GetTasksWaypoint(n) - - for _,_task in pairs(tasks) do - local task=_task --#OPSGROUP.Task - task.status=OPSGROUP.TaskStatus.SCHEDULED - end - - end - - local Speed=self:GetSpeedToWaypoint(n) - + + -- Speed to waypoint. + Speed=Speed or self:GetSpeedToWaypoint(n) + + -- Debug message + self:T(self.lid..string.format("Goto Waypoint UID=%d index=%d from %d at speed %.1f knots", UID, n, self.currentwp, Speed)) + -- Update the route. - self:__UpdateRoute(-1, n, Speed) - + self:__UpdateRoute(0.1, n, nil, Speed) + end - + end --- On after "DetectedUnit" event. @@ -129151,7 +141790,7 @@ function OPSGROUP:onafterDetectedUnit(From, Event, To, Unit) -- Debug. self:T2(self.lid..string.format("Detected unit %s", unitname)) - + if self.detectedunits:FindUnit(unitname) then -- Unit is already in the detected unit set ==> Trigger "DetectedUnitKnown" event. self:DetectedUnitKnown(Unit) @@ -129169,10 +141808,10 @@ end -- @param #string To To state. -- @param Wrapper.Unit#UNIT Unit The detected unit. function OPSGROUP:onafterDetectedUnitNew(From, Event, To, Unit) - + -- Debug info. self:T(self.lid..string.format("Detected New unit %s", Unit:GetName())) - + -- Add unit to detected unit set. self.detectedunits:AddUnit(Unit) end @@ -129190,7 +141829,7 @@ function OPSGROUP:onafterDetectedGroup(From, Event, To, Group) -- Debug info. self:T(self.lid..string.format("Detected group %s", groupname)) - + if self.detectedgroups:FindGroup(groupname) then -- Group is already in the detected set ==> Trigger "DetectedGroupKnown" event. self:DetectedGroupKnown(Group) @@ -129198,7 +141837,7 @@ function OPSGROUP:onafterDetectedGroup(From, Event, To, Group) -- Group is was not detected ==> Trigger "DetectedGroupNew" event. self:DetectedGroupNew(Group) end - + end --- On after "DetectedGroupNew" event. Add newly detected group to detected group set. @@ -129211,7 +141850,7 @@ function OPSGROUP:onafterDetectedGroupNew(From, Event, To, Group) -- Debug info. self:T(self.lid..string.format("Detected New group %s", Group:GetName())) - + -- Add unit to detected unit set. self.detectedgroups:AddGroup(Group) end @@ -129255,53 +141894,53 @@ function OPSGROUP:onbeforeLaserOn(From, Event, To, Target) if Target then - -- Target specified ==> set target. + -- Target specified ==> set target. self:SetLaserTarget(Target) - + else -- No target specified. - self:E(self.lid.."ERROR: No target provided for LASER!") + self:T(self.lid.."ERROR: No target provided for LASER!") return false end - + -- Get the first element alive. local element=self:GetElementAlive() - + if element then - + -- Set element. - self.spot.element=element - + self.spot.element=element + -- Height offset. No offset for aircraft. We take the height for ground or naval. local offsetY=0 - if self.isGround or self.isNaval then + if self.isFlightgroup or self.isNavygroup then offsetY=element.height end - + -- Local offset of the LASER source. self.spot.offset={x=0, y=offsetY, z=0} - + -- Check LOS. if self.spot.CheckLOS then - + -- Check LOS. local los=self:HasLoS(self.spot.Coordinate, self.spot.element, self.spot.offset) - - --self:I({los=los, coord=self.spot.Coordinate, offset=self.spot.offset}) + + --self:T({los=los, coord=self.spot.Coordinate, offset=self.spot.offset}) if los then self:LaserGotLOS() else -- Try to switch laser on again in 10 sec. - self:I(self.lid.."LASER got no LOS currently. Trying to switch the laser on again in 10 sec") + self:T(self.lid.."LASER got no LOS currently. Trying to switch the laser on again in 10 sec") self:__LaserOn(-10, Target) return false end - + end - + else - self:E(self.lid.."ERROR: No element alive for lasing") + self:T(self.lid.."ERROR: No element alive for lasing") return false end @@ -129329,16 +141968,16 @@ function OPSGROUP:onafterLaserOn(From, Event, To, Target) if self.spot.IRon then self.spot.IR=Spot.createInfraRed(DCSunit, self.spot.offset, self.spot.vec3) end - + -- Laser is on. self.spot.On=true - + -- No paused in case it was. self.spot.Paused=false -- Debug message. self:T(self.lid.."Switching LASER on") - + end --- On before "LaserOff" event. Check if LASER is on. @@ -129364,7 +142003,7 @@ function OPSGROUP:onafterLaserOff(From, Event, To) if self.spot.On then self.spot.Laser:destroy() self.spot.IR:destroy() - + -- Set to nil. self.spot.Laser=nil self.spot.IR=nil @@ -129372,13 +142011,13 @@ function OPSGROUP:onafterLaserOff(From, Event, To) -- Stop update timer. self.spot.timer:Stop() - + -- No target unit. self.spot.TargetUnit=nil -- Laser is off. self.spot.On=false - + -- Not paused if it was. self.spot.Paused=false end @@ -129396,17 +142035,17 @@ function OPSGROUP:onafterLaserPause(From, Event, To) -- "Destroy" the laser beam. self.spot.Laser:destroy() self.spot.IR:destroy() - + -- Set to nil. self.spot.Laser=nil self.spot.IR=nil -- Laser is off. self.spot.On=false - + -- Laser is paused. self.spot.Paused=true - + end --- On before "LaserResume" event. @@ -129427,12 +142066,12 @@ function OPSGROUP:onafterLaserResume(From, Event, To) -- Debug info. self:T(self.lid.."Resuming LASER") - + -- Unset paused. self.spot.Paused=false -- Set target. - local target=nil + local target=nil if self.spot.TargetType==0 then target=self.spot.Coordinate elseif self.spot.TargetType==1 or self.spot.TargetType==2 then @@ -129446,7 +142085,7 @@ function OPSGROUP:onafterLaserResume(From, Event, To) -- Debug message. self:T(self.lid.."Switching LASER on again") - + self:LaserOn(target) end @@ -129465,16 +142104,16 @@ function OPSGROUP:onafterLaserCode(From, Event, To, Code) -- Debug message. self:T2(self.lid..string.format("Setting LASER Code to %d", self.spot.Code)) - + if self.spot.On then - + -- Debug info. self:T(self.lid..string.format("New LASER Code is %d", self.spot.Code)) - + -- Set LASER code. self.spot.Laser:setCode(self.spot.Code) end - + end --- On after "LaserLostLOS" event. @@ -129484,23 +142123,19 @@ end -- @param #string To To state. function OPSGROUP:onafterLaserLostLOS(From, Event, To) - --env.info("FF lost LOS") - -- No of sight. self.spot.LOS=false - + -- Lost line of sight. self.spot.lostLOS=true if self.spot.On then - - --env.info("FF lost LOS ==> pause laser") -- Switch laser off. self:LaserPause() - + end - + end --- On after "LaserGotLOS" event. @@ -129512,24 +142147,19 @@ function OPSGROUP:onafterLaserGotLOS(From, Event, To) -- Has line of sight. self.spot.LOS=true - - --env.info("FF Laser Got LOS") if self.spot.lostLOS then - + -- Did not loose LOS anymore. self.spot.lostLOS=false - - --env.info("FF had lost LOS and regained it") -- Resume laser if currently paused. if self.spot.Paused then - --env.info("FF laser was paused ==> resume") self:LaserResume() end end - + end --- Set LASER target. @@ -129541,17 +142171,17 @@ function OPSGROUP:SetLaserTarget(Target) -- Check object type. if Target:IsInstanceOf("SCENERY") then - + -- Scenery as target. Treat it like a coordinate. Set offset to 1 meter above ground. self.spot.TargetType=0 self.spot.offsetTarget={x=0, y=1, z=0} - + elseif Target:IsInstanceOf("POSITIONABLE") then - + local target=Target --Wrapper.Positionable#POSITIONABLE - + if target:IsAlive() then - + if target:IsInstanceOf("GROUP") then -- We got a GROUP as target. self.spot.TargetGroup=target @@ -129566,39 +142196,37 @@ function OPSGROUP:SetLaserTarget(Target) self.spot.TargetType=2 end end - + -- Get object size. local size,x,y,z=self.spot.TargetUnit:GetObjectSize() - + if y then self.spot.offsetTarget={x=0, y=y*0.75, z=0} else self.spot.offsetTarget={x=0, 2, z=0} end - - --env.info(string.format("Target offset %.3f", y)) - + else - self:E("WARNING: LASER target is not alive!") + self:T("WARNING: LASER target is not alive!") return end - + elseif Target:IsInstanceOf("COORDINATE") then - + -- Coordinate as target. self.spot.TargetType=0 self.spot.offsetTarget={x=0, y=0, z=0} - + else - self:E(self.lid.."ERROR: LASER target should be a POSITIONABLE (GROUP, UNIT or STATIC) or a COORDINATE object!") + self:T(self.lid.."ERROR: LASER target should be a POSITIONABLE (GROUP, UNIT or STATIC) or a COORDINATE object!") return end - + -- Set vec3 and account for target offset. self.spot.vec3=UTILS.VecAdd(Target:GetVec3(), self.spot.offsetTarget) - + -- Set coordinate. - self.spot.Coordinate:UpdateFromVec3(self.spot.vec3) + self.spot.Coordinate:UpdateFromVec3(self.spot.vec3) end end @@ -129609,31 +142237,31 @@ function OPSGROUP:_UpdateLaser() -- Check if we have a POSITIONABLE to lase. if self.spot.TargetUnit then - + --- -- Lasing a possibly moving target --- - + if self.spot.TargetUnit:IsAlive() then - -- Get current target position. + -- Get current target position. local vec3=self.spot.TargetUnit:GetVec3() - + -- Add target offset. vec3=UTILS.VecAdd(vec3, self.spot.offsetTarget) - - -- Calculate distance + + -- Calculate distance local dist=UTILS.VecDist3D(vec3, self.spot.vec3) -- Store current position. self.spot.vec3=vec3 - + -- Update beam coordinate. self.spot.Coordinate:UpdateFromVec3(vec3) - + -- Update laser if target moved more than one meter. if dist>1 then - + -- If the laser is ON, set the new laser target point. if self.spot.On then self.spot.Laser:setPoint(vec3) @@ -129641,16 +142269,16 @@ function OPSGROUP:_UpdateLaser() self.spot.IR:setPoint(vec3) end end - + end - + else - + if self.spot.TargetGroup and self.spot.TargetGroup:IsAlive() then - + -- Get first alive unit in the group. local unit=self.spot.TargetGroup:GetHighestThreat() - + if unit then self:T(self.lid..string.format("Switching to target unit %s in the group", unit:GetName())) self.spot.TargetUnit=unit @@ -129660,45 +142288,135 @@ function OPSGROUP:_UpdateLaser() -- Switch laser off. self:T(self.lid.."Target is not alive any more ==> switching LASER off") self:LaserOff() - return + return end - + else - + -- Switch laser off. self:T(self.lid.."Target is not alive any more ==> switching LASER off") self:LaserOff() return end - - end + + end end - + -- Check LOS. if self.spot.CheckLOS then - + -- Check current LOS. local los=self:HasLoS(self.spot.Coordinate, self.spot.element, self.spot.offset) - - --env.info(string.format("FF check LOS current=%s previous=%s", tostring(los), tostring(self.spot.LOS))) - - if los then - -- Got LOS + + if los then + -- Got LOS if self.spot.lostLOS then - --self:I({los=self.spot.LOS, coord=self.spot.Coordinate, offset=self.spot.offset}) + --self:T({los=self.spot.LOS, coord=self.spot.Coordinate, offset=self.spot.offset}) self:LaserGotLOS() end - - else - -- No LOS currently + + else + -- No LOS currently if not self.spot.lostLOS then self:LaserLostLOS() - end + end + + end + + end + +end + +--- On before "ElementSpawned" event. Check that element is not in status spawned already. +-- @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. +function OPSGROUP:onbeforeElementSpawned(From, Event, To, Element) + + if Element and Element.status==OPSGROUP.ElementStatus.SPAWNED then + self:T2(self.lid..string.format("Element %s is already spawned ==> Transition denied!", Element.name)) + return false + end + + return true +end + +--- On after "ElementInUtero" 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. +function OPSGROUP:onafterElementInUtero(From, Event, To, Element) + self:T(self.lid..string.format("Element in utero %s", Element.name)) + + -- Set element status. + self:_UpdateStatus(Element, OPSGROUP.ElementStatus.INUTERO) + +end + +--- On after "ElementDamaged" 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. +function OPSGROUP:onafterElementDamaged(From, Event, To, Element) + self:T(self.lid..string.format("Element damaged %s", Element.name)) + + if Element and (Element.status~=OPSGROUP.ElementStatus.DEAD and Element.status~=OPSGROUP.ElementStatus.INUTERO) then + + local lifepoints=0 + if Element.DCSunit and Element.DCSunit:isExist() then + + -- Get life of unit + lifepoints=Element.DCSunit:getLife() + + -- Debug output. + self:T(self.lid..string.format("Element life %s: %.2f/%.2f", Element.name, lifepoints, Element.life0)) + + end + + if lifepoints<=1.0 then + self:T(self.lid..string.format("Element %s life %.2f <= 1.0 ==> Destroyed!", Element.name, lifepoints)) + self:ElementDestroyed(Element) end end - + +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 @@ -129710,7 +142428,7 @@ end -- @param #OPSGROUP.Element Element The flight group element. function OPSGROUP:onafterElementDestroyed(From, Event, To, Element) self:T(self.lid..string.format("Element destroyed %s", Element.name)) - + -- Cancel all missions. for _,_mission in pairs(self.missionqueue) do local mission=_mission --Ops.Auftrag#AUFTRAG @@ -129718,13 +142436,13 @@ function OPSGROUP:onafterElementDestroyed(From, Event, To, Element) mission:ElementDestroyed(self, Element) end - + -- Increase counter. self.Ndestroyed=self.Ndestroyed+1 - -- Set element status. - self:_UpdateStatus(Element, OPSGROUP.ElementStatus.DEAD) - + -- Element is dead. + self:ElementDead(Element) + end --- On after "ElementDead" event. @@ -129734,23 +142452,25 @@ end -- @param #string To To state. -- @param #OPSGROUP.Element Element The flight group element. function OPSGROUP:onafterElementDead(From, Event, To, Element) - self:T(self.lid..string.format("Element dead %s at t=%.3f", Element.name, timer.getTime())) - + + -- Debug info. + self:I(self.lid..string.format("Element dead %s at t=%.3f", Element.name, timer.getTime())) + -- Set element status. self:_UpdateStatus(Element, OPSGROUP.ElementStatus.DEAD) - + -- Check if element was lasing and if so, switch to another unit alive to lase. if self.spot.On and self.spot.element.name==Element.name then - + -- Switch laser off. self:LaserOff() - - -- If there is another element alive, switch laser on again. + + -- If there is another element alive, switch laser on again. if self:GetNelements()>0 then - + -- New target if any. local target=nil - + if self.spot.TargetType==0 then -- Coordinate target=self.spot.Coordinate @@ -129758,21 +142478,341 @@ function OPSGROUP:onafterElementDead(From, Event, To, Element) -- Static or unit if self.spot.TargetUnit and self.spot.TargetUnit:IsAlive() then target=self.spot.TargetUnit - end + end elseif self.spot.TargetType==3 then -- Group if self.spot.TargetGroup and self.spot.TargetGroup:IsAlive() then target=self.spot.TargetGroup - end + end end - + -- Switch laser on again. if target then self:__LaserOn(-1, target) end end end + + + -- Clear cargo bay of element. + for i=#Element.cargoBay,1,-1 do + local cargo=Element.cargoBay[i] --#OPSGROUP.MyCargo + + -- Remove from cargo bay. + self:_DelCargobay(cargo.group) + + if cargo.group and not (cargo.group:IsDead() or cargo.group:IsStopped()) then + + -- Remove my carrier + cargo.group:_RemoveMyCarrier() + + if cargo.reserved then + + -- This group was not loaded yet ==> Not cargo any more. + cargo.group:_NewCargoStatus(OPSGROUP.CargoStatus.NOTCARGO) + + else + + -- Carrier dead ==> cargo dead. + for _,cargoelement in pairs(cargo.group.elements) do + + -- Debug info. + self:T2(self.lid.."Cargo element dead "..cargoelement.name) + + -- Trigger dead event. + cargo.group:ElementDead(cargoelement) + + end + end + + end + end + +end + +--- On after "Respawn" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #table Template The template used to respawn the group. Default is the inital template of the group. +function OPSGROUP:onafterRespawn(From, Event, To, Template) + + -- Debug info. + self:T(self.lid.."Respawning group!") + + -- Copy template. + local template=UTILS.DeepCopy(Template or self.template) + + -- Late activation off. + template.lateActivation=false + + self:_Respawn(0, template) + +end + +--- Teleport the group to a different location. +-- @param #OPSGROUP self +-- @param Core.Point#COORDINATE Coordinate Coordinate where the group is teleported to. +-- @param #number Delay Delay in seconds before respawn happens. Default 0. +-- @param #boolean NoPauseMission If `true`, dont pause a running mission. +-- @return #OPSGROUP self +function OPSGROUP:Teleport(Coordinate, Delay, NoPauseMission) + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP.Teleport, self, Coordinate, 0, NoPauseMission) + else + + -- Debug message. + self:T(self.lid.."FF Teleporting...") + --Coordinate:MarkToAll("Teleport "..self.groupname) + + -- Check if we have a mission running. + if self:IsOnMission() and not NoPauseMission then + self:T(self.lid.."Pausing current mission for telport") + self:PauseMission() + end + + -- Get copy of template. + local Template=UTILS.DeepCopy(self.template) --DCS#Template + + -- Set late activation of template to current state. + Template.lateActivation=self:IsLateActivated() + + -- Not uncontrolled. + Template.uncontrolled=false + + -- 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. + local units=Template.units + + -- Table with teleported vectors. + local d={} + for i=1,#units do + local unit=units[i] + d[i]={x=Coordinate.x+(units[i].x-units[1].x), y=Coordinate.z+units[i].y-units[1].y} + end + + for i=#units,1,-1 do + local unit=units[i] + + -- Get element. + local element=self:GetElementByName(unit.name) + + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + + -- No parking. + unit.parking=nil + unit.parking_id=nil + + -- Current position. + local vec3=element.unit:GetVec3() + + -- Current heading. + local heading=element.unit:GetHeading() + + -- Set new x,y. + unit.x=d[i].x + unit.y=d[i].y + + -- Set altitude. + unit.alt=Coordinate.y + + -- Set heading. + unit.heading=math.rad(heading) + unit.psi=-unit.heading + else + table.remove(units, i) + end + end + + -- Respawn from new template. + self:_Respawn(0, Template, true) + + end +end + +--- Respawn the group. +-- @param #OPSGROUP self +-- @param #number Delay Delay in seconds before respawn happens. Default 0. +-- @param DCS#Template Template (optional) The template of the Group retrieved with GROUP:GetTemplate(). If the template is not provided, the template will be retrieved of the group itself. +-- @param #boolean Reset Reset waypoints and reinit group if `true`. +-- @return #OPSGROUP self +function OPSGROUP:_Respawn(Delay, Template, Reset) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP._Respawn, self, 0, Template, Reset) + else + + -- Debug message. + self:T2(self.lid.."FF _Respawn") + + -- Given template or get copy of old. + Template=Template or self:_GetTemplate(true) + + -- Number of destroyed units. + self.Ndestroyed=0 + self.Nhit=0 + + -- Check if group is currently alive. + if self:IsAlive() then + + --- + -- Group is ALIVE + --- + + -- Template units. + local units=Template.units + + for i=#units,1,-1 do + local unit=units[i] + + -- Get the element. + local element=self:GetElementByName(unit.name) + + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + + if not Reset then + + -- Parking ID. + unit.parking=element.parking and element.parking.TerminalID or unit.parking + unit.parking_id=nil + + -- Get current position vector. + local vec3=element.unit:GetVec3() + + -- Get heading. + local heading=element.unit:GetHeading() + + -- Set unit position. + unit.x=vec3.x + unit.y=vec3.z + unit.alt=vec3.y + + -- Set heading in rad. + unit.heading=math.rad(heading) + unit.psi=-unit.heading + + end + + else + + -- Element is dead. Remove from template. + table.remove(units, i) + + self.Ndestroyed=self.Ndestroyed+1 + + end + end + + + -- Despawn old group. Dont trigger any remove unit event since this is a respawn. + self:Despawn(0, true) + + else + + --- + -- Group is NOT ALIVE + --- + + -- Ensure elements in utero. + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + self:ElementInUtero(element) + end + + end + + -- Debug output. + self:T({Template=Template}) + + -- Spawn new group. + self.group=_DATABASE:Spawn(Template) + + -- Set DCS group and controller. + self.dcsgroup=self:GetDCSGroup() + self.controller=self.dcsgroup:getController() + + -- Set activation and controlled state. + self.isLateActivated=Template.lateActivation + self.isUncontrolled=Template.uncontrolled + + -- Not dead or destroyed any more. + self.isDead=false + self.isDestroyed=false + + + self.groupinitialized=false + self.wpcounter=1 + self.currentwp=1 + + -- Init waypoints. + self:_InitWaypoints() + + -- Init Group. + self:_InitGroup(Template) + + -- Reset events. + --self:ResetEvents() + + end + + return self +end + +--- On after "InUtero" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterInUtero(From, Event, To) + self:T(self.lid..string.format("Group inutero at t=%.3f", timer.getTime())) + --TODO: set element status to inutero +end + +--- On after "Damaged" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterDamaged(From, Event, To) + self:T(self.lid..string.format("Group damaged at t=%.3f", timer.getTime())) + + --[[ + local lifemin=nil + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if element.status~=OPSGROUP.ElementStatus.DEAD and element.status~=OPSGROUP.ElementStatus.INUTERO then + local life, life0=self:GetLifePoints(element) + if lifemin==nil or life Asset group is gone. + self.cohort:DelGroup(self.groupname) + end + else + -- Not all assets were destroyed (despawn) ==> Add asset back to legion? + end + + + if self.legion then + if not self:IsInUtero() then + + -- Get asset. + local asset=self.legion:GetAssetByName(self.groupname) + + -- Get request. + local request=self.legion:GetRequestByID(asset.rid) + + -- Trigger asset dead event. + self.legion:AssetDead(asset, request) + end + + -- 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 + +--- On before "Stop" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onbeforeStop(From, Event, To) + + -- We check if + if self:IsAlive() then + self:T(self.lid..string.format("WARNING: Group is still alive! Will not stop the FSM. Use :Despawn() instead")) + return false + end + + return true end --- On after "Stop" event. @@ -129819,23 +142946,2239 @@ end -- @param #string Event Event. -- @param #string To To state. function OPSGROUP:onafterStop(From, Event, To) + + -- Handle events: + self:UnHandleEvent(EVENTS.Birth) + self:UnHandleEvent(EVENTS.Dead) + self:UnHandleEvent(EVENTS.RemoveUnit) + + -- Handle events: + if self.isFlightgroup then + self:UnHandleEvent(EVENTS.EngineStartup) + self:UnHandleEvent(EVENTS.Takeoff) + self:UnHandleEvent(EVENTS.Land) + self:UnHandleEvent(EVENTS.EngineShutdown) + self:UnHandleEvent(EVENTS.PilotDead) + 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 + local mission=_mission --Ops.Auftrag#AUFTRAG + self:MissionCancel(mission) + end + -- Stop check timers. self.timerCheckZone:Stop() self.timerQueueUpdate:Stop() + self.timerStatus:Stop() -- Stop FSM scheduler. self.CallScheduler:Clear() - + if self.Scheduler then + self.Scheduler:Clear() + end + + -- Flightcontrol. + if self.flightcontrol then + 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 local life, life0=self:GetLifePoints() local state=self:GetState() local text=string.format("WARNING: Group is still alive! Current state=%s. Life points=%d/%d. Use OPSGROUP:Destroy() or OPSGROUP:Despawn() for a clean stop", state, life, life0) - self:E(self.lid..text) + self:T(self.lid..text) end + -- Remove flight from data base. + _DATABASE.FLIGHTGROUPS[self.groupname]=nil + -- Debug output. - self:I(self.lid.."STOPPED! Unhandled events, cleared scheduler and removed from database.") + self:I(self.lid.."STOPPED! Unhandled events, cleared scheduler and removed from _DATABASE") +end + +--- On after "OutOfAmmo" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterOutOfAmmo(From, Event, To) + self:T(self.lid..string.format("Group is out of ammo at t=%.3f", timer.getTime())) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Cargo Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check cargo transport assignments. +-- @param #OPSGROUP self +-- @return #OPSGROUP self +function OPSGROUP:_CheckCargoTransport() + + -- Abs. missin time in seconds. + local Time=timer.getAbsTime() + + -- Cargo bay debug info. + if self.verbose>=1 then + local text="" + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + for _,_cargo in pairs(element.cargoBay) do + local cargo=_cargo --#OPSGROUP.MyCargo + text=text..string.format("\n- %s in carrier %s, reserved=%s", tostring(cargo.group:GetName()), tostring(element.name), tostring(cargo.reserved)) + end + end + if text=="" then + text=" empty" + end + self:T(self.lid.."Cargo bay:"..text) + end + + -- Cargo queue debug info. + if self.verbose>=3 then + local text="" + for i,_transport in pairs(self.cargoqueue) do + local transport=_transport --Ops.OpsTransport#OPSTRANSPORT + local pickupzone=transport:GetPickupZone() + local deployzone=transport:GetDeployZone() + local pickupname=pickupzone and pickupzone:GetName() or "unknown" + local deployname=deployzone and deployzone:GetName() or "unknown" + text=text..string.format("\n[%d] UID=%d Status=%s: %s --> %s", i, transport.uid, transport:GetState(), pickupname, deployname) + for j,_cargo in pairs(transport:GetCargos()) do + local cargo=_cargo --#OPSGROUP.CargoGroup + local state=cargo.opsgroup:GetState() + local status=cargo.opsgroup.cargoStatus + local name=cargo.opsgroup.groupname + local carriergroup, carrierelement, reserved=cargo.opsgroup:_GetMyCarrier() + local carrierGroupname=carriergroup and carriergroup.groupname or "none" + local carrierElementname=carrierelement and carrierelement.name or "none" + text=text..string.format("\n (%d) %s [%s]: %s, carrier=%s(%s), delivered=%s", j, name, state, status, carrierGroupname, carrierElementname, tostring(cargo.delivered)) + end + end + if text~="" then + self:T(self.lid.."Cargo queue:"..text) + end + end + + if self.cargoTransport and self.cargoTransport:GetCarrierTransportStatus(self)==OPSTRANSPORT.Status.DELIVERED then + -- Remove transport from queue. + self:DelOpsTransport(self.cargoTransport) + -- No current transport any more. + self.cargoTransport=nil + self.cargoTZC=nil + end + + -- Check if there is anything in the queue. + if not self.cargoTransport and not self:IsOnMission() then + self.cargoTransport=self:_GetNextCargoTransport() + if self.cargoTransport and not self:IsActive() then + self:Activate() + end + end + + -- Now handle the transport. + if self.cargoTransport then + + if self:IsNotCarrier() then + + -- Unset time stamps. + self.Tpickingup=nil + self.Tloading=nil + self.Ttransporting=nil + self.Tunloading=nil + + -- Get transport zone combo (TZC). + self.cargoTZC=self.cargoTransport:_GetTransportZoneCombo(self) + + if self.cargoTZC then + + -- Found TZC + self:T(self.lid..string.format("Not carrier ==> pickup at %s [TZC UID=%d]", self.cargoTZC.PickupZone and self.cargoTZC.PickupZone:GetName() or "unknown", self.cargoTZC.uid)) + + -- Initiate the cargo transport process. + self:__Pickup(-1) + + else + self:T2(self.lid.."Not carrier ==> No TZC found") + end + + elseif self:IsPickingup() then + + -- Set time stamp. + self.Tpickingup=self.Tpickingup or Time + + -- Current pickup time. + local tpickingup=Time-self.Tpickingup + + -- Debug Info. + self:T(self.lid..string.format("Picking up at %s [TZC UID=%d] for %s sec...", self.cargoTZC.PickupZone and self.cargoTZC.PickupZone:GetName() or "unknown", self.cargoTZC.uid, tpickingup)) + + elseif self:IsLoading() then + + -- Set loading time stamp. + self.Tloading=self.Tloading or Time + + -- Current pickup time. + local tloading=Time-self.Tloading + + --TODO: Check max loading time. If exceeded ==> abort transport. + + -- Debug info. + self:T(self.lid..string.format("Loading at %s [TZC UID=%d] for %s sec...", self.cargoTZC.PickupZone and self.cargoTZC.PickupZone:GetName() or "unknown", self.cargoTZC.uid, tloading)) + + local boarding=false + local gotcargo=false + for _,_cargo in pairs(self.cargoTZC.Cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + + -- Check if anyone is still boarding. + if cargo.opsgroup:IsBoarding(self.groupname) then + boarding=true + end + + -- Check if we have any cargo to transport. + if cargo.opsgroup:IsLoaded(self.groupname) then + gotcargo=true + end + + end + + -- Boarding finished ==> Transport cargo. + if gotcargo and self.cargoTransport:_CheckRequiredCargos(self.cargoTZC) and not boarding then + self:T(self.lid.."Boarding finished ==> Loaded") + self:LoadingDone() + else + -- No cargo and no one is boarding ==> check again if we can make anyone board. + self:Loading() + end + + -- No cargo and no one is boarding ==> check again if we can make anyone board. + if not gotcargo and not boarding then + --self:Loading() + end + + elseif self:IsTransporting() then + + -- Set time stamp. + self.Ttransporting=self.Ttransporting or Time + + -- Current pickup time. + local ttransporting=Time-self.Ttransporting + + -- Debug info. + self:T(self.lid.."Transporting (nothing to do)") + + elseif self:IsUnloading() then + + -- Set time stamp. + self.Tunloading=self.Tunloading or Time + + -- Current pickup time. + local tunloading=Time-self.Tunloading + + -- Debug info. + self:T(self.lid.."Unloading ==> Checking if all cargo was delivered") + + local delivered=true + for _,_cargo in pairs(self.cargoTZC.Cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + + local carrierGroup=cargo.opsgroup:_GetMyCarrierGroup() + + -- Check that this group is + if (carrierGroup and carrierGroup:GetName()==self:GetName()) and not cargo.delivered then + delivered=false + break + end + + end + + -- Unloading finished ==> pickup next batch or call it a day. + if delivered then + self:T(self.lid.."Unloading finished ==> UnloadingDone") + self:UnloadingDone() + else + self:Unloading() + end + + end + + -- Debug info. (At this point, we might not have a current cargo transport ==> hence the check) + if self.verbose>=2 and self.cargoTransport then + local pickupzone=self.cargoTransport:GetPickupZone(self.cargoTZC) + local deployzone=self.cargoTransport:GetDeployZone(self.cargoTZC) + local pickupname=pickupzone and pickupzone:GetName() or "unknown" + local deployname=deployzone and deployzone:GetName() or "unknown" + local text=string.format("Carrier [%s]: %s --> %s", self.carrierStatus, pickupname, deployname) + for _,_cargo in pairs(self.cargoTransport:GetCargos(self.cargoTZC)) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + local name=cargo.opsgroup:GetName() + local gstatus=cargo.opsgroup:GetState() + local cstatus=cargo.opsgroup.cargoStatus + local weight=cargo.opsgroup:GetWeightTotal() + local carriergroup, carrierelement, reserved=cargo.opsgroup:_GetMyCarrier() + local carrierGroupname=carriergroup and carriergroup.groupname or "none" + local carrierElementname=carrierelement and carrierelement.name or "none" + text=text..string.format("\n- %s (%.1f kg) [%s]: %s, carrier=%s (%s), delivered=%s", name, weight, gstatus, cstatus, carrierElementname, carrierGroupname, tostring(cargo.delivered)) + end + self:I(self.lid..text) + end + + end + + return self +end + + +--- Check if a group is in the cargo bay. +-- @param #OPSGROUP self +-- @param #OPSGROUP OpsGroup Group to check. +-- @return #boolean If `true`, group is in the cargo bay. +function OPSGROUP:_IsInCargobay(OpsGroup) + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + for _,_cargo in pairs(element.cargoBay) do + local cargo=_cargo --#OPSGROUP.MyCargo + if cargo.group.groupname==OpsGroup.groupname then + return true + end + end + end + + return false +end + +--- Add OPSGROUP to cargo bay of a carrier. +-- @param #OPSGROUP self +-- @param #OPSGROUP CargoGroup Cargo group. +-- @param #OPSGROUP.Element CarrierElement The element of the carrier. +-- @param #boolean Reserved Only reserve the cargo bay space. +function OPSGROUP:_AddCargobay(CargoGroup, CarrierElement, Reserved) + + --TODO: Check group is not already in cargobay of this carrier or any other carrier. + + local cargo=self:_GetCargobay(CargoGroup) + + if cargo then + cargo.reserved=Reserved + else + + cargo={} --#OPSGROUP.MyCargo + cargo.group=CargoGroup + cargo.reserved=Reserved + + table.insert(CarrierElement.cargoBay, cargo) + end + + + -- Set my carrier. + CargoGroup:_SetMyCarrier(self, CarrierElement, Reserved) + + -- Fill cargo bay (obsolete). + self.cargoBay[CargoGroup.groupname]=CarrierElement.name + + if not Reserved then + + -- Cargo weight. + local weight=CargoGroup:GetWeightTotal() + + -- Add weight to carrier. + self:AddWeightCargo(CarrierElement.name, weight) + + end + + return self +end + +--- Get all groups currently loaded as cargo. +-- @param #OPSGROUP self +-- @param #string CarrierName (Optional) Only return cargo groups loaded into a particular carrier unit. +-- @return #table Cargo ops groups. +function OPSGROUP:GetCargoGroups(CarrierName) + local cargos={} + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if CarrierName==nil or element.name==CarrierName then + for _,_cargo in pairs(element.cargoBay) do + local cargo=_cargo --#OPSGROUP.MyCargo + if not cargo.reserved then + table.insert(cargos, cargo.group) + end + end + end + end + + return cargos +end + +--- Get cargo bay item. +-- @param #OPSGROUP self +-- @param #OPSGROUP CargoGroup Cargo group. +-- @return #OPSGROUP.MyCargo Cargo bay item or `nil` if the group is not in the carrier. +-- @return #number CargoBayIndex Index of item in the cargo bay table. +-- @return #OPSGROUP.Element Carrier element. +function OPSGROUP:_GetCargobay(CargoGroup) + + -- Loop over elements and their cargo bay items. + local CarrierElement=nil --#OPSGROUP.Element + local cargobayIndex=nil + local reserved=nil + for i,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + for j,_cargo in pairs(element.cargoBay) do + local cargo=_cargo --#OPSGROUP.MyCargo + if cargo.group and cargo.group.groupname==CargoGroup.groupname then + return cargo, j, element + end + end + end + + return nil, nil, nil +end + +--- Remove OPSGROUP from cargo bay of a carrier. +-- @param #OPSGROUP self +-- @param #OPSGROUP CargoGroup Cargo group. +-- @return #boolean If `true`, cargo could be removed. +function OPSGROUP:_DelCargobay(CargoGroup) + + if self.cargoBay[CargoGroup.groupname] then + + -- Not in cargo bay any more. + self.cargoBay[CargoGroup.groupname]=nil + + end + + -- Get cargo bay info. + local cargoBayItem, cargoBayIndex, CarrierElement=self:_GetCargobay(CargoGroup) + + if cargoBayItem and cargoBayIndex then + + -- Debug info. + self:T(self.lid..string.format("Removing cargo group %s from cargo bay (index=%d) of carrier %s", CargoGroup:GetName(), cargoBayIndex, CarrierElement.name)) + + -- Remove + table.remove(CarrierElement.cargoBay, cargoBayIndex) + + -- Reduce weight (if cargo space was not just reserved). + if not cargoBayItem.reserved then + local weight=CargoGroup:GetWeightTotal() + self:RedWeightCargo(CarrierElement.name, weight) + end + + return true + end + + self:T(self.lid.."ERROR: Group is not in cargo bay. Cannot remove it!") + return false +end + +--- Get cargo transport from cargo queue. +-- @param #OPSGROUP self +-- @return Ops.OpsTransport#OPSTRANSPORT The next due cargo transport or `nil`. +function OPSGROUP:_GetNextCargoTransport() + + -- Current position. + local coord=self:GetCoordinate() + + -- Sort results table wrt prio and distance to pickup zone. + local function _sort(a, b) + local transportA=a --Ops.OpsTransport#OPSTRANSPORT + local transportB=b --Ops.OpsTransport#OPSTRANSPORT + --TODO: Include distance + --local distA=transportA.pickupzone:GetCoordinate():Get2DDistance(coord) + --local distB=transportB.pickupzone:GetCoordinate():Get2DDistance(coord) + return (transportA.priomaxweight then + maxweight=weight + end + + end + end + + return maxweight +end + + +--- Get weight of the internal cargo the group is carriing right now. +-- @param #OPSGROUP self +-- @param #string UnitName Name of the unit. Default is of the whole group. +-- @param #boolean IncludeReserved If `false`, cargo weight that is only *reserved* is **not** counted. By default (`true` or `nil`), the reserved cargo is included. +-- @return #number Cargo weight in kg. +function OPSGROUP:GetWeightCargo(UnitName, IncludeReserved) + + -- Calculate weight based on actual cargo weight. + local weight=0 + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + if (UnitName==nil or UnitName==element.name) and element.status~=OPSGROUP.ElementStatus.DEAD then + + weight=weight+element.weightCargo or 0 + + end + + end + + -- Calculate weight from stuff in cargo bay. By default this includes the reserved weight if a cargo group was assigned and is currently boarding. + local gewicht=0 + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if (UnitName==nil or UnitName==element.name) and (element and element.status~=OPSGROUP.ElementStatus.DEAD) then + for _,_cargo in pairs(element.cargoBay) do + local cargo=_cargo --#OPSGROUP.MyCargo + if (not cargo.reserved) or (cargo.reserved==true and (IncludeReserved==true or IncludeReserved==nil)) then + local cargoweight=cargo.group:GetWeightTotal() + gewicht=gewicht+cargoweight + --self:I(self.lid..string.format("unit=%s (reserved=%s): cargo=%s weight=%d, total weight=%d", tostring(UnitName), tostring(IncludeReserved), cargo.group:GetName(), cargoweight, weight)) + end + end + end + end + + -- Debug info. + self:T3(self.lid..string.format("Unit=%s (reserved=%s): weight=%d, gewicht=%d", tostring(UnitName), tostring(IncludeReserved), weight, gewicht)) + + -- Quick check. + if IncludeReserved==false and gewicht~=weight then + self:T(self.lid..string.format("ERROR: FF weight!=gewicht: weight=%.1f, gewicht=%.1f", weight, gewicht)) + end + + return gewicht +end + +--- Get max weight of the internal cargo the group can carry. Optionally, the max cargo weight of a specific unit can be requested. +-- @param #OPSGROUP self +-- @param #string UnitName Name of the unit. Default is of the whole group. +-- @return #number Max cargo weight in kg. This does **not** include any cargo loaded or reserved currently. +function OPSGROUP:GetWeightCargoMax(UnitName) + + local weight=0 + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + if (UnitName==nil or UnitName==element.name) and element.status~=OPSGROUP.ElementStatus.DEAD then + + weight=weight+element.weightMaxCargo + + end + + end + + return weight +end + +--- Get OPSGROUPs in the cargo bay. +-- @param #OPSGROUP self +-- @return #table Cargo OPSGROUPs. +function OPSGROUP:GetCargoOpsGroups() + + local opsgroups={} + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + for _,_cargo in pairs(element.cargoBay) do + local cargo=_cargo --#OPSGROUP.MyCargo + table.insert(opsgroups, cargo.group) + end + end + + return opsgroups +end + +--- Add weight to the internal cargo of an element of the group. +-- @param #OPSGROUP self +-- @param #string UnitName Name of the unit. Default is of the whole group. +-- @param #number Weight Cargo weight to be added in kg. +function OPSGROUP:AddWeightCargo(UnitName, Weight) + + local element=self:GetElementByName(UnitName) + + if element then --we do not check if the element is actually alive because we need to remove cargo from dead units + + -- Add weight. + element.weightCargo=element.weightCargo+Weight + + -- Debug info. + self:T(self.lid..string.format("%s: Adding %.1f kg cargo weight. New cargo weight=%.1f kg", UnitName, Weight, element.weightCargo)) + + -- For airborne units, we set the weight in game. + if self.isFlightgroup then + trigger.action.setUnitInternalCargo(element.name, element.weightCargo) --https://wiki.hoggitworld.com/view/DCS_func_setUnitInternalCargo + end + + end + + return self +end + +--- Reduce weight to the internal cargo of an element of the group. +-- @param #OPSGROUP self +-- @param #string UnitName Name of the unit. +-- @param #number Weight Cargo weight to be reduced in kg. +function OPSGROUP:RedWeightCargo(UnitName, Weight) + + -- Reduce weight by adding negative weight. + self:AddWeightCargo(UnitName, -Weight) + + return self +end + +--- Check if the group can *in principle* be carrier of a cargo group. This checks the max cargo capacity of the group but *not* how much cargo is already loaded (if any). +-- **Note** that the cargo group *cannot* be split into units, i.e. the largest cargo bay of any element of the group must be able to load the whole cargo group in one piece. +-- @param #OPSGROUP self +-- @param #OPSGROUP CargoGroup Cargo group, which needs a carrier. +-- @return #boolean If `true`, there is an element of the group that can load the whole cargo group. +function OPSGROUP:CanCargo(CargoGroup) + + if CargoGroup then + + local weight=CargoGroup:GetWeightTotal() + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + -- Check that element is not dead and has + if element and element.status~=OPSGROUP.ElementStatus.DEAD and element.weightMaxCargo>=weight then + return true + end + + end + + end + + return false +end + +--- Add weight to the internal cargo of an element of the group. +-- @param #OPSGROUP self +-- @param #OPSGROUP CargoGroup Cargo group, which needs a carrier. +-- @return #OPSGROUP.Element Carrier able to transport the cargo. +function OPSGROUP:FindCarrierForCargo(CargoGroup) + + local weight=CargoGroup:GetWeightTotal() + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + local free=self:GetFreeCargobay(element.name) + + if free>=weight then + return element + else + self:T3(self.lid..string.format("%s: Weight %d>%d free cargo bay", element.name, weight, free)) + end + + end + + return nil +end + +--- Set my carrier. +-- @param #OPSGROUP self +-- @param #OPSGROUP CarrierGroup Carrier group. +-- @param #OPSGROUP.Element CarrierElement Carrier element. +-- @param #boolean Reserved If `true`, reserve space for me. +function OPSGROUP:_SetMyCarrier(CarrierGroup, CarrierElement, Reserved) + + -- Debug info. + self:T(self.lid..string.format("Setting My Carrier: %s (%s), reserved=%s", CarrierGroup:GetName(), tostring(CarrierElement.name), tostring(Reserved))) + + self.mycarrier.group=CarrierGroup + self.mycarrier.element=CarrierElement + self.mycarrier.reserved=Reserved + + self.cargoTransportUID=CarrierGroup.cargoTransport and CarrierGroup.cargoTransport.uid or nil + +end + +--- Get my carrier group. +-- @param #OPSGROUP self +-- @return #OPSGROUP Carrier group. +function OPSGROUP:_GetMyCarrierGroup() + if self.mycarrier and self.mycarrier.group then + return self.mycarrier.group + end + return nil +end + +--- Get my carrier element. +-- @param #OPSGROUP self +-- @return #OPSGROUP.Element Carrier element. +function OPSGROUP:_GetMyCarrierElement() + if self.mycarrier and self.mycarrier.element then + return self.mycarrier.element + end + return nil +end + +--- Is my carrier reserved. +-- @param #OPSGROUP self +-- @return #boolean If `true`, space for me was reserved. +function OPSGROUP:_IsMyCarrierReserved() + if self.mycarrier then + return self.mycarrier.reserved + end + return nil +end + + + +--- Get my carrier. +-- @param #OPSGROUP self +-- @return #OPSGROUP Carrier group. +-- @return #OPSGROUP.Element Carrier element. +-- @return #boolean If `true`, space is reserved for me +function OPSGROUP:_GetMyCarrier() + return self.mycarrier.group, self.mycarrier.element, self.mycarrier.reserved +end + + +--- Remove my carrier. +-- @param #OPSGROUP self +-- @return #OPSGROUP self +function OPSGROUP:_RemoveMyCarrier() + self:T(self.lid..string.format("Removing my carrier!")) + self.mycarrier.group=nil + self.mycarrier.element=nil + self.mycarrier.reserved=nil + self.mycarrier={} + self.cargoTransportUID=nil + return self +end + +--- On after "Pickup" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterPickup(From, Event, To) + + -- Old status. + local oldstatus=self.carrierStatus + + -- Set carrier status. + self:_NewCarrierStatus(OPSGROUP.CarrierStatus.PICKUP) + + local TZC=self.cargoTZC + + -- Pickup zone. + local Zone=TZC.PickupZone + + -- Check if already in the pickup zone. + local inzone=self:IsInZone(Zone) + + -- Pickup at an airbase. + local airbasePickup=TZC.PickupAirbase --Wrapper.Airbase#AIRBASE + + -- Check if group is already ready for loading. + local ready4loading=false + if self:IsArmygroup() or self:IsNavygroup() then + + -- Army and Navy groups just need to be inside the zone. + ready4loading=inzone + + else + + -- Aircraft is already parking at the pickup airbase. + ready4loading=self.currbase and airbasePickup and self.currbase:GetName()==airbasePickup:GetName() and self:IsParking() + + -- If a helo is landed in the zone, we also are ready for loading. + if ready4loading==false and self.isHelo and self:IsLandedAt() and inzone then + ready4loading=true + end + end + + -- Ready for loading? + if ready4loading then + + -- We are already in the pickup zone ==> wait and initiate loading. + if (self:IsArmygroup() or self:IsNavygroup()) and not self:IsHolding() then + self:FullStop() + end + + -- Start loading. + self:__Loading(-5) + + else + + -- Set surface type of random coordinate. + local surfacetypes=nil + if self:IsArmygroup() or self:IsFlightgroup() then + surfacetypes={land.SurfaceType.LAND} + elseif self:IsNavygroup() then + surfacetypes={land.SurfaceType.WATER} + end + + -- Get a random coordinate in the pickup zone and let the carrier go there. + local Coordinate=Zone:GetRandomCoordinate(nil, nil, surfacetypes) + --Coordinate:MarkToAll(string.format("Pickup coordinate for group %s [Surface type=%d]", self:GetName(), Coordinate:GetSurfaceType())) + + -- Current Waypoint. + local cwp=self:GetWaypointCurrent() + + -- Current waypoint ID. + local uid=cwp and cwp.uid or nil + + -- Add waypoint. + if self:IsFlightgroup() then + + --- + -- Flight Group + --- + + -- Activate uncontrolled group. + if self:IsParking() and self:IsUncontrolled() then + self:StartUncontrolled() + end + + if airbasePickup then + + --- + -- Pickup at airbase + --- + + -- Get a (random) pre-defined transport path. + local path=self.cargoTransport:_GetPathTransport(self.category, self.cargoTZC) + + -- Get transport path. + if path and oldstatus~=OPSGROUP.CarrierStatus.NOTCARRIER then + + for i=#path.waypoints,1,-1 do + local wp=path.waypoints[i] + local coordinate=COORDINATE:NewFromWaypoint(wp) + local waypoint=FLIGHTGROUP.AddWaypoint(self, coordinate, nil, uid, nil, false) ; waypoint.temp=true + uid=waypoint.uid + if i==1 then + waypoint.temp=false + waypoint.detour=1 --Needs to trigger the landatairbase function. + end + end + + else + + local coordinate=self:GetCoordinate():GetIntermediateCoordinate(Coordinate, 0.5) + + --coordinate:MarkToAll("Pickup Inter Coord") + + -- If this is a helo and no ZONE_AIRBASE was given, we make the helo land in the pickup zone. + local waypoint=FLIGHTGROUP.AddWaypoint(self, coordinate, nil, uid, UTILS.MetersToFeet(self.altitudeCruise), true) ; waypoint.detour=1 + + end + + elseif self.isHelo then + + --- + -- Helo can also land in a zone (NOTE: currently VTOL cannot!) + --- + + -- If this is a helo and no ZONE_AIRBASE was given, we make the helo land in the pickup zone. + local waypoint=FLIGHTGROUP.AddWaypoint(self, Coordinate, nil, uid, UTILS.MetersToFeet(self.altitudeCruise), false) ; waypoint.detour=1 + + else + self:T(self.lid.."ERROR: Transportcarrier aircraft cannot land in Pickup zone! Specify a ZONE_AIRBASE as pickup zone") + end + + -- Cancel landedAt task. This should trigger Cruise once airborne. + if self.isHelo and self:IsLandedAt() then + local Task=self:GetTaskCurrent() + if Task then + self:TaskCancel(Task) + else + self:T(self.lid.."ERROR: No current task but landed at?!") + end + end + + if self:IsWaiting() then + self:__Cruise(-2) + end + + elseif self:IsNavygroup() then + + --- + -- Navy Group + --- + + -- Get a (random) pre-defined transport path. + local path=self.cargoTransport:_GetPathTransport(self.category, self.cargoTZC) + + -- Get transport path. + if path then --and oldstatus~=OPSGROUP.CarrierStatus.NOTCARRIER then + for i=#path.waypoints,1,-1 do + local wp=path.waypoints[i] + local coordinate=COORDINATE:NewFromWaypoint(wp) + local waypoint=NAVYGROUP.AddWaypoint(self, coordinate, nil, uid, nil, false) ; waypoint.temp=true + uid=waypoint.uid + end + end + + -- NAVYGROUP + local waypoint=NAVYGROUP.AddWaypoint(self, Coordinate, nil, uid, self.altitudeCruise, false) ; waypoint.detour=1 + + -- Give cruise command. + self:__Cruise(-2) + + + elseif self:IsArmygroup() then + + --- + -- Army Group + --- + + -- Get a (random) pre-defined transport path. + local path=self.cargoTransport:_GetPathTransport(self.category, self.cargoTZC) + + -- Formation used to go to the pickup zone.. + local Formation=self.cargoTransport:_GetFormationTransport(self.cargoTZC) + + -- Get transport path. + if path and oldstatus~=OPSGROUP.CarrierStatus.NOTCARRIER then + for i=#path.waypoints,1,-1 do + local wp=path.waypoints[i] + local coordinate=COORDINATE:NewFromWaypoint(wp) + local waypoint=ARMYGROUP.AddWaypoint(self, coordinate, nil, uid, wp.action, false) ; waypoint.temp=true + uid=waypoint.uid + end + end + + -- ARMYGROUP + local waypoint=ARMYGROUP.AddWaypoint(self, Coordinate, nil, uid, Formation, false) ; waypoint.detour=1 + + -- Give cruise command. + self:__Cruise(-2) + + end + + end + +end + + +--- On after "Loading" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterLoading(From, Event, To) + + -- Set carrier status. + self:_NewCarrierStatus(OPSGROUP.CarrierStatus.LOADING) + + -- Loading time stamp. + self.Tloading=timer.getAbsTime() + + -- Get valid cargos of the TZC. + local cargos={} + for _,_cargo in pairs(self.cargoTZC.Cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + + -- Check if this group can carry the cargo. + local canCargo=self:CanCargo(cargo.opsgroup) + + -- Check if this group is currently acting as carrier. + local isCarrier=cargo.opsgroup:IsPickingup() or cargo.opsgroup:IsLoading() or cargo.opsgroup:IsTransporting() or cargo.opsgroup:IsUnloading() + + -- Check if cargo is not already cargo. + local isNotCargo=cargo.opsgroup:IsNotCargo(true) + + -- 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() + + -- Check if cargo is currently on a mission. + local isOnMission=cargo.opsgroup:IsOnMission() + + -- Check if current mission is using this ops transport. + if isOnMission then + local mission=cargo.opsgroup:GetMissionCurrent() + if mission and mission.opstransport and mission.opstransport.uid==self.cargoTransport.uid then + isOnMission=not isHolding + 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 + table.insert(cargos, cargo) + end + end + + -- Sort results table wrt descending weight. + local function _sort(a, b) + local cargoA=a --Ops.OpsGroup#OPSGROUP.CargoGroup + local cargoB=b --Ops.OpsGroup#OPSGROUP.CargoGroup + return cargoA.opsgroup:GetWeightTotal()>cargoB.opsgroup:GetWeightTotal() + end + table.sort(cargos, _sort) + + -- Loop over all cargos. + for _,_cargo in pairs(cargos) do + local cargo=_cargo --#OPSGROUP.CargoGroup + + -- Find a carrier for this cargo. + local carrier=self:FindCarrierForCargo(cargo.opsgroup) + + if carrier then + + -- Order cargo group to board the carrier. + cargo.opsgroup:Board(self, carrier) + + end + + end +end + +--- Set (new) cargo status. +-- @param #OPSGROUP self +-- @param #string Status New status. +function OPSGROUP:_NewCargoStatus(Status) + + -- Debug info. + if self.verbose>=2 then + self:I(self.lid..string.format("New cargo status: %s --> %s", tostring(self.cargoStatus), tostring(Status))) + end + + -- Set cargo status. + self.cargoStatus=Status + +end + +--- Set (new) carrier status. +-- @param #OPSGROUP self +-- @param #string Status New status. +function OPSGROUP:_NewCarrierStatus(Status) + + -- Debug info. + if self.verbose>=2 then + self:I(self.lid..string.format("New carrier status: %s --> %s", tostring(self.carrierStatus), tostring(Status))) + end + + -- Set cargo status. + self.carrierStatus=Status + +end + +--- Transfer cargo from to another carrier. +-- @param #OPSGROUP self +-- @param #OPSGROUP CargoGroup The cargo group to be transferred. +-- @param #OPSGROUP CarrierGroup The new carrier group. +-- @param #OPSGROUP.Element CarrierElement The new carrier element. +function OPSGROUP:_TransferCargo(CargoGroup, CarrierGroup, CarrierElement) + + -- Debug info. + self:T(self.lid..string.format("Transferring cargo %s to new carrier group %s", CargoGroup:GetName(), CarrierGroup:GetName())) + + -- Unload from this and directly load into the other carrier. + self:Unload(CargoGroup) + CarrierGroup:Load(CargoGroup, CarrierElement) + +end + +--- On after "Load" event. Carrier loads a cargo group into ints cargo bay. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP CargoGroup The OPSGROUP loaded as cargo. +-- @param #OPSGROUP.Element Carrier The carrier element/unit. +function OPSGROUP:onafterLoad(From, Event, To, CargoGroup, Carrier) + + -- Debug info. + self:T(self.lid..string.format("Loading group %s", tostring(CargoGroup.groupname))) + + -- Carrier element. + local carrier=Carrier or CargoGroup:_GetMyCarrierElement() --#OPSGROUP.Element + + -- No carrier provided. + if not carrier then + -- Try to find a carrier manually. + carrier=self:FindCarrierForCargo(CargoGroup) + end + + if carrier then + + --- + -- Embark Cargo + --- + + -- New cargo status. + CargoGroup:_NewCargoStatus(OPSGROUP.CargoStatus.LOADED) + + -- Clear all waypoints. + CargoGroup:ClearWaypoints() + + -- Add into carrier bay. + self:_AddCargobay(CargoGroup, carrier, false) + + -- Despawn this group. + if CargoGroup:IsAlive() then + CargoGroup:Despawn(0, true) + end + + -- Trigger embarked event for cargo group. + CargoGroup:Embarked(self, carrier) + + -- Trigger Loaded event. + self:Loaded(CargoGroup) + + -- Trigger "Loaded" event for current cargo transport. + if self.cargoTransport then + CargoGroup:_DelMyLift(self.cargoTransport) + self.cargoTransport:Loaded(CargoGroup, self, carrier) + else + self:T(self.lid..string.format("WARNING: Loaded cargo but no current OPSTRANSPORT assignment!")) + end + + else + self:T(self.lid.."ERROR: Cargo has no carrier on Load event!") + end + +end + +--- On after "LoadingDone" event. Carrier has loaded all (possible) cargo at the pickup zone. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterLoadingDone(From, Event, To) + + -- Debug info. + self:T(self.lid.."Carrier Loading Done ==> Transport") + + -- Order group to transport. + self:__Transport(1) + +end + +--- On before "Transport" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onbeforeTransport(From, Event, To) + + if self.cargoTransport==nil then + return false + elseif self.cargoTransport:IsDelivered() then --could be if all cargo was dead on boarding + return false + end + + return true +end + + +--- On after "Transport" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterTransport(From, Event, To) + + -- Set carrier status. + self:_NewCarrierStatus(OPSGROUP.CarrierStatus.TRANSPORTING) + + --TODO: This is all very similar to the onafterPickup() function. Could make it general. + + -- Deploy zone. + local Zone=self.cargoTZC.DeployZone + + -- Check if already in deploy zone. + local inzone=self:IsInZone(Zone) + + -- Deploy airbase (if any). + local airbaseDeploy=self.cargoTZC.DeployAirbase --Wrapper.Airbase#AIRBASE + + -- Check if group is already ready for loading. + local ready2deploy=false + if self:IsArmygroup() or self:IsNavygroup() then + ready2deploy=inzone + else + -- Aircraft is already parking at the pickup airbase. + ready2deploy=self.currbase and airbaseDeploy and self.currbase:GetName()==airbaseDeploy:GetName() and self:IsParking() + + -- If a helo is landed in the zone, we also are ready for loading. + if ready2deploy==false and (self.isHelo or self.isVTOL) and self:IsLandedAt() and inzone then + ready2deploy=true + end + end + + if inzone then + + -- We are already in the deploy zone ==> wait and initiate unloading. + if (self:IsArmygroup() or self:IsNavygroup()) and not self:IsHolding() then + self:FullStop() + end + + -- Start unloading. + self:__Unloading(-5) + + else + + local surfacetypes=nil + if self:IsArmygroup() or self:IsFlightgroup() then + surfacetypes={land.SurfaceType.LAND} + elseif self:IsNavygroup() then + surfacetypes={land.SurfaceType.WATER, land.SurfaceType.SHALLOW_WATER} + end + + -- Coord where the carrier goes to unload. + local Coordinate=Zone:GetRandomCoordinate(nil, nil, surfacetypes) --Core.Point#COORDINATE + + --Coordinate:MarkToAll(string.format("Deploy coordinate for group %s [Surface type=%d]", self:GetName(), Coordinate:GetSurfaceType())) + + -- Add waypoint. + if self:IsFlightgroup() then + + -- Activate uncontrolled group. + if self:IsParking() and self:IsUncontrolled() then + self:StartUncontrolled() + end + + if airbaseDeploy then + + --- + -- Deploy at airbase + --- + + local cwp=self:GetWaypointCurrent() + local uid=cwp and cwp.uid or nil + + -- Get a (random) pre-defined transport path. + local path=self.cargoTransport:_GetPathTransport(self.category, self.cargoTZC) + + -- Get transport path. + if path then + + for i=1, #path.waypoints do + local wp=path.waypoints[i] + local coordinate=COORDINATE:NewFromWaypoint(wp) + local waypoint=FLIGHTGROUP.AddWaypoint(self, coordinate, nil, uid, nil, false) ; waypoint.temp=true + uid=waypoint.uid + if i==#path.waypoints then + waypoint.temp=false + waypoint.detour=1 --Needs to trigger the landatairbase function. + end + end + + else + + local coordinate=self:GetCoordinate():GetIntermediateCoordinate(Coordinate, 0.5) + + --coordinate:MarkToAll("Transport Inter Waypoint") + + -- If this is a helo and no ZONE_AIRBASE was given, we make the helo land in the pickup zone. + local waypoint=FLIGHTGROUP.AddWaypoint(self, coordinate, nil, uid, UTILS.MetersToFeet(self.altitudeCruise), true) ; waypoint.detour=1 + + end + + -- Order group to land at an airbase. + --self:__LandAtAirbase(-0.1, airbaseDeploy) + + elseif self.isHelo then + + --- + -- Helo can also land in a zone + --- + + -- If this is a helo and no ZONE_AIRBASE was given, we make the helo land in the pickup zone. + local waypoint=FLIGHTGROUP.AddWaypoint(self, Coordinate, nil, self:GetWaypointCurrent().uid, UTILS.MetersToFeet(self.altitudeCruise), false) ; waypoint.detour=1 + + else + self:T(self.lid.."ERROR: Aircraft (cargo carrier) cannot land in Deploy zone! Specify a ZONE_AIRBASE as deploy zone") + end + + -- Cancel landedAt task. This should trigger Cruise once airborne. + if self.isHelo and self:IsLandedAt() then + local Task=self:GetTaskCurrent() + if Task then + self:TaskCancel(Task) + else + self:T(self.lid.."ERROR: No current task but landed at?!") + end + end + + elseif self:IsArmygroup() then + + local cwp=self:GetWaypointCurrent() + local uid=cwp and cwp.uid or nil + + -- Get transport path. + local path=self.cargoTransport:_GetPathTransport(self.category, self.cargoTZC) + + -- Formation used for transporting. + local Formation=self.cargoTransport:_GetFormationTransport(self.cargoTZC) + + -- Get transport path. + if path then + for i=1,#path.waypoints do + local wp=path.waypoints[i] + local coordinate=COORDINATE:NewFromWaypoint(wp) + local waypoint=ARMYGROUP.AddWaypoint(self, coordinate, nil, uid, wp.action, false) ; waypoint.temp=true + uid=waypoint.uid + end + end + + -- ARMYGROUP + local waypoint=ARMYGROUP.AddWaypoint(self, Coordinate, nil, uid, Formation, false) ; waypoint.detour=1 + + -- Give cruise command. + self:Cruise() + + elseif self:IsNavygroup() then + + local cwp=self:GetWaypointCurrent() + local uid=cwp and cwp.uid or nil + + -- Get a (random) pre-defined transport path. + local path=self.cargoTransport:_GetPathTransport(self.category, self.cargoTZC) + + -- Get transport path. + if path then + for i=1,#path.waypoints do + local wp=path.waypoints[i] + local coordinate=COORDINATE:NewFromWaypoint(wp) + local waypoint=NAVYGROUP.AddWaypoint(self, coordinate, nil, uid, nil, false) ; waypoint.temp=true + uid=waypoint.uid + end + end + + -- NAVYGROUP + local waypoint=NAVYGROUP.AddWaypoint(self, Coordinate, nil, uid, self.altitudeCruise, false) ; waypoint.detour=1 + + -- Give cruise command. + self:Cruise() + + end + + end + +end + +--- On after "Unloading" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterUnloading(From, Event, To) + + -- Set carrier status to UNLOADING. + self:_NewCarrierStatus(OPSGROUP.CarrierStatus.UNLOADING) + + -- Deploy zone. + local zone=self.cargoTZC.DisembarkZone or self.cargoTZC.DeployZone --Core.Zone#ZONE + + for _,_cargo in pairs(self.cargoTZC.Cargos) do + local cargo=_cargo --#OPSGROUP.CargoGroup + + -- Check that cargo is loaded into this group. + -- NOTE: Could be that the element carriing this cargo group is DEAD, which would mean that the cargo group is also DEAD. + if cargo.opsgroup:IsLoaded(self.groupname) and not cargo.opsgroup:IsDead() then + + -- Disembark to carrier. + local needscarrier=false --#boolean + local carrier=nil --Ops.OpsGroup#OPSGROUP.Element + local carrierGroup=nil --Ops.OpsGroup#OPSGROUP + + -- Try to get the OPSGROUP if deploy zone is a ship. + if zone and zone:IsInstanceOf("ZONE_AIRBASE") and zone:GetAirbase():IsShip() then + local shipname=zone:GetAirbase():GetName() + local ship=UNIT:FindByName(shipname) + local group=ship:GetGroup() + carrierGroup=_DATABASE:GetOpsGroup(group:GetName()) + carrier=carrierGroup:GetElementByName(shipname) + end + + if self.cargoTZC.DisembarkCarriers and #self.cargoTZC.DisembarkCarriers>0 then + + needscarrier=true + + carrier, carrierGroup=self.cargoTransport:FindTransferCarrierForCargo(cargo.opsgroup, zone, self.cargoTZC) + + --TODO: max unloading time if transfer carrier does not arrive in the zone. + + end + + if needscarrier==false or (needscarrier and carrier and carrierGroup) then + + -- Cargo was delivered (somehow). + cargo.delivered=true + + -- Increase number of delivered cargos. + self.cargoTransport.Ndelivered=self.cargoTransport.Ndelivered+1 + + if carrier and carrierGroup then + + --- + -- Delivered to another carrier group. + --- + + self:_TransferCargo(cargo.opsgroup, carrierGroup, carrier) + + elseif zone and zone:IsInstanceOf("ZONE_AIRBASE") and zone:GetAirbase():IsShip() then + + --- + -- Delivered to a ship via helo or VTOL + --- + + -- Issue warning. + self:T(self.lid.."ERROR: Deploy/disembark zone is a ZONE_AIRBASE of a ship! Where to put the cargo? Dumping into the sea, sorry!") + + -- Unload but keep "in utero" (no coordinate provided). + self:Unload(cargo.opsgroup) + + else + + --- + -- Delivered to deploy zone + --- + + if self.cargoTransport:GetDisembarkInUtero(self.cargoTZC) then + + -- Unload but keep "in utero" (no coordinate provided). + self:Unload(cargo.opsgroup) + + else + + -- Get disembark zone of this TZC. + local DisembarkZone=self.cargoTransport:GetDisembarkZone(self.cargoTZC) + + local Coordinate=nil + + + if DisembarkZone then + + -- Random coordinate in disembark zone. + Coordinate=DisembarkZone:GetRandomCoordinate() + + else + + local element=cargo.opsgroup:_GetMyCarrierElement() + + if element then + + -- Get random point in disembark zone. + local zoneCarrier=self:GetElementZoneUnload(element.name) + + -- Random coordinate/heading in the zone. + Coordinate=zoneCarrier:GetRandomCoordinate() + + else + env.info(string.format("FF ERROR carrier element nil!")) + end + + end + + -- Random heading of the group. + local Heading=math.random(0,359) + + -- Activation on/off. + local activation=self.cargoTransport:GetDisembarkActivation(self.cargoTZC) + if cargo.disembarkActivation~=nil then + activation=cargo.disembarkActivation + end + + -- Unload to Coordinate. + self:Unload(cargo.opsgroup, Coordinate, activation, Heading) + + end + + -- Trigger "Unloaded" event for current cargo transport + self.cargoTransport:Unloaded(cargo.opsgroup, self) + + end + + else + self:T(self.lid.."Cargo needs carrier but no carrier is avaiable (yet)!") + end + + else + -- Not loaded or dead + end + + end -- loop over cargos + +end + +--- On before "Unload" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP OpsGroup The OPSGROUP loaded as cargo. +-- @param Core.Point#COORDINATE Coordinate Coordinate were the group is unloaded to. +-- @param #number Heading Heading of group. +function OPSGROUP:onbeforeUnload(From, Event, To, OpsGroup, Coordinate, Heading) + + -- Remove group from carrier bay. If group is not in cargo bay, function will return false and transition is denied. + local removed=self:_DelCargobay(OpsGroup) + + return removed +end + +--- On after "Unload" event. Carrier unloads a cargo group from its cargo bay. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP OpsGroup The OPSGROUP loaded as cargo. +-- @param Core.Point#COORDINATE Coordinate Coordinate were the group is unloaded to. +-- @param #boolean Activated If `true`, group is active. If `false`, group is spawned in late activated state. +-- @param #number Heading (Optional) Heading of group in degrees. Default is random heading for each unit. +function OPSGROUP:onafterUnload(From, Event, To, OpsGroup, Coordinate, Activated, Heading) + + -- New cargo status. + OpsGroup:_NewCargoStatus(OPSGROUP.CargoStatus.NOTCARGO) + + --TODO: Unload flightgroup. Find parking spot etc. + + if Coordinate then + + --- + -- Respawn at a coordinate. + --- + + -- Template for the respawned group. + local Template=UTILS.DeepCopy(OpsGroup.template) --DCS#Template + + -- No late activation. + if Activated==false then + Template.lateActivation=true + else + Template.lateActivation=false + end + + -- Loop over template units. + for _,Unit in pairs(Template.units) do + + local element=OpsGroup:GetElementByName(Unit.name) + + if element then + + local vec3=element.vec3 + + -- Relative pos vector. + local rvec2={x=Unit.x-Template.x, y=Unit.y-Template.y} --DCS#Vec2 + + local cvec2={x=Coordinate.x, y=Coordinate.z} --DCS#Vec2 + + -- Position. + Unit.x=cvec2.x+rvec2.x + Unit.y=cvec2.y+rvec2.y + Unit.alt=land.getHeight({x=Unit.x, y=Unit.y}) + + -- Heading. + Unit.heading=Heading and math.rad(Heading) or Unit.heading + Unit.psi=-Unit.heading + + end + + end + + -- Respawn group. + OpsGroup:_Respawn(0, Template) + + -- Add current waypoint. These have been cleard on loading. + if OpsGroup:IsNavygroup() then + OpsGroup:ClearWaypoints() + OpsGroup.currentwp=1 + OpsGroup.passedfinalwp=true + NAVYGROUP.AddWaypoint(OpsGroup, Coordinate, nil, nil, nil, false) + elseif OpsGroup:IsArmygroup() then + OpsGroup:ClearWaypoints() + OpsGroup.currentwp=1 + OpsGroup.passedfinalwp=true + ARMYGROUP.AddWaypoint(OpsGroup, Coordinate, nil, nil, nil, false) + end + + else + + --- + -- Just remove from this carrier. + --- + + -- Nothing to do. + + OpsGroup.position=self:GetVec3() + + end + + -- Trigger "Disembarked" event. + OpsGroup:Disembarked(OpsGroup:_GetMyCarrierGroup(), OpsGroup:_GetMyCarrierElement()) + + -- Trigger "Unloaded" event. + self:Unloaded(OpsGroup) + + -- Remove my carrier. + OpsGroup:_RemoveMyCarrier() + +end + +--- On after "Unloaded" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP OpsGroupCargo Cargo OPSGROUP that was unloaded from a carrier. +function OPSGROUP:onafterUnloaded(From, Event, To, OpsGroupCargo) + self:T(self.lid..string.format("Unloaded OPSGROUP %s", OpsGroupCargo:GetName())) + + if OpsGroupCargo.legion and OpsGroupCargo:IsInZone(OpsGroupCargo.legion.spawnzone) then + self:T(self.lid..string.format("Unloaded group %s returned to legion", OpsGroupCargo:GetName())) + OpsGroupCargo:Returned() + end + + -- Check if there is a paused mission. + local paused=OpsGroupCargo:_CountPausedMissions()>0 + + if paused then + OpsGroupCargo:UnpauseMission() + end + +end + + +--- On after "UnloadingDone" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterUnloadingDone(From, Event, To) + + -- Debug info + self:T(self.lid.."Cargo unloading done..") + + -- Cancel landedAt task. + if self:IsFlightgroup() and self:IsLandedAt() then + local Task=self:GetTaskCurrent() + self:__TaskCancel(5, Task) + end + + -- Check everything was delivered (or is dead). + local delivered=self:_CheckGoPickup(self.cargoTransport) + + if not delivered then + + -- Get new TZC. + self.cargoTZC=self.cargoTransport:_GetTransportZoneCombo(self) + + if self.cargoTZC then + + -- Pickup the next batch. + self:T(self.lid.."Unloaded: Still cargo left ==> Pickup") + self:Pickup() + + else + + -- Debug info. + self:T(self.lid..string.format("WARNING: Not all cargo was delivered but could not get a transport zone combo ==> setting carrier state to NOT CARRIER")) + + -- This is not a carrier anymore. + self:_NewCarrierStatus(OPSGROUP.CarrierStatus.NOTCARRIER) + + end + + else + + -- Everything delivered. + self:T(self.lid.."Unloaded: ALL cargo unloaded ==> Delivered (current)") + self:Delivered(self.cargoTransport) + + end + +end + +--- On after "Delivered" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsTransport#OPSTRANSPORT CargoTransport The cargo transport assignment. +function OPSGROUP:onafterDelivered(From, Event, To, CargoTransport) + + -- Check if this was the current transport. + if self.cargoTransport and self.cargoTransport.uid==CargoTransport.uid then + + -- Checks + if self:IsPickingup() then + -- Delete pickup waypoint? + local wpindex=self:GetWaypointIndexNext(false) + if wpindex then + self:RemoveWaypoint(wpindex) + end + -- Remove landing airbase. + self.isLandingAtAirbase=nil + elseif self:IsLoading() then + -- Nothing to do? + elseif self:IsTransporting() then + -- This should not happen. Carrier is transporting, how can the cargo be delivered? + elseif self:IsUnloading() then + -- Nothing to do? + end + + -- This is not a carrier anymore. + self:_NewCarrierStatus(OPSGROUP.CarrierStatus.NOTCARRIER) + + -- Startup uncontrolled aircraft to allow it to go back. + if self:IsFlightgroup() then + + local function atbase(_airbase) + local airbase=_airbase --Wrapper.Airbase#AIRBASE + if airbase and self.currbase then + if airbase.AirbaseName==self.currbase.AirbaseName then + return true + end + end + return false + end + + -- Check if uncontrolled and NOT at destination. If so, start up uncontrolled and let flight return to whereever it wants to go. + if self:IsUncontrolled() and not atbase(self.destbase) then + self:StartUncontrolled() + end + if self:IsLandedAt() then + local Task=self:GetTaskCurrent() + self:TaskCancel(Task) + end + else + -- Army & Navy: give Cruise command to "wake up" from waiting status. + self:__Cruise(0.1) + end + + -- Set carrier transport status. + self.cargoTransport:SetCarrierTransportStatus(self, OPSTRANSPORT.Status.DELIVERED) + + -- Check group done. + self:T(self.lid..string.format("All cargo of transport UID=%d delivered ==> check group done in 0.2 sec", self.cargoTransport.uid)) + self:_CheckGroupDone(0.2) + + + end + + -- Remove cargo transport from cargo queue. + --self:DelOpsTransport(CargoTransport) + +end + +--- On after "TransportCancel" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsTransport#OPSTRANSPORT The transport to be cancelled. +function OPSGROUP:onafterTransportCancel(From, Event, To, Transport) + + if self.cargoTransport and self.cargoTransport.uid==Transport.uid then + + --- + -- Current Transport + --- + + -- Debug info. + self:T(self.lid..string.format("Cancel current transport %d", Transport.uid)) + + -- Call delivered= + local calldelivered=false + + if self:IsPickingup() then + + -- On its way to the pickup zone. Remove waypoint. Will be done in delivered. + calldelivered=true + + elseif self:IsLoading() then + + -- Handle cargo groups. + local cargos=Transport:GetCargoOpsGroups(false) + + for _,_opsgroup in pairs(cargos) do + local opsgroup=_opsgroup --#OPSGROUP + + if opsgroup:IsBoarding(self.groupname) then + + -- Remove boarding waypoint. + opsgroup:RemoveWaypoint(self.currentwp+1) + + -- Remove from cargo bay (reserved), remove mycarrier, set cargo status. + self:_DelCargobay(opsgroup) + opsgroup:_RemoveMyCarrier() + opsgroup:_NewCargoStatus(OPSGROUP.CargoStatus.NOTCARGO) + + elseif opsgroup:IsLoaded(self.groupname) then + + -- Get random point in disembark zone. + local zoneCarrier=self:GetElementZoneUnload(opsgroup:_GetMyCarrierElement().name) + + -- Random coordinate/heading in the zone. + local Coordinate=zoneCarrier and zoneCarrier:GetRandomCoordinate() or self.cargoTransport:GetEmbarkZone(self.cargoTZC):GetRandomCoordinate() + + -- Random heading of the group. + local Heading=math.random(0,359) + + -- Unload to Coordinate. + self:Unload(opsgroup, Coordinate, self.cargoTransport:GetDisembarkActivation(self.cargoTZC), Heading) + + -- Trigger "Unloaded" event for current cargo transport + self.cargoTransport:Unloaded(opsgroup, self) + + end + + end + + -- Call delivered. + calldelivered=true + + elseif self:IsTransporting() then + + -- Well, we cannot just unload the cargo anywhere. + + -- TODO: Best would be to bring the cargo back to the pickup zone! + + elseif self:IsUnloading() then + -- Unloading anyway... delivered will be called when done. + else + + end + + -- Transport delivered. + if calldelivered then + self:__Delivered(-2, Transport) + end + + else + + --- + -- NOT the current transport + --- + + -- Set mission group status. + Transport:SetCarrierTransportStatus(self, AUFTRAG.GroupStatus.CANCELLED) + + -- Remove transport from queue. This also removes the carrier from the transport. + self:DelOpsTransport(Transport) + + -- Remove carrier. + --Transport:_DelCarrier(self) + + -- Send group RTB or WAIT if nothing left to do. + self:_CheckGroupDone(1) + + end + +end + + +--- +-- Cargo Group Functions +--- + +--- On before "Board" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP CarrierGroup The carrier group. +-- @param #OPSGROUP.Element Carrier The OPSGROUP element +function OPSGROUP:onbeforeBoard(From, Event, To, CarrierGroup, Carrier) + + if self:IsDead() then + self:T(self.lid.."Group DEAD ==> Deny Board transition!") + return false + elseif CarrierGroup:IsDead() then + self:T(self.lid.."Carrier Group DEAD ==> Deny Board transition!") + self:_NewCargoStatus(OPSGROUP.CargoStatus.NOTCARGO) + return false + elseif Carrier.status==OPSGROUP.ElementStatus.DEAD then + self:T(self.lid.."Carrier Element DEAD ==> Deny Board transition!") + self:_NewCargoStatus(OPSGROUP.CargoStatus.NOTCARGO) + return false + end + + return true +end + +--- On after "Board" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP CarrierGroup The carrier group. +-- @param #OPSGROUP.Element Carrier The OPSGROUP element +function OPSGROUP:onafterBoard(From, Event, To, CarrierGroup, Carrier) + + -- Army or Navy group. + local CarrierIsArmyOrNavy=CarrierGroup:IsArmygroup() or CarrierGroup:IsNavygroup() + local CargoIsArmyOrNavy=self:IsArmygroup() or self:IsNavygroup() + + -- Check that carrier is standing still. + --if (CarrierIsArmyOrNavy and (CarrierGroup:IsHolding() and CarrierGroup:GetVelocity(Carrier.name)<=1)) or (CarrierGroup:IsFlightgroup() and (CarrierGroup:IsParking() or CarrierGroup:IsLandedAt())) then + if (CarrierIsArmyOrNavy and (CarrierGroup:GetVelocity(Carrier.name)<=1)) or (CarrierGroup:IsFlightgroup() and (CarrierGroup:IsParking() or CarrierGroup:IsLandedAt())) then + + -- Board if group is mobile, not late activated and army or navy. Everything else is loaded directly. + local board=self.speedMax>0 and CargoIsArmyOrNavy and self:IsAlive() and CarrierGroup:IsAlive() + + -- Armygroup cannot board ship ==> Load directly. + if self:IsArmygroup() and CarrierGroup:IsNavygroup() then + board=false + end + + 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))) + + -- TODO: Implement embarkzone. + local Coordinate=Carrier.unit:GetCoordinate() + + -- Clear all waypoints. + self:ClearWaypoints(self.currentwp+1) + + if self.isArmygroup then + local waypoint=ARMYGROUP.AddWaypoint(self, Coordinate, nil, nil, ENUMS.Formation.Vehicle.Diamond) ; waypoint.detour=1 + self:Cruise() + else + local waypoint=NAVYGROUP.AddWaypoint(self, Coordinate) ; waypoint.detour=1 + self:Cruise() + end + + -- Set carrier. As long as the group is not loaded, we only reserve the cargo space. + CarrierGroup:_AddCargobay(self, Carrier, true) + + else + + --- + -- Direct load into carrier. + --- + + -- Debug info. + self:T(self.lid..string.format("Board [loaded=%s] with direct load to carrier group=%s, element=%s", tostring(self:IsLoaded()), CarrierGroup:GetName(), tostring(Carrier.name))) + + -- Get current carrier group. + local mycarriergroup=self:_GetMyCarrierGroup() + if mycarriergroup then + self:T(self.lid..string.format("Current carrier group %s", mycarriergroup:GetName())) + end + + -- Unload cargo first. + if mycarriergroup and mycarriergroup:GetName()~=CarrierGroup:GetName() then + -- TODO: Unload triggers other stuff like Disembarked. This can be a problem! + self:T(self.lid.."Unloading from mycarrier") + mycarriergroup:Unload(self) + end + + -- Trigger Load event. + CarrierGroup:Load(self) + + end + + else + + -- Redo boarding call. + self:T(self.lid.."Carrier not ready for boarding yet ==> repeating boarding call in 10 sec") + self:__Board(-10, CarrierGroup, Carrier) + + -- Set carrier. As long as the group is not loaded, we only reserve the cargo space.� + CarrierGroup:_AddCargobay(self, Carrier, true) + + end + + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -129847,37 +145190,37 @@ end function OPSGROUP:_CheckInZones() if self.checkzones and self:IsAlive() then - + local Ncheck=self.checkzones:Count() local Ninside=self.inzones:Count() - + -- Debug info. self:T(self.lid..string.format("Check if group is in %d zones. Currently it is in %d zones.", self.checkzones:Count(), self.inzones:Count())) -- Firstly, check if group is still inside zone it was already in. If not, remove zones and trigger LeaveZone() event. local leftzones={} for inzonename, inzone in pairs(self.inzones:GetSet()) do - + -- Check if group is still inside the zone. local isstillinzone=self.group:IsInZone(inzone) --:IsPartlyOrCompletelyInZone(inzone) - + -- If not, trigger, LeaveZone event. if not isstillinzone then table.insert(leftzones, inzone) - end + end end - + -- Trigger leave zone event. for _,leftzone in pairs(leftzones) do self:LeaveZone(leftzone) end - - + + -- Now, run of all check zones and see if the group entered a zone. local enterzones={} for checkzonename,_checkzone in pairs(self.checkzones:GetSet()) do local checkzone=_checkzone --Core.Zone#ZONE - + -- Is group currtently in this check zone? local isincheckzone=self.group:IsInZone(checkzone) --:IsPartlyOrCompletelyInZone(checkzone) @@ -129885,12 +145228,12 @@ function OPSGROUP:_CheckInZones() table.insert(enterzones, checkzone) end end - + -- Trigger enter zone event. for _,enterzone in pairs(enterzones) do self:EnterZone(enterzone) end - + end end @@ -129899,7 +145242,7 @@ end -- @param #OPSGROUP self function OPSGROUP:_CheckDetectedUnits() - if self.group and not self:IsDead() then + if self.detectionOn and self.group and not self:IsDead() then -- Get detected DCS units. local detectedtargets=self.group:GetDetectedTargets() @@ -129910,33 +145253,33 @@ function OPSGROUP:_CheckDetectedUnits() local DetectedObject=Detection.object -- DCS#Object if DetectedObject and DetectedObject:isExist() and DetectedObject.id_<50000000 then - + -- Unit. local unit=UNIT:Find(DetectedObject) - + if unit and unit:IsAlive() then - + -- Name of detected unit local unitname=unit:GetName() - -- Add unit to detected table of this run. + -- Add unit to detected table of this run. table.insert(detected, unit) - + -- Trigger detected unit event ==> This also triggers the DetectedUnitNew and DetectedUnitKnown events. self:DetectedUnit(unit) - + -- Get group of unit. local group=unit:GetGroup() - + -- Add group to table. - if group then - groups[group:GetName()]=group + if group then + groups[group:GetName()]=group end - + end end end - + -- Call detected group event. for groupname, group in pairs(groups) do self:DetectedGroup(group) @@ -129962,7 +145305,7 @@ function OPSGROUP:_CheckDetectedUnits() end end - + -- Remove lost units from detected set. self.detectedunits:RemoveUnitsByName(lost) @@ -129987,7 +145330,7 @@ function OPSGROUP:_CheckDetectedUnits() end end - + -- Remove lost units from detected set. self.detectedgroups:RemoveGroupsByName(lost) @@ -130000,126 +145343,205 @@ end -- @param #number delay Delay in seconds. function OPSGROUP:_CheckGroupDone(delay) + -- FSM state. + local fsmstate=self:GetState() + if self:IsAlive() and self.isAI then if delay and delay>0 then + -- Debug info. + self:T(self.lid..string.format("Check OPSGROUP [state=%s] done in %.3f seconds...", fsmstate, delay)) + -- Delayed call. self:ScheduleOnce(delay, self._CheckGroupDone, self) else - + + -- Debug info. + self:T(self.lid..string.format("Check OSGROUP [state=%s] done?", fsmstate)) + + -- Group is engaging something. if self:IsEngaging() then + self:T(self.lid.."Engaging! Group NOT done ==> UpdateRoute()") self:UpdateRoute() return end - + + -- Group is returning. + if self:IsReturning() then + self:T(self.lid.."Returning! Group NOT done...") + return + end + + -- Group is rearming. + if self:IsRearming() then + self:T(self.lid.."Rearming! Group NOT done...") + return + end + + -- Group is retreating. + if self:IsRetreating() then + self:T(self.lid.."Retreating! Group NOT done...") + return + end + + if self:IsBoarding() then + self:T(self.lid.."Boarding! Group NOT done...") + return + end + + -- Group is waiting. We deny all updates. + if self:IsWaiting() then + -- If group is waiting, we assume that is the way it is meant to be. + self:T(self.lid.."Waiting! Group NOT done...") + 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() + + -- 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 + + -- Number of remaining tasks/missions? + if nTasks>0 or nMissions>0 or nTransports>0 then + self:T(self.lid..string.format("Group still has tasks, missions or transports ==> NOT DONE")) + return + end + -- Get current waypoint. local waypoint=self:GetWaypoint(self.currentwp) - - --env.info("FF CheckGroupDone") if waypoint then - + -- Number of tasks remaining for this waypoint. local ntasks=self:CountTasksWaypoint(waypoint.uid) - + -- We only want to update the route if there are no more tasks to be done. if ntasks>0 then self:T(self.lid..string.format("Still got %d tasks for the current waypoint UID=%d ==> RETURN (no action)", ntasks, waypoint.uid)) return end - end - - if self.adinfinitum then + end + if self.adinfinitum then + --- -- Parol Ad Infinitum --- if #self.waypoints>0 then - + -- Next waypoint index. local i=self:GetWaypointIndexNext(true) - + -- Get positive speed to first waypoint. local speed=self:GetSpeedToWaypoint(i) - - -- Start route at first waypoint. - self:UpdateRoute(i, speed) - + + -- Cruise. + self:Cruise(speed) + + -- Debug info. self:T(self.lid..string.format("Adinfinitum=TRUE ==> Goto WP index=%d at speed=%d knots", i, speed)) - + else - self:E(self.lid..string.format("WARNING: No waypoints left! Commanding a Full Stop")) - self:__FullStop(-1) + self:T(self.lid..string.format("WARNING: No waypoints left! Commanding a Full Stop")) + self:__FullStop(-1) end else - + --- -- Finite Patrol --- - - if self.passedfinalwp then - + + if self:HasPassedFinalWaypoint() then + --- -- Passed FINAL waypoint --- - - -- No further waypoints. Command a full stop. - self:__FullStop(-1) - - self:T(self.lid..string.format("Passed final WP, adinfinitum=FALSE ==> Full Stop")) - + + if self.legion then + + self:T(self.lid..string.format("Passed final WP, adinfinitum=FALSE, LEGION set ==> RTZ")) + if self.isArmygroup then + self:RTZ(self.legion.spawnzone) + elseif self.isNavygroup then + self:RTZ(self.legion.portzone) + end + + else + + -- No further waypoints. Command a full stop. + self:__FullStop(-1) + + self:T(self.lid..string.format("Passed final WP, adinfinitum=FALSE ==> Full Stop")) + + end + else - + --- -- Final waypoint NOT passed yet --- - + if #self.waypoints>0 then self:T(self.lid..string.format("NOT Passed final WP, #WP>0 ==> Update Route")) - self:UpdateRoute() + self:Cruise() else - self:E(self.lid..string.format("WARNING: No waypoints left! Commanding a Full Stop")) + self:T(self.lid..string.format("WARNING: No waypoints left! Commanding a Full Stop")) self:__FullStop(-1) end - + end - + end - - end + + end end - + end --- Check if group got stuck. -- @param #OPSGROUP self function OPSGROUP:_CheckStuck() - -- Holding means we are not stuck. - if self:IsHolding() or self:Is("Rearming") then + -- Cases we are not stuck. + if self:IsHolding() or self:Is("Rearming") or self:IsWaiting() or self:HasPassedFinalWaypoint() then return end - + -- Current time. local Tnow=timer.getTime() - + -- Expected speed in m/s. local ExpectedSpeed=self:GetExpectedSpeed() - + -- Current speed in m/s. local speed=self:GetVelocity() - + -- Check speed. - if speed<0.5 then - + if speed<0.1 then + if ExpectedSpeed>0 and not self.stuckTimestamp then self:T2(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected", speed, ExpectedSpeed)) self.stuckTimestamp=Tnow self.stuckVec3=self:GetVec3() end - + else -- Moving (again). self.stuckTimestamp=nil @@ -130127,20 +145549,58 @@ function OPSGROUP:_CheckStuck() -- Somehow we are not moving... if self.stuckTimestamp then - + -- Time we are holding. local holdtime=Tnow-self.stuckTimestamp - - if holdtime>=10*60 then - - self:E(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected for %d sec", speed, ExpectedSpeed, holdtime)) - + + if holdtime>=5*60 and holdtime<10*60 then + + -- Debug warning. + self:T(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected for %d sec", speed, ExpectedSpeed, holdtime)) + + -- Check what is happening. + if self:IsEngaging() then + self:__Disengage(1) + elseif self:IsReturning() then + self:__RTZ(1) + else + self:__Cruise(1) + end + + elseif holdtime>=10*60 and holdtime<30*60 then + + -- Debug warning. + self:T(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected for %d sec", speed, ExpectedSpeed, holdtime)) + --TODO: Stuck event! - + -- Look for a current mission and cancel it as we do not seem to be able to perform it. + local mission=self:GetMissionCurrent() + if mission then + self:T(self.lid..string.format("WARNING: Cancelling mission %s [%s] due to being stuck", mission:GetName(), mission:GetType())) + self:MissionCancel(mission) + else + -- Give cruise command again. + if self:IsReturning() then + self:__RTZ(1) + else + self:__Cruise(1) + end + end + + elseif holdtime>=30*60 then + + -- Debug warning. + self:T(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected for %d sec", speed, ExpectedSpeed, holdtime)) + + if self.legion then + self:T(self.lid..string.format("Asset is returned to its legion after being stuck!")) + self:ReturnToLegion() + end + end - + end - + end @@ -130152,25 +145612,29 @@ function OPSGROUP:_CheckDamage() self.life=0 local damaged=false for _,_element in pairs(self.elements) do - local element=_element --Ops.OpsGroup#OPSGROUP - + local element=_element --Ops.OpsGroup#OPSGROUP.Element + + if element.status~=OPSGROUP.ElementStatus.DEAD and element.status~=OPSGROUP.ElementStatus.INUTERO then + -- Current life points. local life=element.unit:GetLife() - + self.life=self.life+life - + if life0 then - + -- Get current ammo. local ammo=self:GetAmmoTot() - + -- Check if rearming is completed. if self:IsRearming() then if ammo.Total==self.ammo.Total then self:Rearmed() end - end - + end + -- Total. if self.outofAmmo and ammo.Total>0 then self.outofAmmo=false @@ -130222,7 +145686,7 @@ function OPSGROUP:_CheckAmmoStatus() -- Guns. if self.outofGuns and ammo.Guns>0 then - self.outoffGuns=false + self.outofGuns=false end if ammo.Guns==0 and self.ammo.Guns>0 and not self.outofGuns then self.outofGuns=true @@ -130231,7 +145695,7 @@ function OPSGROUP:_CheckAmmoStatus() -- Rockets. if self.outofRockets and ammo.Rockets>0 then - self.outoffRockets=false + self.outofRockets=false end if ammo.Rockets==0 and self.ammo.Rockets>0 and not self.outofRockets then self.outofRockets=true @@ -130240,29 +145704,66 @@ function OPSGROUP:_CheckAmmoStatus() -- Bombs. if self.outofBombs and ammo.Bombs>0 then - self.outoffBombs=false + self.outofBombs=false end if ammo.Bombs==0 and self.ammo.Bombs>0 and not self.outofBombs then self.outofBombs=true self:OutOfBombs() end - -- Missiles. + -- Missiles (All). if self.outofMissiles and ammo.Missiles>0 then - self.outoffMissiles=false + self.outofMissiles=false end if ammo.Missiles==0 and self.ammo.Missiles>0 and not self.outofMissiles then self.outofMissiles=true self:OutOfMissiles() end - + + -- Missiles AA. + if self.outofMissilesAA and ammo.MissilesAA>0 then + self.outofMissilesAA=false + end + if ammo.MissilesAA==0 and self.ammo.MissilesAA>0 and not self.outofMissilesAA then + self.outofMissilesAA=true + self:OutOfMissilesAA() + end + + -- Missiles AG. + if self.outofMissilesAG and ammo.MissilesAG>0 then + self.outofMissilesAG=false + end + if ammo.MissilesAG==0 and self.ammo.MissilesAG>0 and not self.outofMissilesAG then + self.outofMissilesAG=true + self:OutOfMissilesAG() + end + + -- Missiles AS. + if self.outofMissilesAS and ammo.MissilesAS>0 then + self.outofMissilesAS=false + end + if ammo.MissilesAS==0 and self.ammo.MissilesAS>0 and not self.outofMissilesAS then + self.outofMissilesAS=true + self:OutOfMissilesAS() + end + + -- Torpedos. + if self.outofTorpedos and ammo.Torpedos>0 then + self.outofTorpedos=false + end + if ammo.Torpedos==0 and self.ammo.Torpedos>0 and not self.outofTorpedos then + self.outofTorpedos=true + self:OutOfTorpedos() + end + + -- Check if group is engaging. if self:IsEngaging() and ammo.Total==0 then self:Disengage() end end - + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -130276,9 +145777,9 @@ function OPSGROUP:_PrintTaskAndMissionStatus() --- -- Tasks: verbose >= 3 --- - + -- Task queue. - if self.verbose>=3 and #self.taskqueue>0 then + if self.verbose>=3 and #self.taskqueue>0 then local text=string.format("Tasks #%d", #self.taskqueue) for i,_task in pairs(self.taskqueue) do local task=_task --Ops.OpsGroup#OPSGROUP.Task @@ -130308,22 +145809,22 @@ function OPSGROUP:_PrintTaskAndMissionStatus() end self:I(self.lid..text) end - + --- -- Missions: verbose>=2 --- - + -- Current mission name. - if self.verbose>=2 then + if self.verbose>=2 then local Mission=self:GetMissionByID(self.currentmission) - + -- Current status. local text=string.format("Missions %d, Current: %s", self:CountRemainingMissison(), Mission and Mission.name or "none") for i,_mission in pairs(self.missionqueue) do local mission=_mission --Ops.Auftrag#AUFTRAG local Cstart= UTILS.SecondsToClock(mission.Tstart, true) local Cstop = mission.Tstop and UTILS.SecondsToClock(mission.Tstop, true) or "INF" - text=text..string.format("\n[%d] %s (%s) status=%s (%s), Time=%s-%s, prio=%d wp=%s targets=%d", + text=text..string.format("\n[%d] %s (%s) status=%s (%s), Time=%s-%s, prio=%d wp=%s targets=%d", i, tostring(mission.name), mission.type, mission:GetGroupStatus(self), tostring(mission.status), Cstart, Cstop, mission.prio, tostring(mission:GetGroupWaypointIndex(self)), mission:CountMissionTargets()) end self:I(self.lid..text) @@ -130335,32 +145836,63 @@ end -- Waypoints & Routing ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Simple task function. Can be used to call a function which has the warehouse and the executing group as parameters. +-- @param #OPSGROUP self +-- @param #string Function The name of the function to call passed as string. +-- @param #number uid Waypoint UID. +function OPSGROUP:_SimpleTaskFunction(Function, uid) + + -- Task script. + local DCSScript = {} + + --_DATABASE:FindOpsGroup(groupname) + + DCSScript[#DCSScript+1] = string.format('local mygroup = _DATABASE:FindOpsGroup(\"%s\") ', self.groupname) -- The group that executes the task function. Very handy with the "...". + DCSScript[#DCSScript+1] = string.format('%s(mygroup, %d)', Function, uid) -- Call the function, e.g. myfunction.(warehouse,mygroup) + + -- Create task. + local DCSTask=CONTROLLABLE.TaskWrappedAction(self, CONTROLLABLE.CommandDoScript(self, table.concat(DCSScript))) + + return DCSTask +end + --- Enhance waypoint table. -- @param #OPSGROUP self -- @param #OPSGROUP.Waypoint Waypoint data. -- @return #OPSGROUP.Waypoint Modified waypoint data. function OPSGROUP:_CreateWaypoint(waypoint) - + -- Set uid. waypoint.uid=self.wpcounter - + -- Waypoint has not been passed yet. waypoint.npassed=0 - + -- Coordinate. waypoint.coordinate=COORDINATE:New(waypoint.x, waypoint.alt, waypoint.y) -- Set waypoint name. - waypoint.name=string.format("Waypoint UID=%d", waypoint.uid) - + waypoint.name=string.format("Waypoint UID=%d", waypoint.uid) + -- Set types. waypoint.patrol=false waypoint.detour=false waypoint.astar=false + waypoint.temp=false + + -- Tasks of this waypoint + local taskswp={} + + -- At each waypoint report passing. + local TaskPassingWaypoint=self:_SimpleTaskFunction("OPSGROUP._PassingWaypoint", waypoint.uid) + table.insert(taskswp, TaskPassingWaypoint) + + -- Waypoint task combo. + waypoint.task=self.group:TaskCombo(taskswp) -- Increase UID counter. self.wpcounter=self.wpcounter+1 - + return waypoint end @@ -130377,59 +145909,115 @@ function OPSGROUP:_AddWaypoint(waypoint, wpnumber) table.insert(self.waypoints, wpnumber, waypoint) -- Debug info. - self:T(self.lid..string.format("Adding waypoint at index=%d id=%d", wpnumber, waypoint.uid)) - + self:T(self.lid..string.format("Adding waypoint at index=%d with UID=%d", wpnumber, waypoint.uid)) + -- Now we obviously did not pass the final waypoint. - self.passedfinalwp=false - - -- Switch to cruise mode. - if self:IsHolding() then - self:Cruise() + if self.currentwp and wpnumber>self.currentwp then + self:_PassedFinalWaypoint(false, string.format("_AddWaypoint: wpnumber/index %d>%d self.currentwp", wpnumber, self.currentwp)) end + end --- Initialize Mission Editor waypoints. -- @param #OPSGROUP self +-- @param #number WpIndexMin +-- @param #number WpIndexMax -- @return #OPSGROUP self -function OPSGROUP:InitWaypoints() +function OPSGROUP:_InitWaypoints(WpIndexMin, WpIndexMax) -- Template waypoints. - self.waypoints0=self.group:GetTemplateRoutePoints() + self.waypoints0=UTILS.DeepCopy(_DATABASE:GetGroupTemplate(self.groupname).route.points) --self.group:GetTemplateRoutePoints() - -- Waypoints + -- Waypoints empty! self.waypoints={} - - for index,wp in pairs(self.waypoints0) do - -- Coordinate of the waypoint. - local coordinate=COORDINATE:New(wp.x, wp.alt, wp.y) - + WpIndexMin=WpIndexMin or 1 + WpIndexMax=WpIndexMax or #self.waypoints0 + WpIndexMax=math.min(WpIndexMax, #self.waypoints0) --Ensure max is not out of bounce. + + --for index,wp in pairs(self.waypoints0) do + + for i=WpIndexMin,WpIndexMax do + + local wp=self.waypoints0[i] --DCS#Waypoint + + -- Coordinate of the waypoint. + local Coordinate=COORDINATE:NewFromWaypoint(wp) + -- Strange! wp.speed=wp.speed or 0 - + -- Speed at the waypoint. local speedknots=UTILS.MpsToKnots(wp.speed) - - if index==1 then + + -- Expected speed to the first waypoint. + if i<=2 then self.speedWp=wp.speed end - + + -- Speed in knots. + local Speed=UTILS.MpsToKnots(wp.speed) + -- Add waypoint. - self:AddWaypoint(coordinate, speedknots, index-1, nil, false) - + local Waypoint=nil + if self:IsFlightgroup() then + Waypoint=FLIGHTGROUP.AddWaypoint(self, Coordinate, Speed, nil, Altitude, false) + elseif self:IsArmygroup() then + Waypoint=ARMYGROUP.AddWaypoint(self, Coordinate, Speed, nil, wp.action, false) + elseif self:IsNavygroup() then + Waypoint=NAVYGROUP.AddWaypoint(self, Coordinate, Speed, nil, Depth, false) + end + + -- Get DCS waypoint tasks set in the ME. EXPERIMENTAL! + local DCStasks=wp.task and wp.task.params.tasks or nil + if DCStasks and self.useMEtasks then + for _,DCStask in pairs(DCStasks) do + -- Wrapped Actions are commands. We do not take those. + if DCStask.id and DCStask.id~="WrappedAction" then + self:AddTaskWaypoint(DCStask,Waypoint, "ME Task") + end + end + end + end - + -- Debug info. self:T(self.lid..string.format("Initializing %d waypoints", #self.waypoints)) - + + -- Flight group specific. + if self:IsFlightgroup() then + + -- Get home and destination airbases from waypoints. + self.homebase=self.homebase or self:GetHomebaseFromWaypoints() + local destbase=self:GetDestinationFromWaypoints() + self.destbase=self.destbase or destbase + self.currbase=self:GetHomebaseFromWaypoints() + + --env.info("FF home base "..(self.homebase and self.homebase:GetName() or "unknown")) + --env.info("FF dest base "..(self.destbase and self.destbase:GetName() or "unknown")) + + -- Remove the landing waypoint. We use RTB for that. It makes adding new waypoints easier as we do not have to check if the last waypoint is the landing waypoint. + if destbase and #self.waypoints>1 then + table.remove(self.waypoints, #self.waypoints) + end + + -- Set destination to homebase. + if self.destbase==nil then + self.destbase=self.homebase + end + + end + -- Update route. if #self.waypoints>0 then - + -- Check if only 1 wp? if #self.waypoints==1 then - self.passedfinalwp=true + self:_PassedFinalWaypoint(true, "_InitWaypoints: #self.waypoints==1") end - + + else + self:T(self.lid.."WARNING: No waypoints initialized. Number of waypoints is 0!") end return self @@ -130447,29 +146035,27 @@ function OPSGROUP:Route(waypoints, delay) else if self:IsAlive() then - - -- DCS task combo. - local Tasks={} - - -- Route (Mission) task. - local TaskRoute=self.group:TaskRoute(waypoints) - table.insert(Tasks, TaskRoute) - - -- TaskCombo of enroute and mission tasks. - local TaskCombo=self.group:TaskCombo(Tasks) - - -- Set tasks. - if #Tasks>1 then - self:SetTask(TaskCombo) - else - self:SetTask(TaskRoute) - end - + + -- Clear all DCS tasks. NOTE: This can make DCS crash! + --self:ClearTasks() + + -- DCS mission task. + local DCSTask = { + id = 'Mission', + params = { + airborne = self:IsFlightgroup(), + route={points=waypoints}, + }, + } + + -- Set mission task. + self:SetTask(DCSTask) + else - self:E(self.lid.."ERROR: Group is not alive! Cannot route group.") + self:T(self.lid.."ERROR: Group is not alive! Cannot route group.") end end - + return self end @@ -130484,25 +146070,25 @@ function OPSGROUP:_UpdateWaypointTasks(n) local nwaypoints=#waypoints for i,_wp in pairs(waypoints) do - local wp=_wp --Ops.OpsGroup#OPSGROUP.Waypoint - + local wp=_wp --Ops.OpsGroup#OPSGROUP.Waypoint + if i>=n or nwaypoints==1 then - + -- Debug info. self:T2(self.lid..string.format("Updating waypoint task for waypoint %d/%d ID=%d. Last waypoint passed %d", i, nwaypoints, wp.uid, self.currentwp)) - + -- Tasks of this waypoint local taskswp={} - + -- At each waypoint report passing. local TaskPassingWaypoint=self.group:TaskFunction("OPSGROUP._PassingWaypoint", self, wp.uid) - table.insert(taskswp, TaskPassingWaypoint) - + table.insert(taskswp, TaskPassingWaypoint) + -- Waypoint task combo. wp.task=self.group:TaskCombo(taskswp) - + end - + end end @@ -130512,93 +146098,222 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Function called when a group is passing a waypoint. ---@param Wrapper.Group#GROUP group Group that passed the waypoint. --@param #OPSGROUP opsgroup Ops group object. --@param #number uid Waypoint UID. -function OPSGROUP._PassingWaypoint(group, opsgroup, uid) - +function OPSGROUP._PassingWaypoint(opsgroup, uid) + + -- Debug message. + local text=string.format("Group passing waypoint uid=%d", uid) + opsgroup:T(opsgroup.lid..text) + -- Get waypoint data. local waypoint=opsgroup:GetWaypointByID(uid) - + if waypoint then - + + -- Increase passing counter. + waypoint.npassed=waypoint.npassed+1 + -- Current wp. local currentwp=opsgroup.currentwp - + -- Get the current waypoint index. opsgroup.currentwp=opsgroup:GetWaypointIndex(uid) - - -- Set expected speed and formation from the next WP. - local wpnext=opsgroup:GetWaypointNext() - if wpnext then - + + local wpistemp=waypoint.temp or waypoint.detour or waypoint.astar + + -- Remove temp waypoints. + if wpistemp then + opsgroup:RemoveWaypointByID(uid) + end + + -- Get next waypoint. Tricky part is that if + local wpnext=opsgroup:GetWaypointNext() + + if wpnext then --and (opsgroup.currentwp<#opsgroup.waypoints or opsgroup.adinfinitum or wpistemp) + + -- Debug info. + opsgroup:T(opsgroup.lid..string.format("Next waypoint UID=%d index=%d", wpnext.uid, opsgroup:GetWaypointIndex(wpnext.uid))) + -- Set formation. - if opsgroup.isGround then + if opsgroup.isArmygroup then opsgroup.formation=wpnext.action end - - -- Set speed. + + -- Set speed to next wp. opsgroup.speed=wpnext.speed - + + if opsgroup.speed<0.01 then + opsgroup.speed=UTILS.KmphToMps(opsgroup.speedCruise) + end + + else + + -- Set passed final waypoint. + opsgroup:_PassedFinalWaypoint(true, "_PassingWaypoint No next Waypoint found") + end - - -- Debug message. - local text=string.format("Group passing waypoint uid=%d", uid) - opsgroup:T(opsgroup.lid..text) - + + -- Check if final waypoint was reached. + if opsgroup.currentwp==#opsgroup.waypoints and not (opsgroup.adinfinitum or wpistemp) then + -- Set passed final waypoint. + opsgroup:_PassedFinalWaypoint(true, "_PassingWaypoint currentwp==#waypoints and NOT adinfinitum and NOT a temporary waypoint") + end + -- Trigger PassingWaypoint event. - if waypoint.astar then - - -- Remove Astar waypoint. - opsgroup:RemoveWaypointByID(uid) - + if waypoint.temp then + + --- + -- Temporary Waypoint + --- + + if (opsgroup:IsNavygroup() or opsgroup:IsArmygroup()) and opsgroup.currentwp==#opsgroup.waypoints then + --TODO: not sure if this works with FLIGHTGROUPS + + -- Removing this for now. + opsgroup:Cruise() + end + + elseif waypoint.astar then + + --- + -- Pathfinding Waypoint + --- + -- Cruise. opsgroup:Cruise() - + elseif waypoint.detour then - - -- Remove detour waypoint. - opsgroup:RemoveWaypointByID(uid) - + + --- + -- Detour Waypoint + --- + if opsgroup:IsRearming() then - + -- Trigger Rearming event. opsgroup:Rearming() - + elseif opsgroup:IsRetreating() then - + -- Trigger Retreated event. opsgroup:Retreated() - + + elseif opsgroup:IsReturning() then + + -- Trigger Returned event. + opsgroup:Returned() + + elseif opsgroup:IsPickingup() then + + if opsgroup:IsFlightgroup() then + + -- Land at current pos and wait for 60 min max. + if opsgroup.cargoTZC then + + if opsgroup.cargoTZC.PickupAirbase then + -- Pickup airbase specified. Land there. + opsgroup:LandAtAirbase(opsgroup.cargoTZC.PickupAirbase) + else + -- Land somewhere in the pickup zone. Only helos can do that. + local coordinate=opsgroup.cargoTZC.PickupZone:GetRandomCoordinate(nil, nil, {land.SurfaceType.LAND}) + opsgroup:LandAt(coordinate, 60*60) + end + + else + local coordinate=opsgroup:GetCoordinate() + opsgroup:LandAt(coordinate, 60*60) + end + + + else + + -- Wait and load cargo. + opsgroup:FullStop() + opsgroup:__Loading(-5) + + end + + elseif opsgroup:IsTransporting() then + + if opsgroup:IsFlightgroup() then + + -- Land at current pos and wait for 60 min max. + if opsgroup.cargoTZC then + + if opsgroup.cargoTZC.DeployAirbase then + -- Deploy airbase specified. Land there. + opsgroup:LandAtAirbase(opsgroup.cargoTZC.DeployAirbase) + else + -- Land somewhere in the pickup zone. Only helos can do that. + local coordinate=opsgroup.cargoTZC.DeployZone:GetRandomCoordinate(nil, nil, {land.SurfaceType.LAND}) + opsgroup:LandAt(coordinate, 60*60) + end + + else + local coordinate=opsgroup:GetCoordinate() + opsgroup:LandAt(coordinate, 60*60) + end + + + else + -- Stop and unload. + opsgroup:FullStop() + opsgroup:Unloading() + end + + elseif opsgroup:IsBoarding() then + + local carrierGroup=opsgroup:_GetMyCarrierGroup() + local carrier=opsgroup:_GetMyCarrierElement() + + if carrierGroup and carrierGroup:IsAlive() then + + if carrier and carrier.unit and carrier.unit:IsAlive() then + + -- Load group into the carrier. + carrierGroup:Load(opsgroup) + + else + opsgroup:E(opsgroup.lid.."ERROR: Group cannot board assigned carrier UNIT as it is NOT alive!") + end + + else + opsgroup:E(opsgroup.lid.."ERROR: Group cannot board assigned carrier GROUP as it is NOT alive!") + end + elseif opsgroup:IsEngaging() then - + -- Nothing to do really. - + opsgroup:T(opsgroup.lid.."Passing engaging waypoint") + else - + -- Trigger DetourReached event. opsgroup:DetourReached() - + if waypoint.detour==0 then opsgroup:FullStop() elseif waypoint.detour==1 then opsgroup:Cruise() else opsgroup:E("ERROR: waypoint.detour should be 0 or 1") + opsgroup:FullStop() end - + end - + else + --- + -- Normal Route Waypoint + --- + -- Check if the group is still pathfinding. if opsgroup.ispathfinding then opsgroup.ispathfinding=false - end + end - -- Increase passing counter. - waypoint.npassed=waypoint.npassed+1 - -- Call event function. opsgroup:PassingWaypoint(waypoint) end @@ -130631,7 +146346,7 @@ function OPSGROUP._TaskDone(group, opsgroup, task) -- Debug message. local text=string.format("_TaskDone %s", task.description) - opsgroup:T3(opsgroup.lid..text) + opsgroup:T(opsgroup.lid..text) -- Set current task to nil so that the next in line can be executed. if opsgroup then @@ -130657,25 +146372,25 @@ end -- @param #string roe ROE of group. Default is value set in `SetDefaultROE` (usually `ENUMS.ROE.ReturnFire`). -- @return #OPSGROUP self function OPSGROUP:SwitchROE(roe) - + if self:IsAlive() or self:IsInUtero() then self.option.ROE=roe or self.optionDefault.ROE - + if self:IsInUtero() then self:T2(self.lid..string.format("Setting current ROE=%d when GROUP is SPAWNED", self.option.ROE)) else - + self.group:OptionROE(self.option.ROE) - + self:T(self.lid..string.format("Setting current ROE=%d (%s)", self.option.ROE, self:_GetROEName(self.option.ROE))) end - - + + else - self:E(self.lid.."WARNING: Cannot switch ROE! Group is not alive") + self:T(self.lid.."WARNING: Cannot switch ROE! Group is not alive") end - + return self end @@ -130719,25 +146434,30 @@ end -- @param #string rot ROT of group. Default is value set in `:SetDefaultROT` (usually `ENUMS.ROT.PassiveDefense`). -- @return #OPSGROUP self function OPSGROUP:SwitchROT(rot) - - if self:IsAlive() or self:IsInUtero() then - - self.option.ROT=rot or self.optionDefault.ROT - - if self:IsInUtero() then - self:T2(self.lid..string.format("Setting current ROT=%d when GROUP is SPAWNED", self.option.ROT)) - else - - self.group:OptionROT(self.option.ROT) - - self:T(self.lid..string.format("Setting current ROT=%d (0=NoReaction, 1=Passive, 2=Evade, 3=ByPass, 4=AllowAbort)", self.option.ROT)) - end - - else - self:E(self.lid.."WARNING: Cannot switch ROT! Group is not alive") + if self:IsFlightgroup() then + + if self:IsAlive() or self:IsInUtero() then + + self.option.ROT=rot or self.optionDefault.ROT + + if self:IsInUtero() then + self:T2(self.lid..string.format("Setting current ROT=%d when GROUP is SPAWNED", self.option.ROT)) + else + + self.group:OptionROT(self.option.ROT) + + -- Debug info. + self:T(self.lid..string.format("Setting current ROT=%d (0=NoReaction, 1=Passive, 2=Evade, 3=ByPass, 4=AllowAbort)", self.option.ROT)) + end + + + else + self:T(self.lid.."WARNING: Cannot switch ROT! Group is not alive") + end + end - + return self end @@ -130759,26 +146479,26 @@ function OPSGROUP:SetDefaultAlarmstate(alarmstate) end --- Set current Alarm State of the group. --- +-- -- * 0 = "Auto" -- * 1 = "Green" -- * 2 = "Red" --- +-- -- @param #OPSGROUP self -- @param #number alarmstate Alarm state of group. Default is 0="Auto". -- @return #OPSGROUP self function OPSGROUP:SwitchAlarmstate(alarmstate) - + if self:IsAlive() or self:IsInUtero() then - + if self.isArmygroup or self.isNavygroup then - + self.option.Alarm=alarmstate or self.optionDefault.Alarm - + if self:IsInUtero() then self:T2(self.lid..string.format("Setting current Alarm State=%d when GROUP is SPAWNED", self.option.Alarm)) else - + if self.option.Alarm==0 then self.group:OptionAlarmStateAuto() elseif self.option.Alarm==1 then @@ -130786,20 +146506,20 @@ function OPSGROUP:SwitchAlarmstate(alarmstate) elseif self.option.Alarm==2 then self.group:OptionAlarmStateRed() else - self:E("ERROR: Unknown Alarm State! Setting to AUTO") + self:T("ERROR: Unknown Alarm State! Setting to AUTO") self.group:OptionAlarmStateAuto() self.option.Alarm=0 end - + self:T(self.lid..string.format("Setting current Alarm State=%d (0=Auto, 1=Green, 2=Red)", self.option.Alarm)) - + end - + end else - self:E(self.lid.."WARNING: Cannot switch Alarm State! Group is not alive.") + self:T(self.lid.."WARNING: Cannot switch Alarm State! Group is not alive.") end - + return self end @@ -130810,6 +146530,217 @@ function OPSGROUP:GetAlarmstate() return self.option.Alarm or self.optionDefault.Alarm end +--- Set the default EPLRS for the group. +-- @param #OPSGROUP self +-- @param #boolean OnOffSwitch If `true`, EPLRS is on by default. If `false` default EPLRS setting is off. If `nil`, default is on if group has EPLRS and off if it does not have a datalink. +-- @return #OPSGROUP self +function OPSGROUP:SetDefaultEPLRS(OnOffSwitch) + + if OnOffSwitch==nil then + self.optionDefault.EPLRS=self.isEPLRS + else + self.optionDefault.EPLRS=OnOffSwitch + end + + return self +end + +--- Switch EPLRS datalink on or off. +-- @param #OPSGROUP self +-- @param #boolean OnOffSwitch If `true` or `nil`, switch EPLRS on. If `false` EPLRS switched off. +-- @return #OPSGROUP self +function OPSGROUP:SwitchEPLRS(OnOffSwitch) + + if self:IsAlive() or self:IsInUtero() then + + if OnOffSwitch==nil then + + self.option.EPLRS=self.optionDefault.EPLRS + + else + + self.option.EPLRS=OnOffSwitch + + end + + if self:IsInUtero() then + self:T2(self.lid..string.format("Setting current EPLRS=%s when GROUP is SPAWNED", tostring(self.option.EPLRS))) + else + + self.group:CommandEPLRS(self.option.EPLRS) + self:T(self.lid..string.format("Setting current EPLRS=%s", tostring(self.option.EPLRS))) + + end + else + self:E(self.lid.."WARNING: Cannot switch EPLRS! Group is not alive") + end + + return self +end + +--- Get current EPLRS state. +-- @param #OPSGROUP self +-- @return #boolean If `true`, EPLRS is on. +function OPSGROUP:GetEPLRS() + return self.option.EPLRS or self.optionDefault.EPLRS +end + +--- Set the default EPLRS for the group. +-- @param #OPSGROUP self +-- @param #boolean OnOffSwitch If `true`, EPLRS is on by default. If `false` default EPLRS setting is off. If `nil`, default is on if group has EPLRS and off if it does not have a datalink. +-- @return #OPSGROUP self +function OPSGROUP:SetDefaultEmission(OnOffSwitch) + + if OnOffSwitch==nil then + self.optionDefault.Emission=true + else + self.optionDefault.EPLRS=OnOffSwitch + end + + return self +end + +--- Switch emission on or off. +-- @param #OPSGROUP self +-- @param #boolean OnOffSwitch If `true` or `nil`, switch emission on. If `false` emission switched off. +-- @return #OPSGROUP self +function OPSGROUP:SwitchEmission(OnOffSwitch) + + if self:IsAlive() or self:IsInUtero() then + + if OnOffSwitch==nil then + + self.option.Emission=self.optionDefault.Emission + + else + + self.option.Emission=OnOffSwitch + + end + + if self:IsInUtero() then + self:T2(self.lid..string.format("Setting current EMISSION=%s when GROUP is SPAWNED", tostring(self.option.Emission))) + else + + self.group:EnableEmission(self.option.Emission) + self:T(self.lid..string.format("Setting current EMISSION=%s", tostring(self.option.Emission))) + + end + else + self:E(self.lid.."WARNING: Cannot switch Emission! Group is not alive") + end + + return self +end + +--- Get current emission state. +-- @param #OPSGROUP self +-- @return #boolean If `true`, emission is on. +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 +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + --- Set default TACAN parameters. -- @param #OPSGROUP self -- @param #number Channel TACAN channel. Default is 74. @@ -130819,20 +146750,20 @@ end -- @param #boolean OffSwitch If true, TACAN is off by default. -- @return #OPSGROUP self function OPSGROUP:SetDefaultTACAN(Channel, Morse, UnitName, Band, OffSwitch) - + self.tacanDefault={} self.tacanDefault.Channel=Channel or 74 self.tacanDefault.Morse=Morse or "XXX" self.tacanDefault.BeaconName=UnitName - if self.isAircraft then + if self:IsFlightgroup() then Band=Band or "Y" else Band=Band or "X" end - self.tacanDefault.Band=Band - - + self.tacanDefault.Band=Band + + if OffSwitch then self.tacanDefault.On=false else @@ -130850,19 +146781,19 @@ end function OPSGROUP:_SwitchTACAN(Tacan) if Tacan then - + self:SwitchTACAN(Tacan.Channel, Tacan.Morse, Tacan.BeaconName, Tacan.Band) - + else - + if self.tacanDefault.On then self:SwitchTACAN() else self:TurnOffTACAN() end - + end - + end --- Activate/switch TACAN beacon settings. @@ -130875,12 +146806,12 @@ end function OPSGROUP:SwitchTACAN(Channel, Morse, UnitName, Band) if self:IsInUtero() then - + self:T(self.lid..string.format("Switching TACAN to DEFAULT when group is spawned")) self:SetDefaultTACAN(Channel, Morse, UnitName, Band) elseif self:IsAlive() then - + Channel=Channel or self.tacanDefault.Channel Morse=Morse or self.tacanDefault.Morse Band=Band or self.tacanDefault.Band @@ -130899,7 +146830,7 @@ function OPSGROUP:SwitchTACAN(Channel, Morse, UnitName, Band) self:T(self.lid.."WARNING: Could not get TACAN unit. Trying first unit in the group") unit=self:GetUnit(1) end - + if unit and unit:IsAlive() then -- Unit ID. @@ -130907,16 +146838,16 @@ function OPSGROUP:SwitchTACAN(Channel, Morse, UnitName, Band) -- Type local Type=BEACON.Type.TACAN - + -- System - local System=BEACON.System.TACAN - if self.isAircraft then + local System=BEACON.System.TACAN + if self:IsFlightgroup() then System=BEACON.System.TACAN_TANKER_Y end - + -- Tacan frequency. local Frequency=UTILS.TACANToFrequency(Channel, Band) - + -- Activate beacon. unit:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, Band, true, Morse, true) @@ -130927,16 +146858,16 @@ function OPSGROUP:SwitchTACAN(Channel, Morse, UnitName, Band) self.tacan.BeaconName=unit:GetName() self.tacan.BeaconUnit=unit self.tacan.On=true - - -- Debug info. + + -- Debug info. self:T(self.lid..string.format("Switching TACAN to Channel %d%s Morse %s on unit %s", self.tacan.Channel, self.tacan.Band, tostring(self.tacan.Morse), self.tacan.BeaconName)) - + else - self:E(self.lid.."ERROR: Cound not set TACAN! Unit is not alive") + self:T(self.lid.."ERROR: Cound not set TACAN! Unit is not alive") end else - self:E(self.lid.."ERROR: Cound not set TACAN! Group is not alive and not in utero any more") + self:T(self.lid.."ERROR: Cound not set TACAN! Group is not alive and not in utero any more") end return self @@ -130967,6 +146898,12 @@ function OPSGROUP:GetTACAN() return self.tacan.Channel, self.tacan.Morse, self.tacan.Band, self.tacan.On, self.tacan.BeaconName end +--- Get current TACAN parameters. +-- @param #OPSGROUP self +-- @return #OPSGROUP.Beacon TACAN beacon. +function OPSGROUP:GetBeaconTACAN() + return self.tacan +end --- Set default ICLS parameters. @@ -130977,12 +146914,12 @@ end -- @param #boolean OffSwitch If true, TACAN is off by default. -- @return #OPSGROUP self function OPSGROUP:SetDefaultICLS(Channel, Morse, UnitName, OffSwitch) - + self.iclsDefault={} self.iclsDefault.Channel=Channel or 1 self.iclsDefault.Morse=Morse or "XXX" self.iclsDefault.BeaconName=UnitName - + if OffSwitch then self.iclsDefault.On=false else @@ -131000,17 +146937,17 @@ end function OPSGROUP:_SwitchICLS(Icls) if Icls then - + self:SwitchICLS(Icls.Channel, Icls.Morse, Icls.BeaconName) - + else - + if self.iclsDefault.On then self:SwitchICLS() else self:TurnOffICLS() end - + end end @@ -131024,17 +146961,17 @@ end function OPSGROUP:SwitchICLS(Channel, Morse, UnitName) if self:IsInUtero() then - + self:SetDefaultICLS(Channel,Morse,UnitName) - + self:T2(self.lid..string.format("Switching ICLS to Channel %d Morse %s on unit %s when GROUP is SPAWNED", self.iclsDefault.Channel, tostring(self.iclsDefault.Morse), tostring(self.iclsDefault.BeaconName))) elseif self:IsAlive() then - + Channel=Channel or self.iclsDefault.Channel Morse=Morse or self.iclsDefault.Morse local unit=self:GetUnit(1) --Wrapper.Unit#UNIT - + if UnitName then if type(UnitName)=="number" then unit=self:GetUnit(UnitName) @@ -131042,7 +146979,7 @@ function OPSGROUP:SwitchICLS(Channel, Morse, UnitName) unit=UNIT:FindByName(UnitName) end end - + if not unit then self:T(self.lid.."WARNING: Could not get ICLS unit. Trying first unit in the group") unit=self:GetUnit(1) @@ -131051,11 +146988,11 @@ function OPSGROUP:SwitchICLS(Channel, Morse, UnitName) if unit and unit:IsAlive() then -- Unit ID. - local UnitID=unit:GetID() + local UnitID=unit:GetID() -- Activate beacon. unit:CommandActivateICLS(Channel, UnitID, Morse) - + -- Update info. self.icls.Channel=Channel self.icls.Morse=Morse @@ -131063,12 +147000,12 @@ function OPSGROUP:SwitchICLS(Channel, Morse, UnitName) self.icls.BeaconName=unit:GetName() self.icls.BeaconUnit=unit self.icls.On=true - + -- Debug info. self:T(self.lid..string.format("Switching ICLS to Channel %d Morse %s on unit %s", self.icls.Channel, tostring(self.icls.Morse), self.icls.BeaconName)) - + else - self:E(self.lid.."ERROR: Cound not set ICLS! Unit is not alive.") + self:T(self.lid.."ERROR: Cound not set ICLS! Unit is not alive.") end end @@ -131098,7 +147035,7 @@ end -- @param #boolean OffSwitch If true, radio is OFF by default. -- @return #OPSGROUP self function OPSGROUP:SetDefaultRadio(Frequency, Modulation, OffSwitch) - + self.radioDefault={} self.radioDefault.Freq=Frequency or 251 self.radioDefault.Modu=Modulation or radio.modulation.AM @@ -131107,7 +147044,7 @@ function OPSGROUP:SetDefaultRadio(Frequency, Modulation, OffSwitch) else self.radioDefault.On=true end - + return self end @@ -131128,35 +147065,35 @@ end function OPSGROUP:SwitchRadio(Frequency, Modulation) if self:IsInUtero() then - + -- Set default radio. self:SetDefaultRadio(Frequency, Modulation) - + -- Debug info. self:T2(self.lid..string.format("Switching radio to frequency %.3f MHz %s when GROUP is SPAWNED", self.radioDefault.Freq, UTILS.GetModulationName(self.radioDefault.Modu))) - + elseif self:IsAlive() then - + Frequency=Frequency or self.radioDefault.Freq Modulation=Modulation or self.radioDefault.Modu - if self.isAircraft and not self.radio.On then + if self:IsFlightgroup() and not self.radio.On then self.group:SetOption(AI.Option.Air.id.SILENCE, false) - end - + end + -- Give command self.group:CommandSetFrequency(Frequency, Modulation) - + -- Update current settings. self.radio.Freq=Frequency - self.radio.Modu=Modulation + self.radio.Modu=Modulation self.radio.On=true - + -- Debug info. self:T(self.lid..string.format("Switching radio to frequency %.3f MHz %s", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu))) - + else - self:E(self.lid.."ERROR: Cound not set Radio! Group is not alive or not in utero any more") + self:T(self.lid.."ERROR: Cound not set Radio! Group is not alive or not in utero any more") end return self @@ -131169,17 +147106,17 @@ function OPSGROUP:TurnOffRadio() if self:IsAlive() then - if self.isAircraft then - + if self:IsFlightgroup() then + -- Set group to be silient. self.group:SetOption(AI.Option.Air.id.SILENCE, true) - + -- Radio is off. self.radio.On=false - + self:T(self.lid..string.format("Switching radio OFF")) else - self:E(self.lid.."ERROR: Radio can only be turned off for aircraft!") + self:T(self.lid.."ERROR: Radio can only be turned off for aircraft!") end end @@ -131194,7 +147131,7 @@ end -- @param #number Formation The formation the groups flies in. -- @return #OPSGROUP self function OPSGROUP:SetDefaultFormation(Formation) - + self.optionDefault.Formation=Formation return self @@ -131207,27 +147144,27 @@ end function OPSGROUP:SwitchFormation(Formation) if self:IsAlive() then - + Formation=Formation or self.optionDefault.Formation - - if self.isAircraft then + + if self:IsFlightgroup() then self.group:SetOption(AI.Option.Air.id.FORMATION, Formation) - - elseif self.isGround then - + + elseif self.isArmygroup then + -- Polymorphic and overwritten in ARMYGROUP. - + else - self:E(self.lid.."ERROR: Formation can only be set for aircraft or ground units!") + self:T(self.lid.."ERROR: Formation can only be set for aircraft or ground units!") return self end - + -- Set current formation. self.option.Formation=Formation - + -- Debug info. - self:T(self.lid..string.format("Switching formation to %d", self.option.Formation)) + self:T(self.lid..string.format("Switching formation to %s", tostring(self.option.Formation))) end @@ -131239,13 +147176,16 @@ end --- Set default callsign. -- @param #OPSGROUP self -- @param #number CallsignName Callsign name. --- @param #number CallsignNumber Callsign number. +-- @param #number CallsignNumber Callsign number. Default 1. -- @return #OPSGROUP self function OPSGROUP:SetDefaultCallsign(CallsignName, CallsignNumber) - self.callsignDefault={} + self.callsignDefault={} --#OPSGROUP.Callsign 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 @@ -131258,9 +147198,10 @@ end function OPSGROUP:SwitchCallsign(CallsignName, CallsignNumber) if self:IsInUtero() then - + -- Set default callsign. We switch to this when group is spawned. self:SetDefaultCallsign(CallsignName, CallsignNumber) + --self.callsign=UTILS.DeepCopy(self.callsignDefault) elseif self:IsAlive() then @@ -131273,17 +147214,46 @@ function OPSGROUP:SwitchCallsign(CallsignName, CallsignNumber) -- Debug. self:T(self.lid..string.format("Switching callsign to %d-%d", self.callsign.NumberSquad, self.callsign.NumberGroup)) - + -- Give command to change the callsign. self.group:CommandSetCallsign(self.callsign.NumberSquad, self.callsign.NumberGroup) + -- Callsign of the group, e.g. Colt-1 + self.callsignName=UTILS.GetCallsignName(self.callsign.NumberSquad).."-"..self.callsign.NumberGroup + self.callsign.NameSquad=UTILS.GetCallsignName(self.callsign.NumberSquad) + + -- Set callsign of elements. + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if element.status~=OPSGROUP.ElementStatus.DEAD then + element.callsign=element.unit:GetCallsign() + end + end + else - --TODO: Error + self:T(self.lid.."ERROR: Group is not alive and not in utero! Cannot switch callsign") end return self end +--- Get callsign of the first element alive. +-- @param #OPSGROUP self +-- @return #string Callsign name, e.g. Uzi11, or "Ghostrider11". +function OPSGROUP:GetCallsignName() + + 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 "Ghostrider11" +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Element and Group Status Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -131293,38 +147263,40 @@ end -- @return #OPSGROUP self function OPSGROUP:_UpdatePosition() - if self:IsAlive() then - + if self:IsExist() then + -- Backup last state to monitor differences. self.positionLast=self.position or self:GetVec3() self.headingLast=self.heading or self:GetHeading() self.orientXLast=self.orientX or self:GetOrientationX() self.velocityLast=self.velocity or self.group:GetVelocityMPS() - + -- Current state. self.position=self:GetVec3() self.heading=self:GetHeading() self.orientX=self:GetOrientationX() self.velocity=self:GetVelocity() - + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + element.vec3=self:GetVec3(element.name) + end + -- Update time. local Tnow=timer.getTime() self.dTpositionUpdate=self.TpositionUpdate and Tnow-self.TpositionUpdate or 0 self.TpositionUpdate=Tnow - + if not self.traveldist then self.traveldist=0 end - + + -- Travel distance since last check. self.travelds=UTILS.VecNorm(UTILS.VecSubstract(self.position, self.positionLast)) - + -- Add up travelled distance. - self.traveldist=self.traveldist+self.travelds - - -- Debug info. - --env.info(string.format("FF Traveled %.1f m", self.traveldist)) - + end return self @@ -131370,17 +147342,25 @@ function OPSGROUP:_AllSimilarStatus(status) for _,_element in pairs(self.elements) do local element=_element --#OPSGROUP.Element - + self:T2(self.lid..string.format("Status=%s, element %s status=%s", status, element.name, element.status)) -- Dead units dont count ==> We wont return false for those. if element.status~=OPSGROUP.ElementStatus.DEAD then - + ---------- -- ALIVE ---------- - if status==OPSGROUP.ElementStatus.SPAWNED then + if status==OPSGROUP.ElementStatus.INUTERO then + + -- Element INUTERO: Check that ALL others are also INUTERO + if element.status~=status then + return false + end + + + elseif status==OPSGROUP.ElementStatus.SPAWNED then -- Element SPAWNED: Check that others are not still IN UTERO if element.status~=status and @@ -131416,7 +147396,7 @@ function OPSGROUP:_AllSimilarStatus(status) element.status==OPSGROUP.ElementStatus.PARKING or element.status==OPSGROUP.ElementStatus.ENGINEON) then return false - end + end elseif status==OPSGROUP.ElementStatus.TAKEOFF then @@ -131438,7 +147418,7 @@ function OPSGROUP:_AllSimilarStatus(status) element.status==OPSGROUP.ElementStatus.SPAWNED or element.status==OPSGROUP.ElementStatus.PARKING or element.status==OPSGROUP.ElementStatus.ENGINEON or - element.status==OPSGROUP.ElementStatus.TAXIING or + element.status==OPSGROUP.ElementStatus.TAXIING or element.status==OPSGROUP.ElementStatus.TAKEOFF) then return false end @@ -131463,7 +147443,7 @@ function OPSGROUP:_AllSimilarStatus(status) end end - + else -- Element is dead. We don't care unless all are dead. end --DEAD @@ -131472,7 +147452,7 @@ function OPSGROUP:_AllSimilarStatus(status) -- Debug info. self:T2(self.lid..string.format("All %d elements have similar status %s ==> returning TRUE", #self.elements, status)) - + return true end @@ -131488,30 +147468,39 @@ function OPSGROUP:_UpdateStatus(element, newstatus, airbase) -- Update status of element. element.status=newstatus - + -- Debug - self:T3(self.lid..string.format("UpdateStatus element=%s: %s --> %s", element.name, oldstatus, newstatus)) + self:T3(self.lid..string.format("UpdateStatus element=%s: %s --> %s", element.name, oldstatus, newstatus)) for _,_element in pairs(self.elements) do local Element=_element -- #OPSGROUP.Element self:T3(self.lid..string.format("Element %s: %s", Element.name, Element.status)) end - if newstatus==OPSGROUP.ElementStatus.SPAWNED then + if newstatus==OPSGROUP.ElementStatus.INUTERO then + --- + -- INUTERO + --- + + if self:_AllSimilarStatus(newstatus) then + self:InUtero() + end + + elseif newstatus==OPSGROUP.ElementStatus.SPAWNED then --- -- SPAWNED --- if self:_AllSimilarStatus(newstatus) then - self:__Spawned(-0.5) + self:Spawned() end - + elseif newstatus==OPSGROUP.ElementStatus.PARKING then --- -- PARKING --- if self:_AllSimilarStatus(newstatus) then - self:__Parking(-0.5) + self:Parking() end elseif newstatus==OPSGROUP.ElementStatus.ENGINEON then @@ -131527,9 +147516,9 @@ function OPSGROUP:_UpdateStatus(element, newstatus, airbase) --- if self:_AllSimilarStatus(newstatus) then - self:__Taxiing(-0.5) + self:Taxiing() end - + elseif newstatus==OPSGROUP.ElementStatus.TAKEOFF then --- -- TAKEOFF @@ -131537,7 +147526,7 @@ function OPSGROUP:_UpdateStatus(element, newstatus, airbase) if self:_AllSimilarStatus(newstatus) then -- Trigger takeoff event. Also triggers airborne event. - self:__Takeoff(-0.5, airbase) + self:Takeoff(airbase) end elseif newstatus==OPSGROUP.ElementStatus.AIRBORNE then @@ -131546,7 +147535,7 @@ function OPSGROUP:_UpdateStatus(element, newstatus, airbase) --- if self:_AllSimilarStatus(newstatus) then - self:__Airborne(-0.5) + self:Airborne() end elseif newstatus==OPSGROUP.ElementStatus.LANDED then @@ -131584,7 +147573,7 @@ function OPSGROUP:_UpdateStatus(element, newstatus, airbase) --- if self:_AllSimilarStatus(newstatus) then - self:__Dead(-1) + self:Dead() end end @@ -131610,11 +147599,15 @@ end -- @return #OPSGROUP.Element The element. function OPSGROUP:GetElementByName(unitname) - for _,_element in pairs(self.elements) do - local element=_element --#OPSGROUP.Element + if unitname and type(unitname)=="string" then + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + if element.name==unitname then + return element + end - if element.name==unitname then - return element end end @@ -131622,6 +147615,181 @@ function OPSGROUP:GetElementByName(unitname) return nil end +--- Get the bounding box of the element. +-- @param #OPSGROUP self +-- @param #string UnitName Name of unit. +-- @return Core.Zone#ZONE_POLYGON Bounding box polygon zone. +function OPSGROUP:GetElementZoneBoundingBox(UnitName) + + local element=self:GetElementByName(UnitName) + + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + + -- Create a new zone if necessary. + element.zoneBoundingbox=element.zoneBoundingbox or ZONE_POLYGON_BASE:New(element.name.." Zone Bounding Box", {}) + + -- Length in meters. + local l=element.length + -- Width in meters. + local w=element.width + + -- Orientation vector. + local X=self:GetOrientationX(element.name) + + -- Heading in degrees. + local heading=math.deg(math.atan2(X.z, X.x)) + + -- Debug info. + self:T(self.lid..string.format("Element %s bouding box: l=%d w=%d heading=%d", element.name, l, w, heading)) + + -- Set of edges facing "North" at the origin of the map. + local b={} + b[1]={x=l/2, y=-w/2} --DCS#Vec2 + b[2]={x=l/2, y=w/2} --DCS#Vec2 + b[3]={x=-l/2, y=w/2} --DCS#Vec2 + b[4]={x=-l/2, y=-w/2} --DCS#Vec2 + + -- Rotate box to match current heading of the unit. + for i,p in pairs(b) do + b[i]=UTILS.Vec2Rotate2D(p, heading) + end + + -- Translate the zone to the positon of the unit. + local vec2=self:GetVec2(element.name) + local d=UTILS.Vec2Norm(vec2) + local h=UTILS.Vec2Hdg(vec2) + for i,p in pairs(b) do + b[i]=UTILS.Vec2Translate(p, d, h) + end + + -- Update existing zone. + element.zoneBoundingbox:UpdateFromVec2(b) + + return element.zoneBoundingbox + end + + return nil +end + +--- Get the loading zone of the element. +-- @param #OPSGROUP self +-- @param #string UnitName Name of unit. +-- @return Core.Zone#ZONE_POLYGON Bounding box polygon zone. +function OPSGROUP:GetElementZoneLoad(UnitName) + + local element=self:GetElementByName(UnitName) + + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + + element.zoneLoad=element.zoneLoad or ZONE_POLYGON_BASE:New(element.name.." Zone Load", {}) + + self:_GetElementZoneLoader(element, element.zoneLoad, self.carrierLoader) + + return element.zoneLoad + end + + return nil +end + +--- Get the unloading zone of the element. +-- @param #OPSGROUP self +-- @param #string UnitName Name of unit. +-- @return Core.Zone#ZONE_POLYGON Bounding box polygon zone. +function OPSGROUP:GetElementZoneUnload(UnitName) + + local element=self:GetElementByName(UnitName) + + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + + element.zoneUnload=element.zoneUnload or ZONE_POLYGON_BASE:New(element.name.." Zone Unload", {}) + + self:_GetElementZoneLoader(element, element.zoneUnload, self.carrierUnloader) + + return element.zoneUnload + end + + return nil +end + +--- Get/update the (un-)loading zone of the element. +-- @param #OPSGROUP self +-- @param #OPSGROUP.Element Element Element. +-- @param Core.Zone#ZONE_POLYGON Zone The zone. +-- @param #OPSGROUP.CarrierLoader Loader Loader parameters. +-- @return Core.Zone#ZONE_POLYGON Bounding box polygon zone. +function OPSGROUP:_GetElementZoneLoader(Element, Zone, Loader) + + if Element.status~=OPSGROUP.ElementStatus.DEAD then + + local l=Element.length + local w=Element.width + + -- Orientation 3D vector where the "nose" is pointing. + local X=self:GetOrientationX(Element.name) + + -- Heading in deg. + local heading=math.deg(math.atan2(X.z, X.x)) + + -- Bounding box at the origin of the map facing "North". + local b={} + + -- Create polygon rectangles. + if Loader.type:lower()=="front" then + table.insert(b, {x= l/2, y=-Loader.width/2}) -- left, low + table.insert(b, {x= l/2+Loader.length, y=-Loader.width/2}) -- left, up + table.insert(b, {x= l/2+Loader.length, y= Loader.width/2}) -- right, up + table.insert(b, {x= l/2, y= Loader.width/2}) -- right, low + elseif Loader.type:lower()=="back" then + table.insert(b, {x=-l/2, y=-Loader.width/2}) -- left, low + table.insert(b, {x=-l/2-Loader.length, y=-Loader.width/2}) -- left, up + table.insert(b, {x=-l/2-Loader.length, y= Loader.width/2}) -- right, up + table.insert(b, {x=-l/2, y= Loader.width/2}) -- right, low + elseif Loader.type:lower()=="left" then + table.insert(b, {x= Loader.length/2, y= -w/2}) -- right, up + table.insert(b, {x= Loader.length/2, y= -w/2-Loader.width}) -- left, up + table.insert(b, {x=-Loader.length/2, y= -w/2-Loader.width}) -- left, down + table.insert(b, {x=-Loader.length/2, y= -w/2}) -- right, down + elseif Loader.type:lower()=="right" then + table.insert(b, {x= Loader.length/2, y= w/2}) -- right, up + table.insert(b, {x= Loader.length/2, y= w/2+Loader.width}) -- left, up + table.insert(b, {x=-Loader.length/2, y= w/2+Loader.width}) -- left, down + table.insert(b, {x=-Loader.length/2, y= w/2}) -- right, down + else + -- All aspect. Rectangle around the unit but need to cut out the area of the unit itself. + b[1]={x= l/2, y=-w/2} --DCS#Vec2 + b[2]={x= l/2, y= w/2} --DCS#Vec2 + b[3]={x=-l/2, y= w/2} --DCS#Vec2 + b[4]={x=-l/2, y=-w/2} --DCS#Vec2 + table.insert(b, {x=b[1].x+Loader.length, y=b[1].y-Loader.width}) + table.insert(b, {x=b[2].x+Loader.length, y=b[2].y+Loader.width}) + table.insert(b, {x=b[3].x-Loader.length, y=b[3].y+Loader.width}) + table.insert(b, {x=b[4].x-Loader.length, y=b[4].y-Loader.width}) + end + + -- Rotate edges to match the current heading of the unit. + for i,p in pairs(b) do + b[i]=UTILS.Vec2Rotate2D(p, heading) + end + + -- Translate box to the current position of the unit. + local vec2=self:GetVec2(Element.name) + local d=UTILS.Vec2Norm(vec2) + local h=UTILS.Vec2Hdg(vec2) + + for i,p in pairs(b) do + b[i]=UTILS.Vec2Translate(p, d, h) + end + + -- Update existing zone. + Zone:UpdateFromVec2(b) + + return Zone + end + + return nil +end + + --- Get the first element of a group, which is alive. -- @param #OPSGROUP self -- @return #OPSGROUP.Element The element or `#nil` if no element is alive any more. @@ -131631,7 +147799,7 @@ function OPSGROUP:GetElementAlive() local element=_element --#OPSGROUP.Element if element.status~=OPSGROUP.ElementStatus.DEAD then if element.unit and element.unit:IsAlive() then - return element + return element end end end @@ -131639,6 +147807,7 @@ function OPSGROUP:GetElementAlive() return nil end + --- Get number of elements alive. -- @param #OPSGROUP self -- @param #string status (Optional) Only count number, which are in a special status. @@ -131657,7 +147826,7 @@ function OPSGROUP:GetNelements(status) end end - + return n end @@ -131675,7 +147844,7 @@ end function OPSGROUP:GetAmmoTot() local units=self.group:GetUnits() - + local Ammo={} --#OPSGROUP.Ammo Ammo.Total=0 Ammo.Guns=0 @@ -131688,15 +147857,15 @@ function OPSGROUP:GetAmmoTot() Ammo.MissilesAS=0 Ammo.MissilesCR=0 Ammo.MissilesSA=0 - - for _,_unit in pairs(units) do + + for _,_unit in pairs(units or {}) do local unit=_unit --Wrapper.Unit#UNIT - + if unit and unit:IsAlive()~=nil then - + -- Get ammo of the unit. local ammo=self:GetAmmoUnit(unit) - + -- Add up total. Ammo.Total=Ammo.Total+ammo.Total Ammo.Guns=Ammo.Guns+ammo.Guns @@ -131709,9 +147878,9 @@ function OPSGROUP:GetAmmoTot() Ammo.MissilesAS=Ammo.MissilesAS+ammo.MissilesAS Ammo.MissilesCR=Ammo.MissilesCR+ammo.MissilesCR Ammo.MissilesSA=Ammo.MissilesSA+ammo.MissilesSA - + end - + end return Ammo @@ -131728,6 +147897,8 @@ function OPSGROUP:GetAmmoUnit(unit, display) if display==nil then display=false end + + unit=unit or self.group:GetUnit(1) -- Init counter. local nammo=0 @@ -131752,12 +147923,18 @@ function OPSGROUP:GetAmmoUnit(unit, display) if ammotable then local weapons=#ammotable + + --self:I(ammotable) -- Loop over all weapons. for w=1,weapons do -- Number of current weapon. local Nammo=ammotable[w]["count"] + + -- Range in meters. Seems only to exist for missiles (not shells). + local rmin=ammotable[w]["desc"]["rangeMin"] or 0 + local rmax=ammotable[w]["desc"]["rangeMaxAltMin"] or 0 -- Type name of current weapon. local Tammo=ammotable[w]["desc"]["typeName"] @@ -131781,7 +147958,7 @@ function OPSGROUP:GetAmmoUnit(unit, display) nshells=nshells+Nammo -- Debug info. - text=text..string.format("- %d shells of type %s\n", Nammo, _weaponName) + text=text..string.format("- %d shells of type %s, range=%d - %d meters\n", Nammo, _weaponName, rmin, rmax) elseif Category==Weapon.Category.ROCKET then @@ -131789,7 +147966,7 @@ function OPSGROUP:GetAmmoUnit(unit, display) nrockets=nrockets+Nammo -- Debug info. - text=text..string.format("- %d rockets of type %s\n", Nammo, _weaponName) + text=text..string.format("- %d rockets of type %s, \n", Nammo, _weaponName, rmin, rmax) elseif Category==Weapon.Category.BOMB then @@ -131807,28 +147984,28 @@ function OPSGROUP:GetAmmoUnit(unit, display) nmissilesAA=nmissilesAA+Nammo elseif MissileCategory==Weapon.MissileCategory.SAM then nmissiles=nmissiles+Nammo - nmissilesSA=nmissilesSA+Nammo + nmissilesSA=nmissilesSA+Nammo elseif MissileCategory==Weapon.MissileCategory.ANTI_SHIP then nmissiles=nmissiles+Nammo nmissilesAS=nmissilesAS+Nammo elseif MissileCategory==Weapon.MissileCategory.BM then nmissiles=nmissiles+Nammo - nmissilesAG=nmissilesAG+Nammo + nmissilesBM=nmissilesBM+Nammo elseif MissileCategory==Weapon.MissileCategory.CRUISE then nmissiles=nmissiles+Nammo - nmissilesCR=nmissilesCR+Nammo + nmissilesCR=nmissilesCR+Nammo elseif MissileCategory==Weapon.MissileCategory.OTHER then nmissiles=nmissiles+Nammo nmissilesAG=nmissilesAG+Nammo end -- Debug info. - text=text..string.format("- %d %s missiles of type %s\n", Nammo, self:_MissileCategoryName(MissileCategory), _weaponName) - + text=text..string.format("- %d %s missiles of type %s, range=%d - %d meters\n", Nammo, self:_MissileCategoryName(MissileCategory), _weaponName, rmin, rmax) + elseif Category==Weapon.Category.TORPEDO then - + -- Add up all rockets. - ntorps=ntorps+Nammo + ntorps=ntorps+Nammo -- Debug info. text=text..string.format("- %d torpedos of type %s\n", Nammo, _weaponName) @@ -131892,26 +148069,372 @@ function OPSGROUP:_MissileCategoryName(categorynumber) return cat end +--- Set passed final waypoint value. +-- @param #OPSGROUP self +-- @param #boolean final If `true`, final waypoint was passed. +-- @param #string comment Some comment as to why the final waypoint was passed. +function OPSGROUP:_PassedFinalWaypoint(final, comment) + + -- Debug info. + self:T(self.lid..string.format("Passed final waypoint=%s [from %s]: comment \"%s\"", tostring(final), tostring(self.passedfinalwp), tostring(comment))) + + if final==true and not self.passedfinalwp then + self:PassedFinalWaypoint() + end + + -- Set value. + self.passedfinalwp=final +end + + --- Get coordinate from an object. -- @param #OPSGROUP self -- @param Wrapper.Object#OBJECT Object The object. -- @return Core.Point#COORDINATE The coordinate of the object. function OPSGROUP:_CoordinateFromObject(Object) - - if Object:IsInstanceOf("COORDINATE") then - return Object - else - if Object:IsInstanceOf("POSITIONABLE") or Object:IsInstanceOf("ZONE_BASE") then - self:T(self.lid.."WARNING: Coordinate is not a COORDINATE but a POSITIONABLE or ZONE. Trying to get coordinate") - return Object:GetCoordinate() + + if Object then + if Object:IsInstanceOf("COORDINATE") then + return Object else - self:E(self.lid.."ERROR: Coordinate is neither a COORDINATE nor any POSITIONABLE or ZONE!") + if Object:IsInstanceOf("POSITIONABLE") or Object:IsInstanceOf("ZONE_BASE") then + self:T(self.lid.."WARNING: Coordinate is not a COORDINATE but a POSITIONABLE or ZONE. Trying to get coordinate") + local coord=Object:GetCoordinate() + return coord + else + self:T(self.lid.."ERROR: Coordinate is neither a COORDINATE nor any POSITIONABLE or ZONE!") + end end - end + else + self:T(self.lid.."ERROR: Object passed is nil!") + end return nil end +--- Check if a unit is an element of the flightgroup. +-- @param #OPSGROUP self +-- @param #string unitname Name of unit. +-- @return #boolean If true, unit is element of the flight group or false if otherwise. +function OPSGROUP:_IsElement(unitname) + + for _,_element in pairs(self.elements) do + local element=_element --Ops.OpsGroup#OPSGROUP.Element + + if element.name==unitname then + return true + end + + end + + return false +end + +--- Count elements of the group. +-- @param #OPSGROUP self +-- @param #table States (Optional) Only count elements in specific states. Can also be a single state passed as #string. +-- @return #number Number of elements. +function OPSGROUP:CountElements(States) + + if States then + if type(States)=="string" then + States={States} + end + else + States=OPSGROUP.ElementStatus + end + + local IncludeDeads=true + + local N=0 + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if element and (IncludeDeads or element.status~=OPSGROUP.ElementStatus.DEAD) then + for _,state in pairs(States) do + if element.status==state then + N=N+1 + break + end + end + end + end + + return N +end + +--- Add a unit/element to the OPS group. +-- @param #OPSGROUP self +-- @param #string unitname Name of unit. +-- @return #OPSGROUP.Element The element or nil. +function OPSGROUP:_AddElementByName(unitname) + + local unit=UNIT:FindByName(unitname) + + if unit then + + -- Get unit template. + local unittemplate=unit:GetTemplate() + --local unittemplate=_DATABASE:GetUnitTemplateFromUnitName(unitname) + + -- Element table. + local element=self:GetElementByName(unitname) + + -- Add element to table. + if element then + -- We already know this element. + else + -- Add a new element. + element={} + element.status=OPSGROUP.ElementStatus.INUTERO + table.insert(self.elements, element) + end + + -- Name and status. + element.name=unitname + + -- Unit and group. + element.unit=unit + element.DCSunit=Unit.getByName(unitname) + element.gid=element.DCSunit:getNumber() + element.uid=element.DCSunit:getID() + --element.group=unit:GetGroup() + element.controller=element.DCSunit:getController() + element.Nhit=0 + element.opsgroup=self + + -- Skill etc. + element.skill=unittemplate.skill or "Unknown" + 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 + + -- Descriptors and type/category. + element.descriptors=unit:GetDesc() + element.category=unit:GetUnitCategory() + element.categoryname=unit:GetCategoryName() + element.typename=unit:GetTypeName() + + -- Describtors. + --self:I({desc=element.descriptors}) + + -- Ammo. + element.ammo0=self:GetAmmoUnit(unit, false) + + -- Life points. + element.life=unit:GetLife() + element.life0=math.max(unit:GetLife0(), element.life) -- Some units report a life0 that is smaller than its initial life points. + + -- Size and dimensions. + element.size, element.length, element.height, element.width=unit:GetObjectSize() + + -- Weight and cargo. + element.weightEmpty=element.descriptors.massEmpty or 666 + + if self.isArmygroup then + + element.weightMaxTotal=element.weightEmpty+10*95 --If max mass is not given, we assume 10 soldiers. + + elseif self.isNavygroup then + + element.weightMaxTotal=element.weightEmpty+10*1000 + + else + + -- Looks like only aircraft have a massMax value in the descriptors. + element.weightMaxTotal=element.descriptors.massMax or element.weightEmpty+8*95 --If max mass is not given, we assume 8 soldiers. + + end + + -- Max cargo weight: + unit:SetCargoBayWeightLimit() + element.weightMaxCargo=unit.__.CargoBayWeightLimit + + -- Cargo bay (empty). + if element.cargoBay then + -- After a respawn, the cargo bay might not be empty! + element.weightCargo=self:GetWeightCargo(element.name, false) + else + element.cargoBay={} + element.weightCargo=0 + end + element.weight=element.weightEmpty+element.weightCargo + + -- FLIGHTGROUP specific. + if self.isFlightgroup then + element.callsign=element.unit:GetCallsign() + element.modex=unittemplate.onboard_num + element.payload=unittemplate.payload + element.pylons=unittemplate.payload and unittemplate.payload.pylons or nil + element.fuelmass0=unittemplate.payload and unittemplate.payload.fuel or 0 + element.fuelmass=element.fuelmass0 + element.fuelrel=element.unit:GetFuel() + else + element.callsign="Peter-1-1" + element.modex="000" + element.payload={} + element.pylons={} + element.fuelmass0=99999 + element.fuelmass =99999 + element.fuelrel=1 + end + + -- Debug text. + local text=string.format("Adding element %s: status=%s, skill=%s, life=%.1f/%.1f category=%s (%d), type=%s, size=%.1f (L=%.1f H=%.1f W=%.1f), weight=%.1f/%.1f (cargo=%.1f/%.1f)", + element.name, element.status, element.skill, element.life, element.life0, element.categoryname, element.category, element.typename, + element.size, element.length, element.height, element.width, element.weight, element.weightMaxTotal, element.weightCargo, element.weightMaxCargo) + self:T(self.lid..text) + + -- Trigger spawned event if alive. + if unit:IsAlive() and element.status~=OPSGROUP.ElementStatus.SPAWNED then + -- This needs to be slightly delayed (or moved elsewhere) or the first element will always trigger the group spawned event as it is not known that more elements are in the group. + self:__ElementSpawned(0.05, element) + end + + return element + end + + return nil +end + +--- Set the template of the group. +-- @param #OPSGROUP self +-- @param #table Template Template to set. Default is from the GROUP. +-- @return #OPSGROUP self +function OPSGROUP:_SetTemplate(Template) + + -- Set the template. + self.template=Template or UTILS.DeepCopy(_DATABASE:GetGroupTemplate(self.groupname)) --self.group:GetTemplate() + + -- Debug info. + self:T3(self.lid.."Setting group template") + + return self +end + +--- Get the template of the group. +-- @param #OPSGROUP self +-- @param #boolean Copy Get a deep copy of the template. +-- @return #table Template table. +function OPSGROUP:_GetTemplate(Copy) + + if self.template then + + if Copy then + local template=UTILS.DeepCopy(self.template) + return template + else + return self.template + end + + else + self:T(self.lid..string.format("ERROR: No template was set yet!")) + end + + return nil +end + +--- Clear waypoints. +-- @param #OPSGROUP self +-- @param #number IndexMin Clear waypoints up to this min WP index. Default 1. +-- @param #number IndexMax Clear waypoints up to this max WP index. Default `#self.waypoints`. +function OPSGROUP:ClearWaypoints(IndexMin, IndexMax) + + IndexMin=IndexMin or 1 + IndexMax=IndexMax or #self.waypoints + + -- Clear all waypoints. + for i=IndexMax,IndexMin,-1 do + table.remove(self.waypoints, i) + end + --self.waypoints={} +end + +--- Get target group. +-- @param #OPSGROUP self +-- @return Wrapper.Group#GROUP Detected target group. +-- @return #number Distance to target. +function OPSGROUP:_GetDetectedTarget() + + -- Target. + local targetgroup=nil --Wrapper.Group#GROUP + local targetdist=math.huge + + -- Loop over detected groups. + for _,_group in pairs(self.detectedgroups:GetSet()) do + local group=_group --Wrapper.Group#GROUP + + if group and group:IsAlive() then + + -- Get 3D vector of target. + local targetVec3=group:GetVec3() + + -- Distance to target. + local distance=UTILS.VecDist3D(self.position, targetVec3) + + if distance<=self.engagedetectedRmax and distance calculate fuel consumption ==> calculate range remaining. Will this give half way accurate results? +-- DONE: Use new UnitLost event instead of crash/dead. +-- DONE: Monitor traveled distance in air ==> calculate fuel consumption ==> calculate range remaining. Will this give half way accurate results? -- DONE: Out of AG/AA missiles. Safe state of out-of-ammo. +-- DONE: Add TACAN beacon. -- DONE: Add tasks. -- DONE: Waypoints, read, add, insert, detour. -- DONE: Get ammo. @@ -132150,10 +148682,10 @@ FLIGHTGROUP.version="0.6.1" function FLIGHTGROUP:New(group) -- First check if we already have a flight group for this group. - local fg=_DATABASE:GetFlightGroup(group) - if fg then - fg:I(fg.lid..string.format("WARNING: Flight group already exists in data base!")) - return fg + local og=_DATABASE:GetOpsGroup(group) + if og then + og:I(og.lid..string.format("WARNING: OPS group already exists in data base!")) + return og end -- Inherit everything from FSM class. @@ -132163,15 +148695,14 @@ function FLIGHTGROUP:New(group) self.lid=string.format("FLIGHTGROUP %s | ", self.groupname) -- Defaults - --self:SetVerbosity(0) + self:SetDefaultROE() + self:SetDefaultROT() + self:SetDefaultEPLRS(self.isEPLRS) + self:SetDetection() self:SetFuelLowThreshold() self:SetFuelLowRTB() self:SetFuelCriticalThreshold() - self:SetFuelCriticalRTB() - self:SetDefaultROE() - self:SetDefaultROT() - self:SetDetection() - self.isFlightgroup=true + self:SetFuelCriticalRTB() -- Holding flag. self.flaghold=USERFLAG:New(string.format("%s_FlagHold", self.groupname)) @@ -132179,27 +148710,22 @@ function FLIGHTGROUP:New(group) -- Add FSM transitions. -- From State --> Event --> To State - self:AddTransition("*", "RTB", "Inbound") -- Group is returning to destination base. + self:AddTransition("*", "LandAtAirbase", "Inbound") -- Group is ordered to land at an airbase. + self:AddTransition("*", "RTB", "Inbound") -- Group is returning to (home/destination) airbase. self:AddTransition("*", "RTZ", "Inbound") -- Group is returning to destination zone. Not implemented yet! self:AddTransition("Inbound", "Holding", "Holding") -- Group is in holding pattern. self:AddTransition("*", "Refuel", "Going4Fuel") -- Group is send to refuel at a tanker. - self:AddTransition("Going4Fuel", "Refueled", "Airborne") -- Group finished refueling. + self:AddTransition("Going4Fuel", "Refueled", "Cruising") -- Group finished refueling. self:AddTransition("*", "LandAt", "LandingAt") -- Helo group is ordered to land at a specific point. self:AddTransition("LandingAt", "LandedAt", "LandedAt") -- Helo group landed landed at a specific point. - self:AddTransition("*", "Wait", "*") -- Group is orbiting. - self:AddTransition("*", "FuelLow", "*") -- Fuel state of group is low. Default ~25%. self:AddTransition("*", "FuelCritical", "*") -- Fuel state of group is critical. Default ~10%. - self:AddTransition("*", "OutOfMissilesAA", "*") -- Group is out of A2A missiles. - self:AddTransition("*", "OutOfMissilesAG", "*") -- Group is out of A2G missiles. - self:AddTransition("*", "OutOfMissilesAS", "*") -- Group is out of A2S(ship) missiles. Not implemented yet! - - self:AddTransition("Airborne", "EngageTarget", "Engaging") -- Engage targets. - self:AddTransition("Engaging", "Disengage", "Airborne") -- Engagement over. + self:AddTransition("Cruising", "EngageTarget", "Engaging") -- Engage targets. + self:AddTransition("Engaging", "Disengage", "Cruising") -- Engagement over. self:AddTransition("*", "ElementParking", "*") -- An element is parking. self:AddTransition("*", "ElementEngineOn", "*") -- An element spooled up the engines. @@ -132215,6 +148741,7 @@ function FLIGHTGROUP:New(group) self:AddTransition("*", "Taxiing", "Taxiing") -- The whole flight group is taxiing. self:AddTransition("*", "Takeoff", "Airborne") -- The whole flight group is airborne. self:AddTransition("*", "Airborne", "Airborne") -- The whole flight group is airborne. + self:AddTransition("*", "Cruise", "Cruising") -- The whole flight group is cruising. self:AddTransition("*", "Landing", "Landing") -- The whole flight group is landing. self:AddTransition("*", "Landed", "Landed") -- The whole flight group has landed. self:AddTransition("*", "Arrived", "Arrived") -- The whole flight group has arrived. @@ -132234,44 +148761,38 @@ function FLIGHTGROUP:New(group) -- TODO: Add pseudo functions. - -- Debug trace. - if false then - BASE:TraceOnOff(true) - BASE:TraceClass(self.ClassName) - BASE:TraceLevel(1) - end - - -- Add to data base. - _DATABASE:AddFlightGroup(self) - -- 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() + self:_InitWaypoints() -- Initialize group. self:_InitGroup() -- Start the status monitoring. - self:__Status(-1) - + self.timerStatus=TIMER:New(self.Status, self):Start(1, 30) + -- Start queue update timer. self.timerQueueUpdate=TIMER:New(self._QueueUpdate, self):Start(2, 5) - + -- Start check zone timer. self.timerCheckZone=TIMER:New(self._CheckInZones, self):Start(3, 10) + -- Add OPSGROUP to _DATABASE. + _DATABASE:AddOpsGroup(self) + return self end @@ -132289,21 +148810,33 @@ function FLIGHTGROUP:AddTaskEnrouteEngageTargetsInZone(ZoneRadius, TargetTypes, self:AddTaskEnroute(Task) end ---- Set AIRWING the flight group belongs to. --- @param #FLIGHTGROUP self --- @param Ops.AirWing#AIRWING airwing The AIRWING object. --- @return #FLIGHTGROUP self -function FLIGHTGROUP:SetAirwing(airwing) - self:T(self.lid..string.format("Add flight to AIRWING %s", airwing.alias)) - self.airwing=airwing - return self -end - --- Get airwing the flight group belongs to. -- @param #FLIGHTGROUP self -- @return Ops.AirWing#AIRWING The AIRWING object. function FLIGHTGROUP:GetAirWing() - return self.airwing + return self.legion +end + +--- Set if aircraft is VTOL capable. Unfortunately, there is no DCS way to determine this via scripting. +-- @param #FLIGHTGROUP self +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetVTOL() + self.isVTOL=true + 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. @@ -132314,7 +148847,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 @@ -132324,15 +148857,12 @@ function FLIGHTGROUP:SetFlightControl(flightcontrol) end -- Set FC. - self:I(self.lid..string.format("Setting FLIGHTCONTROL to airbase %s", flightcontrol.airbasename)) + self:T(self.lid..string.format("Setting FLIGHTCONTROL to airbase %s", flightcontrol.airbasename)) 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 @@ -132351,6 +148881,9 @@ end -- @param Wrapper.Airbase#AIRBASE HomeAirbase The home airbase. -- @return #FLIGHTGROUP self function FLIGHTGROUP:SetHomebase(HomeAirbase) + if type(HomeAirbase)=="string" then + HomeAirbase=AIRBASE:FindByName(HomeAirbase) + end self.homebase=HomeAirbase return self end @@ -132360,6 +148893,9 @@ end -- @param Wrapper.Airbase#AIRBASE DestinationAirbase The destination airbase. -- @return #FLIGHTGROUP self function FLIGHTGROUP:SetDestinationbase(DestinationAirbase) + if type(DestinationAirbase)=="string" then + DestinationAirbase=AIRBASE:FindByName(DestinationAirbase) + end self.destbase=DestinationAirbase return self end @@ -132457,55 +148993,6 @@ function FLIGHTGROUP:SetFuelCriticalRTB(switch) return self end ---- Enable to automatically engage detected targets. --- @param #FLIGHTGROUP self --- @param #number RangeMax Max range in NM. Only detected targets within this radius from the group will be engaged. Default is 25 NM. --- @param #table TargetTypes Types of target attributes that will be engaged. See [DCS enum attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes). Default "All". --- @param Core.Set#SET_ZONE EngageZoneSet Set of zones in which targets are engaged. Default is anywhere. --- @param Core.Set#SET_ZONE NoEngageZoneSet Set of zones in which targets are *not* engaged. Default is nowhere. --- @return #FLIGHTGROUP self -function FLIGHTGROUP:SetEngageDetectedOn(RangeMax, TargetTypes, EngageZoneSet, NoEngageZoneSet) - - -- Ensure table. - if TargetTypes then - if type(TargetTypes)~="table" then - TargetTypes={TargetTypes} - end - else - TargetTypes={"All"} - end - - -- Ensure SET_ZONE if ZONE is provided. - if EngageZoneSet and EngageZoneSet:IsInstanceOf("ZONE_BASE") then - local zoneset=SET_ZONE:New():AddZone(EngageZoneSet) - EngageZoneSet=zoneset - end - if NoEngageZoneSet and NoEngageZoneSet:IsInstanceOf("ZONE_BASE") then - local zoneset=SET_ZONE:New():AddZone(NoEngageZoneSet) - NoEngageZoneSet=zoneset - end - - -- Set parameters. - self.engagedetectedOn=true - self.engagedetectedRmax=UTILS.NMToMeters(RangeMax or 25) - self.engagedetectedTypes=TargetTypes - self.engagedetectedEngageZones=EngageZoneSet - self.engagedetectedNoEngageZones=NoEngageZoneSet - - -- Ensure detection is ON or it does not make any sense. - self:SetDetection(true) - - return self -end - ---- Disable to automatically engage detected targets. --- @param #FLIGHTGROUP self --- @return #OPSGROUP self -function FLIGHTGROUP:SetEngageDetectedOff() - self.engagedetectedOn=false - return self -end - --- Enable that the group is despawned after landing. This can be useful to avoid DCS taxi issues with other AI or players or jamming taxiways. -- @param #FLIGHTGROUP self @@ -132515,89 +149002,133 @@ function FLIGHTGROUP:SetDespawnAfterLanding() return self end +--- Enable that the group is despawned after holding. This can be useful to avoid DCS taxi issues with other AI or players or jamming taxiways. +-- @param #FLIGHTGROUP self +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetDespawnAfterHolding() + self.despawnAfterHolding=true + return self +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() - return self:Is("Parking") +function FLIGHTGROUP:IsParking(Element) + local is=self:Is("Parking") + if Element then + is=Element.status==OPSGROUP.ElementStatus.PARKING + end + return is 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() - return self:Is("Taxiing") +function FLIGHTGROUP:IsTaxiing(Element) + local is=self:Is("Taxiing") + if Element then + is=Element.status==OPSGROUP.ElementStatus.TAXIING + end + return is end ---- Check if flight is airborne. +--- 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(Element) + local is=self:Is("Airborne") or self:Is("Cruising") + if Element then + is=Element.status==OPSGROUP.ElementStatus.AIRBORNE + end + return is +end + +--- Check if flight is airborne or cruising. -- @param #FLIGHTGROUP self -- @return #boolean If true, flight is airborne. -function FLIGHTGROUP:IsAirborne() - return self:Is("Airborne") -end - ---- Check if flight is waiting after passing final waypoint. --- @param #FLIGHTGROUP self --- @return #boolean If true, flight is waiting. -function FLIGHTGROUP:IsWaiting() - return self:Is("Waiting") +function FLIGHTGROUP:IsCruising() + local is=self:Is("Cruising") + return is 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() - return self:Is("Landing") +function FLIGHTGROUP:IsLanding(Element) + local is=self:Is("Landing") + if Element then + is=Element.status==OPSGROUP.ElementStatus.LANDING + end + return is 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() - return self:Is("Landed") +function FLIGHTGROUP:IsLanded(Element) + local is=self:Is("Landed") + if Element then + is=Element.status==OPSGROUP.ElementStatus.LANDED + end + return is 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() - return self:Is("Arrived") +function FLIGHTGROUP:IsArrived(Element) + local is=self:Is("Arrived") + if Element then + is=Element.status==OPSGROUP.ElementStatus.ARRIVED + end + return is end --- Check if flight is inbound and traveling to holding pattern. -- @param #FLIGHTGROUP self -- @return #boolean If true, flight is holding. function FLIGHTGROUP:IsInbound() - return self:Is("Inbound") + local is=self:Is("Inbound") + return is end --- Check if flight is holding and waiting for landing clearance. -- @param #FLIGHTGROUP self -- @return #boolean If true, flight is holding. function FLIGHTGROUP:IsHolding() - return self:Is("Holding") + local is=self:Is("Holding") + return is end --- Check if flight is going for fuel. -- @param #FLIGHTGROUP self -- @return #boolean If true, flight is refueling. function FLIGHTGROUP:IsGoing4Fuel() - return self:Is("Going4Fuel") + local is=self:Is("Going4Fuel") + return is end --- Check if helo(!) flight is ordered to land at a specific point. -- @param #FLIGHTGROUP self -- @return #boolean If true, group has task to land somewhere. function FLIGHTGROUP:IsLandingAt() - return self:Is("LandingAt") + local is=self:Is("LandingAt") + return is 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") + is=self:Is("LandedAt") + return is end --- Check if flight is low on fuel. @@ -132614,6 +149145,15 @@ function FLIGHTGROUP:IsFuelCritical() return self.fuelcritical end +--- Check if flight is good on fuel (not below low or even critical state). +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight is good on fuel. +function FLIGHTGROUP:IsFuelGood() + local isgood=not (self.fuellow or self.fuelcritical) + return isgood +end + + --- Check if flight can do air-to-ground tasks. -- @param #FLIGHTGROUP self -- @param #boolean ExcludeGuns If true, exclude gun @@ -132653,13 +149193,20 @@ function FLIGHTGROUP:StartUncontrolled(delay) self:ScheduleOnce(delay, FLIGHTGROUP.StartUncontrolled, self) else - if self:IsAlive() then - --TODO: check Alive==true and Alive==false ==> Activate first + local alive=self:IsAlive() + + if alive~=nil then + -- Check if group is already active. + local _delay=0 + if alive==false then + self:Activate() + _delay=1 + end self:T(self.lid.."Starting uncontrolled group") - self.group:StartUncontrolled(delay) - self.isUncontrolled=true + self.group:StartUncontrolled(_delay) + self.isUncontrolled=false else - self:E(self.lid.."ERROR: Could not start uncontrolled group as it is NOT alive!") + self:T(self.lid.."ERROR: Could not start uncontrolled group as it is NOT alive!") end end @@ -132678,8 +149225,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 @@ -132693,7 +149252,7 @@ function FLIGHTGROUP:GetFuelMin() local fuelmin=math.huge for i,_element in pairs(self.elements) do - local element=_element --#FLIGHTGROUP.Element + local element=_element --Ops.OpsGroup#OPSGROUP.Element local unit=element.unit @@ -132721,155 +149280,166 @@ end -- Status ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ----- Update status. +--- Status update. -- @param #FLIGHTGROUP self -function FLIGHTGROUP:onbeforeStatus(From, Event, To) - - -- First we check if elements are still alive. Could be that they were despawned without notice, e.g. when landing on a too small airbase. - for i,_element in pairs(self.elements) do - local element=_element --#FLIGHTGROUP.Element - - -- Check that element is not already dead or not yet alive. - if element.status~=OPSGROUP.ElementStatus.DEAD and element.status~=OPSGROUP.ElementStatus.INUTERO then - - -- Unit shortcut. - local unit=element.unit - - local isdead=false - if unit and unit:IsAlive() then - - -- Get life points. - local life=unit:GetLife() or 0 - - -- Units with life <=1 are dead. - if life<=1 then - --env.info(string.format("FF unit %s: live<=1 in status at T=%.3f", unit:GetName(), timer.getTime())) - isdead=true - end - - else - -- Not alive any more. - --env.info(string.format("FF unit %s: NOT alive in status at T=%.3f", unit:GetName(), timer.getTime())) - isdead=true - end - - -- This one is dead. - if isdead then - local text=string.format("Element %s is dead at t=%.3f! Maybe despawned without notice or landed at a too small airbase. Calling ElementDead in 60 sec to give other events a chance", - tostring(element.name), timer.getTime()) - self:E(self.lid..text) - self:__ElementDead(60, element) - end - - end - end - - if self:IsDead() then - self:T(self.lid..string.format("Onbefore Status DEAD ==> false")) - return false - elseif self:IsStopped() then - self:T(self.lid..string.format("Onbefore Status STOPPED ==> false")) - return false - end - - return true -end - ---- On after "Status" event. --- @param #FLIGHTGROUP self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. -function FLIGHTGROUP:onafterStatus(From, Event, To) +function FLIGHTGROUP:Status() -- FSM state. local fsmstate=self:GetState() - -- Update position. - self:_UpdatePosition() + -- Is group alive? + local alive=self:IsAlive() + + if alive then - --- - -- Detection - --- - - -- Check if group has detected any units. - if self.detectionOn then + -- Update position. + self:_UpdatePosition() + + -- Check if group has detected any units. self:_CheckDetectedUnits() - end - - --- - -- Parking - --- - - -- Check if flight began to taxi (if it was parking). - if self:IsParking() then - for _,_element in pairs(self.elements) do - local element=_element --#FLIGHTGROUP.Element - 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 + + -- Check ammo status. + self:_CheckAmmoStatus() + + -- Check damage. + self:_CheckDamage() + + -- TODO: Check if group is waiting? + if self:IsWaiting() then + if self.Twaiting and self.dTwait then + if timer.getAbsTime()>self.Twaiting+self.dTwait then + --self.Twaiting=nil + --self.dTwait=nil + --self:Cruise() + end + end + end + + + -- TODO: _CheckParking() function + + -- Check if flight began to taxi (if it was parking). + 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=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 + --self:T(self.lid..string.format("Element %s is in PARKING queue but has no parking spot assigned!", element.name)) end - - else - --self:E(self.lid..string.format("Element %s is in PARKING queue but has no parking spot assigned!", element.name)) end end - end + else + -- Check damage. + self:_CheckDamage() + end + --- -- Group --- -- Short info. if self.verbose>=1 then - + + -- Number of elements. + local nelem=self:CountElements() + local Nelem=#self.elements + + -- Get number of tasks and missions. local nTaskTot, nTaskSched, nTaskWP=self:CountRemainingTasks() local nMissions=self:CountRemainingMissison() - - - local text=string.format("Status %s [%d/%d]: Tasks=%d (%d,%d) Curr=%d, Missions=%s, Waypoint=%d/%d, Detected=%d, Home=%s, Destination=%s", - fsmstate, #self.elements, #self.elements, nTaskTot, nTaskSched, nTaskWP, self.taskcurrent, nMissions, self.currentwp or 0, self.waypoints and #self.waypoints or 0, - self.detectedunits:Count(), self.homebase and self.homebase:GetName() or "unknown", self.destbase and self.destbase:GetName() or "unknown") - self:I(self.lid..text) + -- ROE and Alarm State. + local roe=self:GetROE() or -1 + local rot=self:GetROT() or -1 + + -- Waypoint stuff. + local wpidxCurr=self.currentwp + local wpuidCurr=self:GetWaypointUIDFromIndex(wpidxCurr) or 0 + local wpidxNext=self:GetWaypointIndexNext() or 0 + local wpuidNext=self:GetWaypointUIDFromIndex(wpidxNext) or 0 + local wpN=#self.waypoints or 0 + local wpF=tostring(self.passedfinalwp) + + -- Speed. + local speed=UTILS.MpsToKnots(self.velocity or 0) + local speedEx=UTILS.MpsToKnots(self:GetExpectedSpeed()) + + -- Altitude. + local alt=self.position and self.position.y or 0 + + -- Heading in degrees. + local hdg=self.heading or 0 + + -- TODO: GetFormation function. + local formation=self.option.Formation or "unknown" + + -- Life points. + local life=self.life or 0 + + -- Total ammo. + local ammo=self:GetAmmoTot().Total + + -- Detected units. + local ndetected=self.detectionOn and tostring(self.detectedunits:Count()) or "Off" + + -- Get cargo weight. + local cargo=0 + for _,_element in pairs(self.elements) do + local element=_element --Ops.OpsGroup#OPSGROUP.Element + cargo=cargo+element.weightCargo + end + + -- Home and destination base. + local home=self.homebase and self.homebase:GetName() or "unknown" + local dest=self.destbase and self.destbase:GetName() or "unknown" + local curr=self.currbase and self.currbase:GetName() or "N/A" + + -- Info text. + local text=string.format("%s [%d/%d]: ROE/ROT=%d/%d | T/M=%d/%d | Wp=%d[%d]-->%d[%d]/%d [%s] | Life=%.1f | v=%.1f (%d) | Hdg=%03d | Ammo=%d | Detect=%s | Cargo=%.1f | Base=%s [%s-->%s]", + fsmstate, nelem, Nelem, roe, rot, nTaskTot, nMissions, wpidxCurr, wpuidCurr, wpidxNext, wpuidNext, wpN, wpF, life, speed, speedEx, hdg, ammo, ndetected, cargo, curr, home, dest) + self:I(self.lid..text) + end --- -- Elements --- - + if self.verbose>=2 then local text="Elements:" for i,_element in pairs(self.elements) do - local element=_element --#FLIGHTGROUP.Element + local element=_element --Ops.OpsGroup#OPSGROUP.Element local name=element.name local status=element.status local unit=element.unit local fuel=unit:GetFuel() or 0 local life=unit:GetLifeRelative() or 0 + local lp=unit:GetLife() + local lp0=unit:GetLife0() local parking=element.parking and tostring(element.parking.TerminalID) or "X" - -- Check if element is not dead and we missed an event. - --if life<=0 and element.status~=OPSGROUP.ElementStatus.DEAD and element.status~=OPSGROUP.ElementStatus.INUTERO then - -- self:ElementDead(element) - --end - -- Get ammo. local ammo=self:GetAmmoElement(element) -- Output text for element. - text=text..string.format("\n[%d] %s: status=%s, fuel=%.1f, life=%.1f, guns=%d, rockets=%d, bombs=%d, missiles=%d (AA=%d, AG=%d, AS=%s), parking=%s", - i, name, status, fuel*100, life*100, ammo.Guns, ammo.Rockets, ammo.Bombs, ammo.Missiles, ammo.MissilesAA, ammo.MissilesAG, ammo.MissilesAS, parking) + text=text..string.format("\n[%d] %s: status=%s, fuel=%.1f, life=%.1f [%.1f/%.1f], guns=%d, rockets=%d, bombs=%d, missiles=%d (AA=%d, AG=%d, AS=%s), parking=%s", + i, name, status, fuel*100, life*100, lp, lp0, ammo.Guns, ammo.Rockets, ammo.Bombs, ammo.Missiles, ammo.MissilesAA, ammo.MissilesAG, ammo.MissilesAS, parking) end if #self.elements==0 then text=text.." none!" @@ -132881,7 +149451,9 @@ function FLIGHTGROUP:onafterStatus(From, Event, To) -- Distance travelled --- - if self.verbose>=4 and self:IsAlive() then + if self.verbose>=4 and alive then + + -- TODO: _Check distance travelled. -- Travelled distance since last check. local ds=self.travelds @@ -132897,7 +149469,7 @@ function FLIGHTGROUP:onafterStatus(From, Event, To) local TmaxFuel=math.huge for _,_element in pairs(self.elements) do - local element=_element --#FLIGHTGROUP.Element + local element=_element --Ops.OpsGroup#OPSGROUP.Element -- Get relative fuel of element. local fuel=element.unit:GetFuel() or 0 @@ -132923,25 +149495,24 @@ function FLIGHTGROUP:onafterStatus(From, Event, To) end -- Log outut. - self:I(self.lid..string.format("Travelled ds=%.1f km dt=%.1f s ==> v=%.1f knots. Fuel left for %.1f min", self.traveldist/1000, dt, UTILS.MpsToKnots(v), TmaxFuel/60)) - + self:T(self.lid..string.format("Travelled ds=%.1f km dt=%.1f s ==> v=%.1f knots. Fuel left for %.1f min", self.traveldist/1000, dt, UTILS.MpsToKnots(v), TmaxFuel/60)) + end - --- - -- Tasks & Missions - --- - - self:_PrintTaskAndMissionStatus() - --- -- Fuel State --- + -- TODO: _CheckFuelState() function. + -- Only if group is in air. - if self:IsAlive() and self.group:IsAirborne(true) then + if alive and self.group:IsAirborne(true) then local fuelmin=self:GetFuelMin() + -- Debug info. + self:T2(self.lid..string.format("Fuel state=%d", fuelmin)) + if fuelmin>=self.fuellowthresh then self.fuellow=false end @@ -132960,35 +149531,13 @@ function FLIGHTGROUP:onafterStatus(From, Event, To) if fuelmin See also OPSGROUP ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Flightgroup event function, handling the birth of a unit. --- @param #FLIGHTGROUP self --- @param Core.Event#EVENTDATA EventData Event data. -function FLIGHTGROUP:OnEventBirth(EventData) - - --env.info(string.format("EVENT: Birth for unit %s", tostring(EventData.IniUnitName))) - - -- 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 - - -- Set group. - self.group=self.group or EventData.IniGroup - - if self.respawning then - - local function reset() - self.respawning=nil - end - - -- Reset switch in 1 sec. This should allow all birth events of n>1 groups to have passed. - -- TODO: Can I do this more rigorously? - self:ScheduleOnce(1, reset) - - else - - -- Set homebase if not already set. - if EventData.Place then - self.homebase=self.homebase or EventData.Place - end - - if self.homebase and not self.destbase then - self.destbase=self.homebase - end - - -- Get element. - local element=self:GetElementByName(unitname) - - -- Create element spawned event if not already present. - if not self:_IsElement(unitname) then - element=self:AddElementByName(unitname) - end - - -- Set element to spawned state. - self:T(self.lid..string.format("EVENT: Element %s born at airbase %s==> spawned", element.name, self.homebase and self.homebase:GetName() or "unknown")) - -- This is delayed by a millisec because inAir check for units spawned in air failed (returned false even though the unit was spawned in air). - self:__ElementSpawned(0.0, element) - - end - - end - -end - --- Flightgroup event function handling the crash of a unit. -- @param #FLIGHTGROUP self -- @param Core.Event#EVENTDATA EventData Event data. @@ -133166,6 +149609,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, ? @@ -133177,6 +149628,7 @@ function FLIGHTGROUP:OnEventEngineStartup(EventData) self:ElementEngineOn(element) end end + ]] end end @@ -133189,6 +149641,7 @@ end -- @param #FLIGHTGROUP self -- @param Core.Event#EVENTDATA EventData Event data. function FLIGHTGROUP:OnEventTakeOff(EventData) + self:T3(self.lid.."EVENT: TakeOff") -- Check that this is the right group. if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then @@ -133200,7 +149653,7 @@ function FLIGHTGROUP:OnEventTakeOff(EventData) local element=self:GetElementByName(unitname) if element then - self:T3(self.lid..string.format("EVENT: Element %s took off ==> airborne", element.name)) + self:T2(self.lid..string.format("EVENT: Element %s took off ==> airborne", element.name)) self:ElementTakeoff(element, EventData.Place) end @@ -133253,11 +149706,14 @@ 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 local airbase=self:GetClosestAirbase() - local parking=self:GetParkingSpot(element, 10, airbase) + local parking=self:GetParkingSpot(element, 100, airbase) if airbase and parking then self:ElementArrived(element, airbase, parking) @@ -133267,7 +149723,7 @@ function FLIGHTGROUP:OnEventEngineShutdown(EventData) end else - --self:I(self.lid..string.format("EVENT: Element %s shut down engines but is NOT alive ==> waiting for crash event (==> dead)", element.name)) + --self:T(self.lid..string.format("EVENT: Element %s shut down engines but is NOT alive ==> waiting for crash event (==> dead)", element.name)) end end -- element nil? @@ -133292,7 +149748,7 @@ function FLIGHTGROUP:OnEventCrash(EventData) local element=self:GetElementByName(unitname) if element and element.status~=OPSGROUP.ElementStatus.DEAD then - self:T(self.lid..string.format("EVENT: Element %s crashed ==> destroyed", element.name)) + self:T(self.lid..string.format("EVENT: Element %s crashed ==> destroyed", element.name)) self:ElementDestroyed(element) end @@ -133308,7 +149764,7 @@ function FLIGHTGROUP:OnEventUnitLost(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: Unit %s lost at t=%.3f", EventData.IniUnitName, timer.getTime())) - + local unit=EventData.IniUnit local group=EventData.IniGroup local unitname=EventData.IniUnitName @@ -133320,75 +149776,13 @@ function FLIGHTGROUP:OnEventUnitLost(EventData) self:T(self.lid..string.format("EVENT: Element %s unit lost ==> destroyed t=%.3f", element.name, timer.getTime())) self:ElementDestroyed(element) end - - end - -end - ---- Flightgroup event function handling the crash of a unit. --- @param #FLIGHTGROUP self --- @param Core.Event#EVENTDATA EventData Event data. -function FLIGHTGROUP:OnEventKill(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 - - -- Target name - local targetname=tostring(EventData.TgtUnitName) - - -- Debug info. - self:T2(self.lid..string.format("EVENT: Unit %s killed object %s!", tostring(EventData.IniUnitName), targetname)) - - -- Check if this was a UNIT or STATIC object. - local target=UNIT:FindByName(targetname) - if not target then - target=STATIC:FindByName(targetname, false) - end - - -- Only count UNITS and STATICs (not SCENERY) - if target then - - -- Debug info. - self:T(self.lid..string.format("EVENT: Unit %s killed unit/static %s!", tostring(EventData.IniUnitName), targetname)) - - -- Kill counter. - self.Nkills=self.Nkills+1 - - -- Check if on a mission. - local mission=self:GetMissionCurrent() - if mission then - mission.Nkills=mission.Nkills+1 -- Increase mission kill counter. - end - - end - - end - -end - ---- Flightgroup event function handling the crash of a unit. --- @param #FLIGHTGROUP self --- @param Core.Event#EVENTDATA EventData Event data. -function FLIGHTGROUP:OnEventRemoveUnit(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 - - -- Get element. - local element=self:GetElementByName(unitname) - - if element then - self:T3(self.lid..string.format("EVENT: Element %s removed ==> dead", element.name)) - self:ElementDead(element) - end end end + + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- FSM functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -133398,14 +149792,20 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. 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) - if Element.unit:InAir(true) then + if Element.unit:InAir(not self.isHelo) then -- Setting check because of problems with helos dynamically spawned where inAir WRONGLY returned true if spawned at an airbase or farp! -- Trigger ElementAirborne event. Add a little delay because spawn is also delayed! self:__ElementAirborne(0.11, Element) else @@ -133424,6 +149824,7 @@ function FLIGHTGROUP:onafterElementSpawned(From, Event, To, Element) self:__ElementParking(0.11, Element) end end + end --- On after "ElementParking" event. @@ -133431,25 +149832,31 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. -- @param Wrapper.Airbase#AIRBASE.ParkingSpot Spot Parking Spot. function FLIGHTGROUP:onafterElementParking(From, Event, To, Element, Spot) + + -- Set parking spot. + if Spot then + self:_SetElementParkingAt(Element, Spot) + end + + -- Debug info. self:T(self.lid..string.format("Element parking %s at spot %s", Element.name, Element.parking and tostring(Element.parking.TerminalID) or "N/A")) -- Set element status. self:_UpdateStatus(Element, OPSGROUP.ElementStatus.PARKING) - if Spot then - self:_SetElementParkingAt(Element, Spot) - end - if self:IsTakeoffCold() then -- 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 --- On after "ElementEngineOn" event. @@ -133457,7 +149864,7 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. function FLIGHTGROUP:onafterElementEngineOn(From, Event, To, Element) -- Debug info. @@ -133465,6 +149872,7 @@ function FLIGHTGROUP:onafterElementEngineOn(From, Event, To, Element) -- Set element status. self:_UpdateStatus(Element, OPSGROUP.ElementStatus.ENGINEON) + end --- On after "ElementTaxiing" event. @@ -133472,7 +149880,7 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. function FLIGHTGROUP:onafterElementTaxiing(From, Event, To, Element) -- Get terminal ID. @@ -133486,6 +149894,7 @@ function FLIGHTGROUP:onafterElementTaxiing(From, Event, To, Element) -- Set element status. self:_UpdateStatus(Element, OPSGROUP.ElementStatus.TAXIING) + end --- On after "ElementTakeoff" event. @@ -133493,7 +149902,7 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. -- @param Wrapper.Airbase#AIRBASE airbase The airbase if applicable or nil. function FLIGHTGROUP:onafterElementTakeoff(From, Event, To, Element, airbase) self:T(self.lid..string.format("Element takeoff %s at %s airbase.", Element.name, airbase and airbase:GetName() or "unknown")) @@ -133507,7 +149916,8 @@ function FLIGHTGROUP:onafterElementTakeoff(From, Event, To, Element, airbase) self:_UpdateStatus(Element, OPSGROUP.ElementStatus.TAKEOFF, airbase) -- Trigger element airborne event. - self:__ElementAirborne(2, Element) + self:__ElementAirborne(0.01, Element) + end --- On after "ElementAirborne" event. @@ -133515,12 +149925,15 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. function FLIGHTGROUP:onafterElementAirborne(From, Event, To, Element) + + -- Debug info. self:T2(self.lid..string.format("Element airborne %s", Element.name)) -- Set element status. self:_UpdateStatus(Element, OPSGROUP.ElementStatus.AIRBORNE) + end --- On after "ElementLanded" event. @@ -133528,31 +149941,52 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. -- @param Wrapper.Airbase#AIRBASE airbase The airbase if applicable or nil. function FLIGHTGROUP:onafterElementLanded(From, Event, To, Element, airbase) + + -- Debug info. self:T2(self.lid..string.format("Element landed %s at %s airbase", Element.name, airbase and airbase:GetName() or "unknown")) - if self.despawnAfterLanding then - - -- Despawn the element. - self:DespawnElement(Element) - - else + -- Set element status. + self:_UpdateStatus(Element, OPSGROUP.ElementStatus.LANDED, airbase) - -- Helos with skids land directly on parking spots. - if self.ishelo then - - local Spot=self:GetParkingSpot(Element, 10, airbase) - + -- Helos with skids land directly on parking spots. + if self.isHelo then + + local Spot=self:GetParkingSpot(Element, 10, airbase) + + if Spot then self:_SetElementParkingAt(Element, Spot) - + self:_UpdateStatus(Element, OPSGROUP.ElementStatus.ARRIVED) end - - -- Set element status. - self:_UpdateStatus(Element, OPSGROUP.ElementStatus.LANDED, airbase) + + end + + -- Despawn after landing. + if self.despawnAfterLanding then - end + if self.legion then + + if airbase and self.legion.airbase and airbase.AirbaseName==self.legion.airbase.AirbaseName then + + if self:IsLanded() then + -- Everybody landed ==> Return to legion. Will despawn the last one. + self:ReturnToLegion() + else + -- Despawn the element. + self:DespawnElement(Element) + end + + end + + else + + -- Despawn the element. + self:DespawnElement(Element) + + end + end end --- On after "ElementArrived" event. @@ -133560,12 +149994,13 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. -- @param Wrapper.Airbase#AIRBASE airbase The airbase, where the element arrived. -- @param Wrapper.Airbase#AIRBASE.ParkingSpot Parking The Parking spot the element has. function FLIGHTGROUP:onafterElementArrived(From, Event, To, Element, airbase, Parking) self:T(self.lid..string.format("Element arrived %s at %s airbase using parking spot %d", Element.name, airbase and airbase:GetName() or "unknown", Parking and Parking.TerminalID or -99)) + -- Set element parking. self:_SetElementParkingAt(Element, Parking) -- Set element status. @@ -133577,12 +150012,12 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. function FLIGHTGROUP:onafterElementDestroyed(From, Event, To, Element) -- Call OPSGROUP function. self:GetParent(self).onafterElementDestroyed(self, From, Event, To, Element) - + end --- On after "ElementDead" event. @@ -133590,33 +150025,72 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #FLIGHTGROUP.Element Element The flight group element. +-- @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 - + end ---- On after "Spawned" event. Sets the template, initializes the waypoints. +--- On after "Spawned" event. -- @param #FLIGHTGROUP self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. function FLIGHTGROUP:onafterSpawned(From, Event, To) self:T(self.lid..string.format("Flight spawned")) + + -- Debug info. + if self.verbose>=1 then + local text=string.format("Initialized Flight Group %s:\n", self.groupname) + text=text..string.format("Unit type = %s\n", self.actype) + text=text..string.format("Speed max = %.1f Knots\n", UTILS.KmphToKnots(self.speedMax)) + text=text..string.format("Range max = %.1f km\n", self.rangemax/1000) + text=text..string.format("Ceiling = %.1f feet\n", UTILS.MetersToFeet(self.ceiling)) + text=text..string.format("Weight = %.1f kg\n", self:GetWeightTotal()) + text=text..string.format("Cargo bay = %.1f kg\n", self:GetFreeCargobay()) + text=text..string.format("Tanker type = %s\n", tostring(self.tankertype)) + text=text..string.format("Refuel type = %s\n", tostring(self.refueltype)) + text=text..string.format("AI = %s\n", tostring(self.isAI)) + text=text..string.format("Has EPLRS = %s\n", tostring(self.isEPLRS)) + text=text..string.format("Helicopter = %s\n", tostring(self.isHelo)) + text=text..string.format("Elements = %d\n", #self.elements) + text=text..string.format("Waypoints = %d\n", #self.waypoints) + text=text..string.format("Radio = %.1f MHz %s %s\n", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu), tostring(self.radio.On)) + text=text..string.format("Ammo = %d (G=%d/R=%d/B=%d/M=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Bombs, self.ammo.Missiles) + text=text..string.format("FSM state = %s\n", self:GetState()) + text=text..string.format("Is alive = %s\n", tostring(self.group:IsAlive())) + text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated())) + text=text..string.format("Uncontrolled = %s\n", tostring(self:IsUncontrolled())) + text=text..string.format("Start Air = %s\n", tostring(self:IsTakeoffAir())) + 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 - -- Update position. + -- Update position. self:_UpdatePosition() + -- Not dead or destroyed yet. + self.isDead=false + self.isDestroyed=false + if self.isAI then -- Set ROE. @@ -133624,41 +150098,60 @@ function FLIGHTGROUP:onafterSpawned(From, Event, To) -- Set ROT. self:SwitchROT(self.option.ROT) + + -- 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) - + -- Set TACAN beacon. self:_SwitchTACAN() - + -- Set radio freq and modu. if self.radioDefault then self:SwitchRadio() else self:SetDefaultRadio(self.radio.Freq, self.radio.Modu, self.radio.On) end - + -- Set callsign. if self.callsignDefault then self:SwitchCallsign(self.callsignDefault.NumberSquad, self.callsignDefault.NumberGroup) else self:SetDefaultCallsign(self.callsign.NumberSquad, self.callsign.NumberGroup) end - + -- TODO: make this input. self:GetGroup():SetOption(AI.Option.Air.id.PROHIBIT_JETT, true) self:GetGroup():SetOption(AI.Option.Air.id.PROHIBIT_AB, true) -- Does not seem to work. AI still used the after burner. self:GetGroup():SetOption(AI.Option.Air.id.RTB_ON_BINGO, false) - --self.group:SetOption(AI.Option.Air.id.RADAR_USING, AI.Option.Air.val.RADAR_USING.FOR_CONTINUOUS_SEARCH) + --self.group:SetOption(AI.Option.Air.id.RADAR_USING, AI.Option.Air.val.RADAR_USING.FOR_CONTINUOUS_SEARCH) -- Update route. self:__UpdateRoute(-0.5) else - - -- 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 @@ -133669,11 +150162,22 @@ end -- @param #string Event Event. -- @param #string To To state. function FLIGHTGROUP:onafterParking(From, Event, To) - self:T(self.lid..string.format("Flight is parking")) - - local airbase=self:GetClosestAirbase() --self.group:GetCoordinate():GetClosestAirbase() + -- Get closest airbase + local airbase=self:GetClosestAirbase() local airbasename=airbase:GetName() or "unknown" + + -- Debug info + self:T(self.lid..string.format("Flight is parking at airbase %s", airbasename)) + + -- Set current airbase. + self.currbase=airbase + + -- Set homebase to current airbase if not defined yet. + -- This is necessary, e.g, when flights are spawned at an airbase because they do not have a takeoff waypoint. + if not self.homebase then + self.homebase=airbase + end -- Parking time stamp. self.Tparking=timer.getAbsTime() @@ -133683,7 +150187,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 @@ -133691,12 +150195,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 @@ -133711,10 +150213,7 @@ function FLIGHTGROUP:onafterTaxiing(From, Event, To) -- Parking over. self.Tparking=nil - -- TODO: need a better check for the airbase. - local airbase=self:GetClosestAirbase() --self.group:GetCoordinate():GetClosestAirbase(nil, self.group:GetCoalition()) - - 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 @@ -133723,8 +150222,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 @@ -133756,11 +150253,45 @@ end function FLIGHTGROUP:onafterAirborne(From, Event, To) self:T(self.lid..string.format("Flight airborne")) + -- No current airbase any more. + self.currbase=nil + + -- Cruising. + self:__Cruise(-0.01) + +end + +--- On after "Cruising" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterCruise(From, Event, To) + self:T(self.lid..string.format("Flight cruising")) + + -- Not waiting anymore. + self.Twaiting=nil + self.dTwait=nil + if self.isAI then - self:_CheckGroupDone(1) + + --- + -- AI + --- + + -- Check group Done. + self:_CheckGroupDone(nil, 120) + else - self:_UpdateMenu() + + --- + -- CLIENT + --- + + --self:_UpdateMenu(0.1) + end + end --- On after "Landing" event. @@ -133771,8 +150302,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 @@ -133785,11 +150331,11 @@ 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 - + end --- On after "LandedAt" event. @@ -133798,9 +150344,16 @@ end -- @param #string Event Event. -- @param #string To To state. function FLIGHTGROUP:onafterLandedAt(From, Event, To) - self:T(self.lid..string.format("Flight landed at")) -end + self:T(self.lid..string.format("Flight landed at")) + -- Trigger (un-)loading process. + if self:IsPickingup() then + self:__Loading(-1) + elseif self:IsTransporting() then + self:__Unloading(-1) + end + +end --- On after "Arrived" event. -- @param #FLIGHTGROUP self @@ -133815,9 +150368,103 @@ function FLIGHTGROUP:onafterArrived(From, Event, To) -- Add flight to arrived queue. self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.ARRIVED) end + + if not self.isAI then + -- Player landed. No despawn. + return + end + + --TODO: Check that current base is airwing base. + local airwing=self:GetAirWing() --airwing:GetAirbaseName()==self.currbase:GetName() - -- Despawn in 5 min. - if not self.airwing then + -- Check what to do. + if airwing and not (self:IsPickingup() or self:IsTransporting()) then + + -- Debug info. + self:T(self.lid..string.format("Airwing asset group %s arrived ==> Adding asset back to stock of airwing %s", self.groupname, airwing.alias)) + + -- Add the asset back to the airwing. + --airwing:AddAsset(self.group, 1) + self:ReturnToLegion(1) + + elseif self.isLandingAtAirbase then + + local Template=UTILS.DeepCopy(self.template) --DCS#Template + + -- No late activation. + self.isLateActivated=false + Template.lateActivation=self.isLateActivated + + -- Spawn in uncontrolled state. + self.isUncontrolled=true + Template.uncontrolled=self.isUncontrolled + + -- First waypoint of the group. + local SpawnPoint=Template.route.points[1] + + -- These are only for ships and FARPS. + SpawnPoint.linkUnit = nil + SpawnPoint.helipadId = nil + SpawnPoint.airdromeId = nil + + -- Airbase. + local airbase=self.isLandingAtAirbase --Wrapper.Airbase#AIRBASE + + -- Get airbase ID and category. + local AirbaseID = airbase:GetID() + + -- Set airdromeId. + if airbase:IsShip() then + SpawnPoint.linkUnit = AirbaseID + SpawnPoint.helipadId = AirbaseID + elseif airbase:IsHelipad() then + SpawnPoint.linkUnit = AirbaseID + SpawnPoint.helipadId = AirbaseID + elseif airbase:IsAirdrome() then + SpawnPoint.airdromeId = AirbaseID + end + + -- Set waypoint type/action. + SpawnPoint.alt = 0 + SpawnPoint.type = COORDINATE.WaypointType.TakeOffParking + SpawnPoint.action = COORDINATE.WaypointAction.FromParkingArea + + local units=Template.units + + for i=#units,1,-1 do + local unit=units[i] + local element=self:GetElementByName(unit.name) + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + unit.parking=element.parking and element.parking.TerminalID or nil + unit.parking_id=nil + local vec3=element.unit:GetVec3() + local heading=element.unit:GetHeading() + unit.x=vec3.x + unit.y=vec3.z + unit.alt=vec3.y + unit.heading=math.rad(heading) + unit.psi=-unit.heading + else + table.remove(units, i) + end + end + + -- Respawn with this template. + self:_Respawn(0, Template) + + -- Reset. + self.isLandingAtAirbase=nil + + -- Init (un-)loading process. + if self:IsPickingup() then + self:__Loading(-1) + elseif self:IsTransporting() then + self:__Unloading(-1) + end + + else + -- Depawn after 5 min. Important to trigger dead events before DCS despawns on its own without any notification. + self:T(self.lid..string.format("Despawning group in 5 minutes after arrival!")) self:Despawn(5*60) end end @@ -133834,22 +150481,10 @@ function FLIGHTGROUP:onafterDead(From, Event, To) self.flightcontrol:_RemoveFlight(self) self.flightcontrol=nil end - - if self.Ndestroyed==#self.elements then - if self.squadron then - -- All elements were destroyed ==> Asset group is gone. - self.squadron:DelGroup(self.groupname) - end - else - if self.airwing then - -- Not all assets were destroyed (despawn) ==> Add asset back to airwing. - self.airwing:AddAsset(self.group, 1) - end - end -- Call OPSGROUP function. self:GetParent(self).onafterDead(self, From, Event, To) - + end @@ -133858,61 +150493,93 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #number n Waypoint number. +-- @param #number n Next waypoint index. Default is the one coming after that one that has been passed last. +-- @param #number N Waypoint Max waypoint index to be included in the route. Default is the final waypoint. -- @return #boolean Transision allowed? -function FLIGHTGROUP:onbeforeUpdateRoute(From, Event, To, n) +function FLIGHTGROUP:onbeforeUpdateRoute(From, Event, To, n, N) -- Is transition allowed? We assume yes until proven otherwise. local allowed=true local trepeat=nil - if self:IsAlive() then -- and (self:IsAirborne() or self:IsWaiting() or self:IsInbound() or self:IsHolding()) then + if self:IsAlive() then -- Alive & Airborne ==> Update route possible. self:T3(self.lid.."Update route possible. Group is ALIVE") elseif self:IsDead() then -- Group is dead! No more updates. - self:E(self.lid.."Update route denied. Group is DEAD!") + self:T(self.lid.."Update route denied. Group is DEAD!") allowed=false + elseif self:IsInUtero() then + self:T(self.lid.."Update route denied. Group is INUTERO!") + allowed=false else -- Not airborne yet. Try again in 5 sec. self:T(self.lid.."Update route denied ==> checking back in 5 sec") trepeat=-5 allowed=false end + + -- Check if group is uncontrolled. If so, the mission task cannot be set yet! + if allowed and self:IsUncontrolled() then + self:T(self.lid.."Update route denied. Group is UNCONTROLLED!") + local mission=self:GetMissionCurrent() + if mission and mission.type==AUFTRAG.Type.ALERT5 then + trepeat=nil --Alert 5 is just waiting for the real mission. No need to try to update the route. + else + trepeat=-5 + end + allowed=false + end + -- Requested waypoint index <1. Something is seriously wrong here! if n and n<1 then - self:E(self.lid.."Update route denied because waypoint n<1!") + self:T(self.lid.."Update route denied because waypoint n<1!") allowed=false end + -- No current waypoint. Something is serously wrong! if not self.currentwp then - self:E(self.lid.."Update route denied because self.currentwp=nil!") + self:T(self.lid.."Update route denied because self.currentwp=nil!") allowed=false end - local N=n or self.currentwp+1 - if not N or N<1 then - self:E(self.lid.."Update route denied because N=nil or N<1") + local Nn=n or self.currentwp+1 + if not Nn or Nn<1 then + self:T(self.lid.."Update route denied because N=nil or N<1") trepeat=-5 allowed=false end + -- Check for a current task. if self.taskcurrent>0 then - - --local task=self:GetTaskCurrent() + + -- Get the current task. Must not be executing already. local task=self:GetTaskByID(self.taskcurrent) - + if task then if task.dcstask.id=="PatrolZone" then - -- For patrol zone, we need to allow the update. + -- For patrol zone, we need to allow the update as we insert new waypoints. + self:T2(self.lid.."Allowing update route for Task: PatrolZone") + elseif task.dcstask.id=="ReconMission" then + -- For recon missions, we need to allow the update as we insert new waypoints. + self:T2(self.lid.."Allowing update route for Task: ReconMission") + elseif task.dcstask.id=="Hover" then + -- For recon missions, we need to allow the update as we insert new waypoints. + self:T2(self.lid.."Allowing update route for Task: Hover") + elseif task.dcstask.id==AUFTRAG.SpecialTask.RELOCATECOHORT then + -- For relocate + self:T2(self.lid.."Allowing update route for Task: Relocate Cohort") + elseif task.description and task.description=="Task_Land_At" then + -- We allow this + self:T2(self.lid.."Allowing update route for Task: Task_Land_At") else local taskname=task and task.description or "No description" - self:E(self.lid..string.format("WARNING: Update route denied because taskcurrent=%d>0! Task description = %s", self.taskcurrent, tostring(taskname))) + self:T(self.lid..string.format("WARNING: Update route denied because taskcurrent=%d>0! Task description = %s", self.taskcurrent, tostring(taskname))) allowed=false end else - -- Now this can happen, if we directly use TaskExecute as the task is not in the task queue and cannot be removed. - self:T(self.lid..string.format("WARNING: before update route taskcurrent=%d>0 but no task?!", self.taskcurrent)) + -- Now this can happen, if we directly use TaskExecute as the task is not in the task queue and cannot be removed. Therefore, also directly executed tasks should be added to the queue! + self:T(self.lid..string.format("WARNING: before update route taskcurrent=%d (>0!) but no task?!", self.taskcurrent)) -- Anyhow, a task is running so we do not allow to update the route! allowed=false end @@ -133928,8 +150595,9 @@ function FLIGHTGROUP:onbeforeUpdateRoute(From, Event, To, n) end -- Debug info. - self:T2(self.lid..string.format("Onbefore Updateroute allowed=%s state=%s repeat in %s", tostring(allowed), self:GetState(), tostring(trepeat))) + self:T2(self.lid..string.format("Onbefore Updateroute in state %s: allowed=%s (repeat in %s)", self:GetState(), tostring(allowed), tostring(trepeat))) + -- Try again? if trepeat then self:__UpdateRoute(trepeat, n) end @@ -133942,14 +150610,16 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #number n Waypoint number. Default is next waypoint. -function FLIGHTGROUP:onafterUpdateRoute(From, Event, To, n) +-- @param #number n Next waypoint index. Default is the one coming after that one that has been passed last. +-- @param #number N Waypoint Max waypoint index to be included in the route. Default is the final waypoint. +function FLIGHTGROUP:onafterUpdateRoute(From, Event, To, n, N) -- Update route from this waypoint number onwards. n=n or self.currentwp+1 - -- Update waypoint tasks, i.e. inject WP tasks into waypoint table. - self:_UpdateWaypointTasks(n) + -- Max index. + N=N or #self.waypoints + N=math.min(N, #self.waypoints) -- Waypoints. local wp={} @@ -133957,22 +150627,29 @@ function FLIGHTGROUP:onafterUpdateRoute(From, Event, To, n) -- Current velocity. local speed=self.group and self.group:GetVelocityKMH() or 100 + -- Waypoint type. + local waypointType=COORDINATE.WaypointType.TurningPoint + local waypointAction=COORDINATE.WaypointAction.TurningPoint + if self:IsLanded() or self:IsLandedAt() or self:IsAirborne()==false then + -- Had some issues with passing waypoint function of the next WP called too ealy when the type is TurningPoint. Setting it to TakeOff solved it! + waypointType=COORDINATE.WaypointType.TakeOff + --waypointType=COORDINATE.WaypointType.TakeOffGroundHot + --waypointAction=COORDINATE.WaypointAction.FromGroundAreaHot + end + -- Set current waypoint or we get problem that the _PassingWaypoint function is triggered too early, i.e. right now and not when passing the next WP. - local current=self.group:GetCoordinate():WaypointAir(COORDINATE.WaypointAltType.BARO, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, speed, true, nil, {}, "Current") + local current=self:GetCoordinate():WaypointAir(COORDINATE.WaypointAltType.BARO, waypointType, waypointAction, speed, true, nil, {}, "Current") table.insert(wp, current) - - local Nwp=self.waypoints and #self.waypoints or 0 -- Add remaining waypoints to route. - for i=n, Nwp do + for i=n, N do table.insert(wp, self.waypoints[i]) end -- Debug info. local hb=self.homebase and self.homebase:GetName() or "unknown" local db=self.destbase and self.destbase:GetName() or "unknown" - self:T(self.lid..string.format("Updating route for WP #%d-%d homebase=%s destination=%s", n, #wp, hb, db)) - + self:T(self.lid..string.format("Updating route for WP #%d-%d [%s], homebase=%s destination=%s", n, #wp, self:GetState(), hb, db)) if #wp>1 then @@ -133984,7 +150661,7 @@ function FLIGHTGROUP:onafterUpdateRoute(From, Event, To, n) --- -- No waypoints left --- - + if self:IsAirborne() then self:T(self.lid.."No waypoints left ==> CheckGroupDone") self:_CheckGroupDone() @@ -133994,37 +150671,17 @@ function FLIGHTGROUP:onafterUpdateRoute(From, Event, To, n) end ---- On after "Respawn" event. --- @param #FLIGHTGROUP self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. --- @param #table Template The template used to respawn the group. -function FLIGHTGROUP:onafterRespawn(From, Event, To, Template) - - self:T(self.lid.."Respawning group!") - - local template=UTILS.DeepCopy(Template or self.template) - - if self.group and self.group:InAir() then - template.lateActivation=false - self.respawning=true - self.group=self.group:Respawn(template) - end - -end - --- On after "OutOfMissilesAA" event. -- @param #FLIGHTGROUP self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. function FLIGHTGROUP:onafterOutOfMissilesAA(From, Event, To) - self:I(self.lid.."Group is out of AA Missiles!") + self:T(self.lid.."Group is out of AA Missiles!") if self.outofAAMrtb then -- Back to destination or home. local airbase=self.destbase or self.homebase - self:__RTB(-5,airbase) + self:__RTB(-5, airbase) end end @@ -134034,11 +150691,11 @@ end -- @param #string Event Event. -- @param #string To To state. function FLIGHTGROUP:onafterOutOfMissilesAG(From, Event, To) - self:I(self.lid.."Group is out of AG Missiles!") + self:T(self.lid.."Group is out of AG Missiles!") if self.outofAGMrtb then -- Back to destination or home. local airbase=self.destbase or self.homebase - self:__RTB(-5,airbase) + self:__RTB(-5, airbase) end end @@ -134052,51 +150709,99 @@ end -- -- @param #FLIGHTGROUP self -- @param #number delay Delay in seconds. -function FLIGHTGROUP:_CheckGroupDone(delay) +-- @param #number waittime Time to wait if group is done. +function FLIGHTGROUP:_CheckGroupDone(delay, waittime) + + -- FSM state. + local fsmstate=self:GetState() if self:IsAlive() and self.isAI then if delay and delay>0 then + -- Debug info. + self:T(self.lid..string.format("Check FLIGHTGROUP [state=%s] done in %.3f seconds... (t=%.4f)", fsmstate, delay, timer.getTime())) + -- Delayed call. self:ScheduleOnce(delay, FLIGHTGROUP._CheckGroupDone, self) else - -- First check if there is a paused mission that - if self.missionpaused then - self:UnpauseMission() + -- Debug info. + self:T(self.lid..string.format("Check FLIGHTGROUP [state=%s] done? (t=%.4f)", fsmstate, timer.getTime())) + + -- Group is currently engaging. + if self:IsEngaging() then + self:T(self.lid.."Engaging! Group NOT done...") return end - -- Group is currently engaging. - if self:IsEngaging() then - 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() + + -- 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 + + -- Group is ordered to land at an airbase. + if self.isLandingAtAirbase then + self:T(self.lid..string.format("Landing at airbase %s! Group NOT done...", self.isLandingAtAirbase:GetName())) + return + end + + -- Group is waiting. + if self:IsWaiting() then + self:T(self.lid.."Waiting! Group NOT done...") + return + end + + -- Debug info. + self:T(self.lid..string.format("Remaining (final=%s): missions=%d, tasks=%d, transports=%d", tostring(self.passedfinalwp), nMissions, nTasks, nTransports)) + -- Final waypoint passed? - if self.passedfinalwp then + -- Or next waypoint index is the first waypoint. Could be that the group was on a mission and the mission waypoints were deleted. then the final waypoint is FALSE but no real waypoint left. + -- Since we do not do ad infinitum, this leads to a rapid oscillation between UpdateRoute and CheckGroupDone! + if self:HasPassedFinalWaypoint() or self:GetWaypointIndexNext()==1 then + + --- + -- Final Waypoint PASSED + --- -- Got current mission or task? - if self.currentmission==nil and self.taskcurrent==0 then + if self.currentmission==nil and self.taskcurrent==0 and (self.cargoTransport==nil or self.cargoTransport:GetCarrierTransportStatus(self)==OPSTRANSPORT.Status.DELIVERED) then -- Number of remaining tasks/missions? - if nTasks==0 and nMissions==0 then - + if nTasks==0 and nMissions==0 and nTransports==0 then + local destbase=self.destbase or self.homebase local destzone=self.destzone or self.homezone -- Send flight to destination. - if destbase then - self:T(self.lid.."Passed Final WP and No current and/or future missions/task ==> RTB!") - self:__RTB(-3, destbase) + if waittime then + self:T(self.lid..string.format("Passed Final WP and No current and/or future missions/tasks/transports. Waittime given ==> Waiting for %d sec!", waittime)) + self:Wait(waittime) + elseif destbase then + if self.currbase and self.currbase.AirbaseName==destbase.AirbaseName and self:IsParking() then + self:T(self.lid.."Passed Final WP and No current and/or future missions/tasks/transports AND parking at destination airbase ==> Arrived!") + self:Arrived() + else + self:T(self.lid.."Passed Final WP and No current and/or future missions/tasks/transports ==> RTB!") + self:__RTB(-0.1, destbase) + end elseif destzone then - self:T(self.lid.."Passed Final WP and No current and/or future missions/task ==> RTZ!") - self:__RTZ(-3, destzone) + self:T(self.lid.."Passed Final WP and No current and/or future missions/tasks/transports ==> RTZ!") + self:__RTZ(-0.1, destzone) else self:T(self.lid.."Passed Final WP and NO Tasks/Missions left. No DestBase or DestZone ==> Wait!") self:__Wait(-1) @@ -134110,8 +150815,17 @@ function FLIGHTGROUP:_CheckGroupDone(delay) self:T(self.lid..string.format("Passed Final WP but still have current Task (#%s) or Mission (#%s) left to do", tostring(self.taskcurrent), tostring(self.currentmission))) end else - self:T(self.lid..string.format("Flight (status=%s) did NOT pass the final waypoint yet ==> update route", self:GetState())) - self:__UpdateRoute(-1) + + --- + -- Final Waypoint NOT PASSED + --- + + -- Debug info. + self:T(self.lid..string.format("Flight (status=%s) did NOT pass the final waypoint yet ==> update route in -0.01 sec", self:GetState())) + + -- Update route. + self:__UpdateRoute(-0.01) + end end @@ -134129,57 +150843,81 @@ 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 local Tsuspend=nil if airbase==nil then - self:E(self.lid.."ERROR: Airbase is nil in RTB() call!") + self:T(self.lid.."ERROR: Airbase is nil in RTB() call!") allowed=false end -- Check that coaliton is okay. We allow same (blue=blue, red=red) or landing on neutral bases. if airbase and airbase:GetCoalition()~=self.group:GetCoalition() and airbase:GetCoalition()>0 then - self:E(self.lid..string.format("ERROR: Wrong airbase coalition %d in RTB() call! We allow only same as group %d or neutral airbases 0.", airbase:GetCoalition(), self.group:GetCoalition())) - allowed=false + self:T(self.lid..string.format("ERROR: Wrong airbase coalition %d in RTB() call! We allow only same as group %d or neutral airbases 0", airbase:GetCoalition(), self.group:GetCoalition())) + return false + end + + if self.currbase and self.currbase:GetName()==airbase:GetName() then + self:T(self.lid.."WARNING: Currbase is already same as RTB airbase. RTB canceled!") + return false + end + + -- Check if the group has landed at an airbase. If so, we lost control and RTBing is not possible (only after a respawn). + if self:IsLanded() then + self:T(self.lid.."WARNING: Flight has already landed. RTB canceled!") + return false end if not self.group:IsAirborne(true) then -- this should really not happen, either the AUFTRAG is cancelled before the group was airborne or it is stuck at the ground for some reason - self:I(self.lid..string.format("WARNING: Group is not AIRBORNE ==> RTB event is suspended for 20 sec.")) + self:T(self.lid..string.format("WARNING: Group [%s] is not AIRBORNE ==> RTB event is suspended for 20 sec", self:GetState())) allowed=false Tsuspend=-20 local groupspeed = self.group:GetVelocityMPS() - if groupspeed <= 1 then self.RTBRecallCount = self.RTBRecallCount+1 end - if self.RTBRecallCount > 6 then + if groupspeed<=1 and not self:IsParking() then + self.RTBRecallCount = self.RTBRecallCount+1 + end + if self.RTBRecallCount>6 then + self:T(self.lid..string.format("WARNING: Group [%s] is not moving and was called RTB %d times. Assuming a problem and despawning!", self:GetState(), self.RTBRecallCount)) + self.RTBRecallCount=0 self:Despawn(5) + return end end - + -- Only if fuel is not low or critical. - if not (self:IsFuelLow() or self:IsFuelCritical()) then + if self:IsFuelGood() then -- Check if there are remaining tasks. local Ntot,Nsched, Nwp=self:CountRemainingTasks() if self.taskcurrent>0 then - self:I(self.lid..string.format("WARNING: Got current task ==> RTB event is suspended for 10 sec.")) + self:T(self.lid..string.format("WARNING: Got current task ==> RTB event is suspended for 10 sec")) Tsuspend=-10 allowed=false end if Nsched>0 then - self:I(self.lid..string.format("WARNING: Still got %d SCHEDULED tasks in the queue ==> RTB event is suspended for 10 sec.", Nsched)) + self:T(self.lid..string.format("WARNING: Still got %d SCHEDULED tasks in the queue ==> RTB event is suspended for 10 sec", Nsched)) Tsuspend=-10 allowed=false end if Nwp>0 then - self:I(self.lid..string.format("WARNING: Still got %d WAYPOINT tasks in the queue ==> RTB event is suspended for 10 sec.", Nwp)) + self:T(self.lid..string.format("WARNING: Still got %d WAYPOINT tasks in the queue ==> RTB event is suspended for 10 sec", Nwp)) Tsuspend=-10 allowed=false end + + if self.Twaiting and self.dTwait then + self:T(self.lid..string.format("WARNING: Group is Waiting for a specific duration ==> RTB event is canceled", Nwp)) + allowed=false + end end @@ -134190,7 +150928,7 @@ function FLIGHTGROUP:onbeforeRTB(From, Event, To, airbase, SpeedTo, SpeedHold) return allowed else - self:E(self.lid.."WARNING: Group is not alive! RTB call not allowed.") + self:T(self.lid.."WARNING: Group is not alive! RTB call not allowed.") return false end @@ -134213,52 +150951,162 @@ function FLIGHTGROUP:onafterRTB(From, Event, To, airbase, SpeedTo, SpeedHold, Sp -- Set the destination base. self.destbase=airbase - -- Clear holding time in any case. - self.Tholding=nil - -- Cancel all missions. for _,_mission in pairs(self.missionqueue) do local mission=_mission --Ops.Auftrag#AUFTRAG local mystatus=mission:GetGroupStatus(self) - + -- Check if mission is already over! if not (mystatus==AUFTRAG.GroupStatus.DONE or mystatus==AUFTRAG.GroupStatus.CANCELLED) then local text=string.format("Canceling mission %s in state=%s", mission.name, mission.status) self:T(self.lid..text) self:MissionCancel(mission) end - + end + -- Land at airbase. + self:_LandAtAirbase(airbase, SpeedTo, SpeedHold, SpeedLand) + +end + + +--- On before "LandAtAirbase" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Airbase#AIRBASE airbase The airbase to hold at. +function FLIGHTGROUP:onbeforeLandAtAirbase(From, Event, To, airbase) + + if self:IsAlive() then + + local allowed=true + local Tsuspend=nil + + if airbase==nil then + self:T(self.lid.."ERROR: Airbase is nil in LandAtAirase() call!") + allowed=false + end + + -- Check that coaliton is okay. We allow same (blue=blue, red=red) or landing on neutral bases. + if airbase and airbase:GetCoalition()~=self.group:GetCoalition() and airbase:GetCoalition()>0 then + self:T(self.lid..string.format("ERROR: Wrong airbase coalition %d in LandAtAirbase() call! We allow only same as group %d or neutral airbases 0", airbase:GetCoalition(), self.group:GetCoalition())) + return false + end + + if self.currbase and self.currbase:GetName()==airbase:GetName() then + self:T(self.lid.."WARNING: Currbase is already same as LandAtAirbase airbase. LandAtAirbase canceled!") + return false + end + + -- Check if the group has landed at an airbase. If so, we lost control and RTBing is not possible (only after a respawn). + if self:IsLanded() then + self:T(self.lid.."WARNING: Flight has already landed. LandAtAirbase canceled!") + return false + end + + if self:IsParking() then + allowed=false + Tsuspend=-30 + self:T(self.lid.."WARNING: Flight is parking. LandAtAirbase call delayed by 30 sec") + elseif self:IsTaxiing() then + allowed=false + Tsuspend=-1 + self:T(self.lid.."WARNING: Flight is parking. LandAtAirbase call delayed by 1 sec") + end + + if Tsuspend and not allowed then + self:__LandAtAirbase(Tsuspend, airbase) + end + + return allowed + else + self:T(self.lid.."WARNING: Group is not alive! LandAtAirbase call not allowed") + return false + end + +end + + +--- On after "LandAtAirbase" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Airbase#AIRBASE airbase The airbase to hold at. +function FLIGHTGROUP:onafterLandAtAirbase(From, Event, To, airbase) + + self.isLandingAtAirbase=airbase + + self:_LandAtAirbase(airbase) + +end + +--- Land at an airbase. +-- @param #FLIGHTGROUP self +-- @param Wrapper.Airbase#AIRBASE airbase Airbase where the group shall land. +-- @param #number SpeedTo Speed used for travelling from current position to holding point in knots. +-- @param #number SpeedHold Holding speed in knots. +-- @param #number SpeedLand Landing speed in knots. Default 170 kts. +function FLIGHTGROUP:_LandAtAirbase(airbase, SpeedTo, SpeedHold, SpeedLand) + + -- Set current airbase. + self.currbase=airbase + + -- Passed final waypoint! + self:_PassedFinalWaypoint(true, "_LandAtAirbase") + + -- Not waiting any more. + self.Twaiting=nil + self.dTwait=nil + -- Defaults: SpeedTo=SpeedTo or UTILS.KmphToKnots(self.speedCruise) - SpeedHold=SpeedHold or (self.ishelo and 80 or 250) - SpeedLand=SpeedLand or (self.ishelo and 40 or 170) + SpeedHold=SpeedHold or (self.isHelo and 80 or 250) + SpeedLand=SpeedLand or (self.isHelo and 40 or 170) + + -- Clear holding time in any case. + self.Tholding=nil -- Debug message. local text=string.format("Flight group set to hold at airbase %s. SpeedTo=%d, SpeedHold=%d, SpeedLand=%d", airbase:GetName(), SpeedTo, SpeedHold, SpeedLand) self:T(self.lid..text) - local althold=self.ishelo and 1000+math.random(10)*100 or math.random(4,10)*1000 + -- Holding altitude. + local althold=self.isHelo and 1000+math.random(10)*100 or math.random(4,10)*1000 -- Holding points. - local c0=self.group:GetCoordinate() + local c0=self:GetCoordinate() local p0=airbase:GetZone():GetRandomCoordinate():SetAltitude(UTILS.FeetToMeters(althold)) local p1=nil local wpap=nil -- 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 self.Debug 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. @@ -134266,21 +151114,43 @@ function FLIGHTGROUP:onafterRTB(From, Event, To, airbase, SpeedTo, SpeedHold, Sp -- 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. + local c1=c0:GetIntermediateCoordinate(p0, 0.25):SetAltitude(self.altitudeCruise, true) + local c2=c0:GetIntermediateCoordinate(p0, 0.75):SetAltitude(self.altitudeCruise, true) -- Altitude above ground for a glide slope of 3 degrees. - local x1=self.ishelo and UTILS.NMToMeters(5.0) or UTILS.NMToMeters(10) - local x2=self.ishelo and UTILS.NMToMeters(2.5) or UTILS.NMToMeters(5) + local x1=self.isHelo and UTILS.NMToMeters(2.0) or UTILS.NMToMeters(10) + local x2=self.isHelo and UTILS.NMToMeters(1.0) or UTILS.NMToMeters(5) local alpha=math.rad(3) local h1=x1*math.tan(alpha) local h2=x2*math.tan(alpha) - local runway=airbase:GetActiveRunway() + -- Get active runway. + local runway=airbase:GetActiveRunwayLanding() -- Set holding flag to 0=false. self.flaghold:Set(0) - local holdtime=5*60 + -- Set holding time. + local holdtime=2*60 if fc or self.airboss then holdtime=nil end @@ -134296,27 +151166,34 @@ function FLIGHTGROUP:onafterRTB(From, Event, To, airbase, SpeedTo, SpeedHold, Sp -- Waypoints from current position to holding point. local wp={} - wp[#wp+1]=c0:WaypointAir(nil, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(SpeedTo), true , nil, {}, "Current Pos") - wp[#wp+1]=p0:WaypointAir(nil, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(SpeedTo), true , nil, {TaskArrived, TaskHold, TaskKlar}, "Holding Point") + -- NOTE: Currently, this first waypoint confuses the AI. It makes them go in circles. Looks like they cannot find the waypoint and are flying around it. + --wp[#wp+1]=c0:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(SpeedTo), true , nil, {}, "Current Pos") + wp[#wp+1]=c1:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(SpeedTo), true , nil, {}, "Climb") + wp[#wp+1]=c2:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(SpeedTo), true , nil, {}, "Descent") + wp[#wp+1]=p0:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(SpeedTo), true , nil, {TaskArrived, TaskHold, TaskKlar}, "Holding Point") -- Approach point: 10 NN in direction of runway. - if airbase:GetAirbaseCategory()==Airbase.Category.AIRDROME then + if airbase:IsAirdrome() then --- -- 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(nil, 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) wp[#wp+1]=pland:WaypointAirLanding(UTILS.KnotsToKmph(SpeedLand), airbase, {}, "Landing") - elseif airbase:GetAirbaseCategory()==Airbase.Category.SHIP then + elseif airbase:IsShip() or airbase:IsHelipad() then --- - -- Ship + -- Ship or Helipad --- local pland=airbase:GetCoordinate() @@ -134326,33 +151203,13 @@ function FLIGHTGROUP:onafterRTB(From, Event, To, airbase, SpeedTo, SpeedHold, Sp if self.isAI then - local routeto=false - if fc or world.event.S_EVENT_KILL then - routeto=true - end - -- Clear all tasks. -- Warning, looks like this can make DCS CRASH! Had this after calling RTB once passed the final waypoint. --self:ClearTasks() - -- Respawn? - if routeto then - - -- Just route the group. Respawn might happen when going from holding to final. - self:Route(wp, 1) - - else - - -- Get group template. - local Template=self.group:GetTemplate() - - -- Set route points. - Template.route.points=wp - - --Respawn the group with new waypoints. - self:Respawn(Template) - - end + -- Just route the group. Respawn might happen when going from holding to final. + -- NOTE: I have delayed that here because of RTB calling _LandAtAirbase which resets current task immediately. So the stop flag change to 1 will not trigger TaskDone() and a current mission is not done either + self:Route(wp, 0.1) end @@ -134363,37 +151220,31 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param Core.Point#COORDINATE Coord Coordinate where to orbit. Default current position. --- @param #number Altitude Altitude in feet. Default 10000 ft. --- @param #number Speed Speed in knots. Default 250 kts. -function FLIGHTGROUP:onbeforeWait(From, Event, To, Coord, Altitude, Speed) +-- @param #number Duration Duration how long the group will be waiting in seconds. Default `nil` (=forever). +-- @param #number Altitude Altitude in feet. Default 10,000 ft for airplanes and 1,000 feet for helos. +-- @param #number Speed Speed in knots. Default 250 kts for airplanes and 20 kts for helos. +function FLIGHTGROUP:onbeforeWait(From, Event, To, Duration, Altitude, Speed) local allowed=true local Tsuspend=nil - -- Check if there are remaining tasks. - local Ntot,Nsched, Nwp=self:CountRemainingTasks() - - if self.taskcurrent>0 then - self:I(self.lid..string.format("WARNING: Got current task ==> WAIT event is suspended for 10 sec.")) - Tsuspend=-10 + -- Check for a current task. + if self.taskcurrent>0 and not self:IsLandedAt() then + self:T(self.lid..string.format("WARNING: Got current task ==> WAIT event is suspended for 30 sec!")) + Tsuspend=-30 allowed=false end - - if Nsched>0 then - self:I(self.lid..string.format("WARNING: Still got %d SCHEDULED tasks in the queue ==> WAIT event is suspended for 10 sec.", Nsched)) - Tsuspend=-10 - allowed=false - end - - if Nwp>0 then - self:I(self.lid..string.format("WARNING: Still got %d WAYPOINT tasks in the queue ==> WAIT event is suspended for 10 sec.", Nwp)) - Tsuspend=-10 - allowed=false + + -- Check for a current transport assignment. + if self.cargoTransport and not self:IsLandedAt() then + --self:T(self.lid..string.format("WARNING: Got current TRANSPORT assignment ==> WAIT event is suspended for 30 sec!")) + --Tsuspend=-30 + --allowed=false end + -- Call wait again. if Tsuspend and not allowed then - self:__Wait(Tsuspend, Coord, Altitude, Speed) + self:__Wait(Tsuspend, Duration, Altitude, Speed) end return allowed @@ -134405,26 +151256,53 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param Core.Point#COORDINATE Coord Coordinate where to orbit. Default current position. --- @param #number Altitude Altitude in feet. Default 10000 ft. --- @param #number Speed Speed in knots. Default 250 kts. -function FLIGHTGROUP:onafterWait(From, Event, To, Coord, Altitude, Speed) +-- @param #number Duration Duration how long the group will be waiting in seconds. Default `nil` (=forever). +-- @param #number Altitude Altitude in feet. Default 10,000 ft for airplanes and 1,000 feet for helos. +-- @param #number Speed Speed in knots. Default 250 kts for airplanes and 20 kts for helos. +function FLIGHTGROUP:onafterWait(From, Event, To, Duration, Altitude, Speed) - Coord=Coord or self.group:GetCoordinate() - Altitude=Altitude or (self.ishelo and 1000 or 10000) - Speed=Speed or (self.ishelo and 80 or 250) + -- Group will orbit at its current position. + local Coord=self:GetCoordinate() + + -- Set altitude: 1000 ft for helos and 10,000 ft for panes. + if Altitude then + Altitude=UTILS.FeetToMeters(Altitude) + else + Altitude=self.altitudeCruise + end + + -- Set speed. + Speed=Speed or (self.isHelo and 20 or 250) -- Debug message. - local text=string.format("Flight group set to wait/orbit at altitude %d m and speed %.1f km/h", Altitude, Speed) + local text=string.format("Group set to wait/orbit at altitude %d m and speed %.1f km/h for %s seconds", Altitude, Speed, tostring(Duration)) self:T(self.lid..text) --TODO: set ROE passive. introduce roe event/state/variable. - -- Orbit task. - local TaskOrbit=self.group:TaskOrbit(Coord, UTILS.FeetToMeters(Altitude), UTILS.KnotsToMps(Speed)) + -- Orbit until flaghold=1 (true) but max 5 min if no FC is giving the landing clearance. + self.flaghold:Set(0) + local TaskOrbit = self.group:TaskOrbit(Coord, Altitude, UTILS.KnotsToMps(Speed)) + local TaskStop = self.group:TaskCondition(nil, self.flaghold.UserFlagName, 1, nil, Duration) + local TaskCntr = self.group:TaskControlled(TaskOrbit, TaskStop) + local TaskOver = self.group:TaskFunction("FLIGHTGROUP._FinishedWaiting", self) + + local DCSTasks + if Duration or true then + DCSTasks=self.group:TaskCombo({TaskCntr, TaskOver}) + else + DCSTasks=self.group:TaskCombo({TaskOrbit, TaskOver}) + end + -- Set task. - self:SetTask(TaskOrbit) + self:PushTask(DCSTasks) + + -- Set time stamp. + self.Twaiting=timer.getAbsTime() + + -- Max waiting + self.dTwait=Duration end @@ -134439,7 +151317,7 @@ function FLIGHTGROUP:onafterRefuel(From, Event, To, Coordinate) -- Debug message. local text=string.format("Flight group set to refuel at the nearest tanker") - self:I(self.lid..text) + self:T(self.lid..text) --TODO: set ROE passive. introduce roe event/state/variable. --TODO: cancel current task @@ -134454,7 +151332,7 @@ function FLIGHTGROUP:onafterRefuel(From, Event, To, Coordinate) local Speed=self.speedCruise - local coordinate=self.group:GetCoordinate() + local coordinate=self:GetCoordinate() Coordinate=Coordinate or coordinate:Translate(UTILS.NMToMeters(5), self.group:GetHeading(), true) @@ -134474,7 +151352,7 @@ function FLIGHTGROUP:onafterRefueled(From, Event, To) -- Debug message. local text=string.format("Flight group finished refuelling") - self:I(self.lid..text) + self:T(self.lid..text) -- Check if flight is done. self:_CheckGroupDone(1) @@ -134491,26 +151369,56 @@ function FLIGHTGROUP:onafterHolding(From, Event, To) -- Set holding flag to 0 (just in case). self.flaghold:Set(0) + + -- Despawn after holding. + if self.despawnAfterHolding then + if self.legion then + self:ReturnToLegion(1) + else + self:Despawn(1) + end + return + end -- 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 - if self.ishelo then + if self.isHelo then local carrierpos=self.airboss:GetCoordinate() local carrierheading=self.airboss:GetHeading() @@ -134548,50 +151456,50 @@ function FLIGHTGROUP:onafterEngageTarget(From, Event, To, Target) -- Check target object. if Target:IsInstanceOf("UNIT") or Target:IsInstanceOf("STATIC") then - + DCStask=self:GetGroup():TaskAttackUnit(Target, true) - + elseif Target:IsInstanceOf("GROUP") then DCStask=self:GetGroup():TaskAttackGroup(Target, nil, nil, nil, nil, nil, nil, true) - + elseif Target:IsInstanceOf("SET_UNIT") then local DCSTasks={} - + for _,_unit in pairs(Target:GetSet()) do --detected by =HRP= Zero local unit=_unit --Wrapper.Unit#UNIT local task=self:GetGroup():TaskAttackUnit(unit, true) table.insert(DCSTasks) end - + -- Task combo. DCStask=self:GetGroup():TaskCombo(DCSTasks) elseif Target:IsInstanceOf("SET_GROUP") then local DCSTasks={} - + for _,_unit in pairs(Target:GetSet()) do --detected by =HRP= Zero local unit=_unit --Wrapper.Unit#UNIT local task=self:GetGroup():TaskAttackGroup(Target, nil, nil, nil, nil, nil, nil, true) table.insert(DCSTasks) end - + -- Task combo. DCStask=self:GetGroup():TaskCombo(DCSTasks) - + else - self:E("ERROR: unknown Target in EngageTarget! Needs to be a UNIT, STATIC, GROUP, SET_UNIT or SET_GROUP") + self:T("ERROR: unknown Target in EngageTarget! Needs to be a UNIT, STATIC, GROUP, SET_UNIT or SET_GROUP") return end -- Create new task.The description "Engage_Target" is checked so do not change that lightly. local Task=self:NewTaskScheduled(DCStask, 1, "Engage_Target", 0) - + -- Backup ROE setting. Task.backupROE=self:GetROE() - + -- Switch ROE to open fire self:SwitchROE(ENUMS.ROE.OpenFire) @@ -134602,7 +151510,7 @@ function FLIGHTGROUP:onafterEngageTarget(From, Event, To, Target) end -- Execute task. - self:TaskExecute(Task) + self:TaskExecute(Task) end @@ -134624,7 +151532,7 @@ end -- @param Core.Point#COORDINATE Coordinate The coordinate where to land. Default is current position. -- @param #number Duration The duration in seconds to remain on ground. Default 600 sec (10 min). function FLIGHTGROUP:onbeforeLandAt(From, Event, To, Coordinate, Duration) - return self.ishelo + return self.isHelo end --- On after "LandAt" event. Order helicopter to land at a specific point. @@ -134633,11 +151541,13 @@ end -- @param #string Event Event. -- @param #string To To state. -- @param Core.Point#COORDINATE Coordinate The coordinate where to land. Default is current position. --- @param #number Duration The duration in seconds to remain on ground. Default 600 sec (10 min). +-- @param #number Duration The duration in seconds to remain on ground. Default `nil` = forever. function FLIGHTGROUP:onafterLandAt(From, Event, To, Coordinate, Duration) -- Duration. - Duration=Duration or 600 + --Duration=Duration or 600 + + self:T(self.lid..string.format("Landing at Coordinate for %s seconds", tostring(Duration))) Coordinate=Coordinate or self:GetCoordinate() @@ -134645,11 +151555,8 @@ function FLIGHTGROUP:onafterLandAt(From, Event, To, Coordinate, Duration) local Task=self:NewTaskScheduled(DCStask, 1, "Task_Land_At", 0) - -- Add task with high priority. - --self:AddTask(task, 1, "Task_Land_At", 0) - self:TaskExecute(Task) - + end --- On after "FuelLow" event. @@ -134659,9 +151566,12 @@ end -- @param #string To To state. function FLIGHTGROUP:onafterFuelLow(From, Event, To) + -- Current min fuel. + local fuel=self:GetFuelMin() or 0 + -- Debug message. - local text=string.format("Low fuel for flight group %s", self.groupname) - self:I(self.lid..text) + local text=string.format("Low fuel %d for flight group %s", fuel, self.groupname) + self:T(self.lid..text) -- Set switch to true. self.fuellow=true @@ -134669,55 +151579,30 @@ function FLIGHTGROUP:onafterFuelLow(From, Event, To) -- Back to destination or home. local airbase=self.destbase or self.homebase - if self.airwing then + if self.fuellowrefuel and self.refueltype then - -- Get closest tanker from airwing that can refuel this flight. - local tanker=self.airwing:GetTankerForFlight(self) + -- Find nearest tanker within 50 NM. + local tanker=self:FindNearestTanker(50) + + if tanker then - if tanker and self.fuellowrefuel then - -- Debug message. - self:I(self.lid..string.format("Send to refuel at tanker %s", tanker.flightgroup:GetName())) - - -- Get a coordinate towards the tanker. - local coordinate=self:GetCoordinate():GetIntermediateCoordinate(tanker.flightgroup:GetCoordinate(), 0.75) + self:T(self.lid..string.format("Send to refuel at tanker %s", tanker:GetName())) - -- Send flight to tanker with refueling task. + -- Get a coordinate towards the tanker. + local coordinate=self:GetCoordinate():GetIntermediateCoordinate(tanker:GetCoordinate(), 0.75) + + -- Trigger refuel even. self:Refuel(coordinate) - else - - if airbase and self.fuellowrtb then - self:RTB(airbase) - --TODO: RTZ - end - - end - - else - - if self.fuellowrefuel and self.refueltype then - - local tanker=self:FindNearestTanker(50) - - if tanker then - - self:I(self.lid..string.format("Send to refuel at tanker %s", tanker:GetName())) - - -- Get a coordinate towards the tanker. - local coordinate=self:GetCoordinate():GetIntermediateCoordinate(tanker:GetCoordinate(), 0.75) - - self:Refuel(coordinate) - - return - end - end - - if airbase and self.fuellowrtb then - self:RTB(airbase) - --TODO: RTZ + return end + end + -- Send back to airbase. + if airbase and self.fuellowrtb then + self:RTB(airbase) + --TODO: RTZ end end @@ -134731,7 +151616,7 @@ function FLIGHTGROUP:onafterFuelCritical(From, Event, To) -- Debug message. local text=string.format("Critical fuel for flight group %s", self.groupname) - self:I(self.lid..text) + self:T(self.lid..text) -- Set switch to true. self.fuelcritical=true @@ -134745,44 +151630,6 @@ function FLIGHTGROUP:onafterFuelCritical(From, Event, To) end end ---- On after "Stop" event. --- @param #FLIGHTGROUP self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. -function FLIGHTGROUP:onafterStop(From, Event, To) - - -- Check if group is still alive. - if self:IsAlive() then - - -- Set element parking spot to FREE (after arrived for example). - if self.flightcontrol then - for _,_element in pairs(self.elements) do - local element=_element --#FLIGHTGROUP.Element - self:_SetElementParkingFree(element) - end - end - - end - - -- Handle events: - self:UnHandleEvent(EVENTS.Birth) - self:UnHandleEvent(EVENTS.EngineStartup) - self:UnHandleEvent(EVENTS.Takeoff) - self:UnHandleEvent(EVENTS.Land) - self:UnHandleEvent(EVENTS.EngineShutdown) - self:UnHandleEvent(EVENTS.PilotDead) - self:UnHandleEvent(EVENTS.Ejection) - self:UnHandleEvent(EVENTS.Crash) - self:UnHandleEvent(EVENTS.RemoveUnit) - - -- Call OPSGROUP function. - self:GetParent(self).onafterStop(self, From, Event, To) - - -- Remove flight from data base. - _DATABASE.FLIGHTGROUPS[self.groupname]=nil -end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Task functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -134819,6 +151666,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. @@ -134829,46 +151690,63 @@ function FLIGHTGROUP._FinishedRefuelling(group, flightgroup) flightgroup:__Refueled(-1) end +--- Function called when flight finished waiting. +-- @param Wrapper.Group#GROUP group Group object. +-- @param #FLIGHTGROUP flightgroup Flight group object. +function FLIGHTGROUP._FinishedWaiting(group, flightgroup) + flightgroup:T(flightgroup.lid..string.format("Group finished waiting")) + + -- Not waiting any more. + flightgroup.Twaiting=nil + flightgroup.dTwait=nil + + -- Check group done. + flightgroup:_CheckGroupDone(0.1) +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Misc functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Initialize group parameters. Also initializes waypoints if self.waypoints is nil. -- @param #FLIGHTGROUP self +-- @param #table Template Template used to init the group. Default is `self.template`. -- @return #FLIGHTGROUP self -function FLIGHTGROUP:_InitGroup() +function FLIGHTGROUP:_InitGroup(Template) -- First check if group was already initialized. if self.groupinitialized then - self:E(self.lid.."WARNING: Group was already initialized!") + self:T(self.lid.."WARNING: Group was already initialized! Will NOT do it again!") return end - + -- Group object. local group=self.group --Wrapper.Group#GROUP -- Get template of group. - self.template=group:GetTemplate() - - -- Define category. - self.isAircraft=true - self.isNaval=false - self.isGround=false + local template=Template or self:_GetTemplate() -- Helo group. - self.ishelo=group:IsHelicopter() + self.isHelo=group:IsHelicopter() -- Is (template) group uncontrolled. - self.isUncontrolled=self.template.uncontrolled + self.isUncontrolled=template.uncontrolled -- Is (template) group late activated. - self.isLateActivated=self.template.lateActivation + self.isLateActivated=template.lateActivation -- Max speed in km/h. self.speedMax=group:GetSpeedMax() + + -- Is group mobile? + if self.speedMax>3.6 then + self.isMobile=true + else + self.isMobile=false + end -- Cruise speed limit 350 kts for fixed and 80 knots for rotary wings. - local speedCruiseLimit=self.ishelo and UTILS.KnotsToKmph(80) or UTILS.KnotsToKmph(350) + local speedCruiseLimit=self.isHelo and UTILS.KnotsToKmph(80) or UTILS.KnotsToKmph(350) -- Cruise speed: 70% of max speed but within limit. self.speedCruise=math.min(self.speedMax*0.7, speedCruiseLimit) @@ -134877,12 +151755,13 @@ function FLIGHTGROUP:_InitGroup() self.ammo=self:GetAmmoTot() -- Radio parameters from template. Default is set on spawn if not modified by user. - self.radio.Freq=tonumber(self.template.frequency) - self.radio.Modu=tonumber(self.template.modulation) - self.radio.On=self.template.communication - + self.radio.Freq=tonumber(template.frequency) + self.radio.Modu=tonumber(template.modulation) + self.radio.On=template.communication + -- Set callsign. Default is set on spawn if not modified by user. - local callsign=self.template.units[1].callsign + 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={} @@ -134890,18 +151769,17 @@ function FLIGHTGROUP:_InitGroup() 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.NumberElement=callsign[3] -- First element only + self.callsign.NumberSquad=tonumber(callsign[1]) + self.callsign.NumberGroup=tonumber(callsign[2]) self.callsign.NameSquad=UTILS.GetCallsignName(self.callsign.NumberSquad) -- Set default formation. - if self.ishelo then + if self.isHelo then self.optionDefault.Formation=ENUMS.Formation.RotaryWing.EchelonLeft.D300 else self.optionDefault.Formation=ENUMS.Formation.FixedWing.EchelonLeft.Group end - + -- Default TACAN off. self:SetDefaultTACAN(nil, nil, nil, nil, true) self.tacan=UTILS.DeepCopy(self.tacanDefault) @@ -134912,129 +151790,41 @@ function FLIGHTGROUP:_InitGroup() -- 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. + local units=self.group:GetUnits() + + -- DCS group. + local dcsgroup=Group.getByName(self.groupname) + local size0=dcsgroup:getInitialSize() + + -- Quick check. + if #units~=size0 then + self:T(self.lid..string.format("ERROR: Got #units=%d but group consists of %d units!", #units, size0)) + end + -- Add elemets. - for _,unit in pairs(self.group:GetUnits()) do - local element=self:AddElementByName(unit:GetName()) - end - - -- Get first unit. This is used to extract other parameters. - local unit=self.group:GetUnit(1) - - if unit then - - self.rangemax=unit:GetRange() - - self.descriptors=unit:GetDesc() - - self.actype=unit:GetTypeName() - - self.ceiling=self.descriptors.Hmax - - self.tankertype=select(2, unit:IsTanker()) - self.refueltype=select(2, unit:IsRefuelable()) - - -- Debug info. - if self.verbose>=1 then - local text=string.format("Initialized Flight Group %s:\n", self.groupname) - text=text..string.format("Unit type = %s\n", self.actype) - text=text..string.format("Speed max = %.1f Knots\n", UTILS.KmphToKnots(self.speedMax)) - text=text..string.format("Range max = %.1f km\n", self.rangemax/1000) - text=text..string.format("Ceiling = %.1f feet\n", UTILS.MetersToFeet(self.ceiling)) - text=text..string.format("Tanker type = %s\n", tostring(self.tankertype)) - text=text..string.format("Refuel type = %s\n", tostring(self.refueltype)) - text=text..string.format("AI = %s\n", tostring(self.isAI)) - text=text..string.format("Helicopter = %s\n", tostring(self.group:IsHelicopter())) - text=text..string.format("Elements = %d\n", #self.elements) - text=text..string.format("Waypoints = %d\n", #self.waypoints) - text=text..string.format("Radio = %.1f MHz %s %s\n", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu), tostring(self.radio.On)) - text=text..string.format("Ammo = %d (G=%d/R=%d/B=%d/M=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Bombs, self.ammo.Missiles) - text=text..string.format("FSM state = %s\n", self:GetState()) - text=text..string.format("Is alive = %s\n", tostring(self.group:IsAlive())) - text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated())) - text=text..string.format("Uncontrolled = %s\n", tostring(self:IsUncontrolled())) - text=text..string.format("Start Air = %s\n", tostring(self:IsTakeoffAir())) - 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())) - self:I(self.lid..text) - end - - -- Init done. - self.groupinitialized=true - + for _,unit in pairs(units) do + self:_AddElementByName(unit:GetName()) end + -- Init done. + self.groupinitialized=true + return self end ---- Add an element to the flight group. --- @param #FLIGHTGROUP self --- @param #string unitname Name of unit. --- @return #FLIGHTGROUP.Element The element or nil. -function FLIGHTGROUP:AddElementByName(unitname) - - local unit=UNIT:FindByName(unitname) - - if unit then - - local element={} --#FLIGHTGROUP.Element - - element.name=unitname - element.unit=unit - element.status=OPSGROUP.ElementStatus.INUTERO - element.group=unit:GetGroup() - - -- TODO: this is wrong when grouping is used! - local unittemplate=element.unit:GetTemplate() - - element.modex=unittemplate.onboard_num - element.skill=unittemplate.skill - element.payload=unittemplate.payload - element.pylons=unittemplate.payload and unittemplate.payload.pylons or nil --element.unit:GetTemplatePylons() - element.fuelmass0=unittemplate.payload and unittemplate.payload.fuel or 0 --element.unit:GetTemplatePayload().fuel - element.fuelmass=element.fuelmass0 - element.fuelrel=element.unit:GetFuel() - element.category=element.unit:GetUnitCategory() - element.categoryname=element.unit:GetCategoryName() - element.callsign=element.unit:GetCallsign() - element.size=element.unit:GetObjectSize() - - if element.skill=="Client" or element.skill=="Player" then - element.ai=false - element.client=CLIENT:FindByName(unitname) - else - element.ai=true - end - - -- Debug text. - local text=string.format("Adding element %s: status=%s, skill=%s, modex=%s, fuelmass=%.1f (%d), category=%d, categoryname=%s, callsign=%s, ai=%s", - element.name, element.status, element.skill, element.modex, element.fuelmass, element.fuelrel*100, element.category, element.categoryname, element.callsign, tostring(element.ai)) - self:T(self.lid..text) - - -- Add element to table. - table.insert(self.elements, element) - - if unit:IsAlive() then - self:ElementSpawned(element) - end - - return element - end - - return nil -end - --- Check if a unit is and element of the flightgroup. -- @param #FLIGHTGROUP self -- @return Wrapper.Airbase#AIRBASE Final destination airbase or #nil. function FLIGHTGROUP:GetHomebaseFromWaypoints() - local wp=self:GetWaypoint(1) + local wp=self.waypoints0 and self.waypoints0[1] or nil --self:GetWaypoint(1) if wp then @@ -135044,7 +151834,7 @@ function FLIGHTGROUP:GetHomebaseFromWaypoints() -- Get airbase ID depending on airbase category. local airbaseID=nil - + if wp.airdromeId then airbaseID=wp.airdromeId else @@ -135052,7 +151842,7 @@ function FLIGHTGROUP:GetHomebaseFromWaypoints() end local airbase=AIRBASE:FindByID(airbaseID) - + return airbase end @@ -135120,13 +151910,13 @@ function FLIGHTGROUP:FindNearestTanker(Radius) local istanker, refuelsystem=unit:IsTanker() - if istanker and self.refueltype==refuelsystem then + if istanker and self.refueltype==refuelsystem and unit:IsAlive() and unit:GetCoalition()==self:GetCoalition() then -- Distance. local d=unit:GetCoordinate():Get2DDistance(coord) if d1 then table.remove(self.waypoints, #self.waypoints) else self.destbase=self.homebase @@ -135329,7 +152120,7 @@ function FLIGHTGROUP:InitWaypoints() -- Check if only 1 wp? if #self.waypoints==1 then - self.passedfinalwp=true + self:_PassedFinalWaypoint(true, "FLIGHTGROUP:InitWaypoints #self.waypoints==1") end end @@ -135340,29 +152131,76 @@ end --- Add an AIR waypoint to the flight plan. -- @param #FLIGHTGROUP self -- @param Core.Point#COORDINATE Coordinate The coordinate of the waypoint. Use COORDINATE:SetAltitude(altitude) to define the altitude. --- @param #number Speed Speed in knots. Default 350 kts. +-- @param #number Speed Speed in knots. Default is cruise speed. -- @param #number AfterWaypointWithID Insert waypoint after waypoint given ID. Default is to insert as last waypoint. -- @param #number Altitude Altitude in feet. Default is y-component of Coordinate. Note that these altitudes are wrt to sea level (barometric altitude). -- @param #boolean Updateroute If true or nil, call UpdateRoute. If false, no call. -- @return Ops.OpsGroup#OPSGROUP.Waypoint Waypoint table. function FLIGHTGROUP:AddWaypoint(Coordinate, Speed, AfterWaypointWithID, Altitude, Updateroute) + -- Create coordinate. + local coordinate=self:_CoordinateFromObject(Coordinate) + + -- Set waypoint index. + local wpnumber=self:GetWaypointIndexAfterID(AfterWaypointWithID) + + -- Speed in knots. + Speed=Speed or self:GetSpeedCruise() + + -- Create air waypoint. + local wp=coordinate:WaypointAir(COORDINATE.WaypointAltType.BARO, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(Speed), true, nil, {}) + + -- Create waypoint data table. + local waypoint=self:_CreateWaypoint(wp) + + -- Set altitude. + if Altitude then + waypoint.alt=UTILS.FeetToMeters(Altitude) + end + + -- Add waypoint to table. + self:_AddWaypoint(waypoint, wpnumber) + + -- Debug info. + self:T(self.lid..string.format("Adding AIR waypoint #%d, speed=%.1f knots. Last waypoint passed was #%s. Total waypoints #%d", wpnumber, Speed, self.currentwp, #self.waypoints)) + + -- Update route. + if Updateroute==nil or Updateroute==true then + self:__UpdateRoute(-0.01) + end + + return waypoint +end + +--- Add an LANDING waypoint to the flight plan. +-- @param #FLIGHTGROUP self +-- @param Wrapper.Airbase#AIRBASE Airbase The airbase where the group should land. +-- @param #number Speed Speed in knots. Default 350 kts. +-- @param #number AfterWaypointWithID Insert waypoint after waypoint given ID. Default is to insert as last waypoint. +-- @param #number Altitude Altitude in feet. Default is y-component of Coordinate. Note that these altitudes are wrt to sea level (barometric altitude). +-- @param #boolean Updateroute If true or nil, call UpdateRoute. If false, no call. +-- @return Ops.OpsGroup#OPSGROUP.Waypoint Waypoint table. +function FLIGHTGROUP:AddWaypointLanding(Airbase, Speed, AfterWaypointWithID, Altitude, Updateroute) + -- Set waypoint index. local wpnumber=self:GetWaypointIndexAfterID(AfterWaypointWithID) if wpnumber>self.currentwp then - self.passedfinalwp=false + self:_PassedFinalWaypoint(false, "AddWaypointLanding") end -- Speed in knots. - Speed=Speed or 350 + Speed=Speed or self.speedCruise + + -- Get coordinate of airbase. + local Coordinate=Airbase:GetCoordinate() -- Create air waypoint. - local wp=Coordinate:WaypointAir(COORDINATE.WaypointAltType.BARO, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(Speed), true, nil, {}) + local wp=Coordinate:WaypointAir(COORDINATE.WaypointAltType.BARO,COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, Speed, nil, Airbase, {}, "Landing Temp", nil) -- Create waypoint data table. local waypoint=self:_CreateWaypoint(wp) - + -- Set altitude. if Altitude then waypoint.alt=UTILS.FeetToMeters(Altitude) @@ -135382,31 +152220,38 @@ function FLIGHTGROUP:AddWaypoint(Coordinate, Speed, AfterWaypointWithID, Altitud return waypoint end - - ---- Check if a unit is an element of the flightgroup. +--- Get player element. -- @param #FLIGHTGROUP self --- @param #string unitname Name of unit. --- @return #boolean If true, unit is element of the flight group or false if otherwise. -function FLIGHTGROUP:_IsElement(unitname) +-- @return Ops.OpsGroup#OPSGROUP.Element The element. +function FLIGHTGROUP:GetPlayerElement() for _,_element in pairs(self.elements) do - local element=_element --#FLIGHTGROUP.Element - - if element.name==unitname then - return true + local element=_element --Ops.OpsGroup#OPSGROUP.Element + if not element.ai then + return element end - end - return false + return nil end +--- Get player element. +-- @param #FLIGHTGROUP self +-- @return #string Player name or `nil`. +function FLIGHTGROUP:GetPlayerName() + local playerElement=self:GetPlayerElement() + + if playerElement then + return playerElement.playerName + end + + return nil +end --- Set parking spot of element. -- @param #FLIGHTGROUP self --- @param #FLIGHTGROUP.Element Element The element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The element. -- @param Wrapper.Airbase#AIRBASE.ParkingSpot Spot Parking Spot. function FLIGHTGROUP:_SetElementParkingAt(Element, Spot) @@ -135417,6 +152262,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 @@ -135430,7 +152282,7 @@ end --- Set parking spot of element to free -- @param #FLIGHTGROUP self --- @param #FLIGHTGROUP.Element Element The element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The element. function FLIGHTGROUP:_SetElementParkingFree(Element) if Element.parking then @@ -135540,25 +152392,37 @@ end --- Returns the parking spot of the element. -- @param #FLIGHTGROUP self --- @param #FLIGHTGROUP.Element element Element of the flight group. +-- @param Ops.OpsGroup#OPSGROUP.Element element Element of the flight group. -- @param #number maxdist Distance threshold in meters. Default 5 m. -- @param Wrapper.Airbase#AIRBASE airbase (Optional) The airbase to check for parking. Default is closest airbase to the element. -- @return Wrapper.Airbase#AIRBASE.ParkingSpot Parking spot or nil if no spot is within distance threshold. function FLIGHTGROUP:GetParkingSpot(element, maxdist, airbase) + -- Coordinate of unit landed local coord=element.unit:GetCoordinate() - airbase=airbase or self:GetClosestAirbase() --coord:GetClosestAirbase(nil, self:GetCoalition()) + -- Airbase. + 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". + end local spot=nil --Wrapper.Airbase#AIRBASE.ParkingSpot local dist=nil 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) + if dist0 then - self:I(self.lid..string.format("FF updating menu in %.1f sec", delay)) + -- Delayed call. self:ScheduleOnce(delay, FLIGHTGROUP._UpdateMenu, self) else - self:I(self.lid.."FF updating menu NOW") - - -- Get current position of group. - local position=self.group: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) + -- Player element. + local player=self:GetPlayerElement() + + if player and player.status~=OPSGROUP.ElementStatus.DEAD then + + -- Debug text. + if self.verbose>=2 then + local text=string.format("Updating MENU: State=%s, ATC=%s [%s]", self:GetState(), + self.flightcontrol and self.flightcontrol.airbasename or "None", self.flightcontrol and self.flightcontrol:GetFlightStatus(self) or "Unknown") + + -- Message to group. + MESSAGE:New(text, 5):ToGroup(self.group) + self:I(self.lid..text) + end + + -- Get current position of player. + local position=self:GetCoordinate(nil, player.name) + + -- 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 Event --> To State self:AddTransition("*", "FullStop", "Holding") -- Hold position. self:AddTransition("*", "Cruise", "Cruising") -- Hold position. + + self:AddTransition("*", "RTZ", "Returning") -- Group is returning to (home) zone. + self:AddTransition("Returning", "Returned", "Returned") -- Group is returned to (home) zone. + + self:AddTransition("*", "Detour", "Cruising") -- Make a detour to a coordinate and resume route afterwards. + self:AddTransition("*", "DetourReached", "*") -- Group reached the detour coordinate. + + self:AddTransition("*", "Retreat", "Retreating") -- Order a retreat. + self:AddTransition("Retreating", "Retreated", "Retreated") -- Group retreated. + + 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 + self:AddTransition("Engaging", "Disengage", "Cruising") -- Disengage and back to cruising. - self:AddTransition("*", "TurnIntoWind", "IntoWind") -- Command the group to turn into the wind. - self:AddTransition("IntoWind", "TurnedIntoWind", "IntoWind") -- Group turned into wind. - self:AddTransition("IntoWind", "TurnIntoWindStop", "IntoWind") -- Stop a turn into wind. - self:AddTransition("IntoWind", "TurnIntoWindOver", "Cruising") -- Turn into wind is over. + self:AddTransition("*", "TurnIntoWind", "Cruising") -- Command the group to turn into the wind. + self:AddTransition("*", "TurnedIntoWind", "*") -- Group turned into wind. + self:AddTransition("*", "TurnIntoWindStop", "*") -- Stop a turn into wind. + self:AddTransition("*", "TurnIntoWindOver", "*") -- Turn into wind is over. self:AddTransition("*", "TurningStarted", "*") -- Group started turning. self:AddTransition("*", "TurningStopped", "*") -- Group stopped turning. - self:AddTransition("*", "Detour", "OnDetour") -- Make a detour to a coordinate and resume route afterwards. - self:AddTransition("OnDetour", "DetourReached", "Cruising") -- Group reached the detour coordinate. - self:AddTransition("*", "CollisionWarning", "*") -- Collision warning. self:AddTransition("*", "ClearAhead", "*") -- Clear ahead. - self:AddTransition("*", "Dive", "Diving") -- Command a submarine to dive. - self:AddTransition("Diving", "Surface", "Cruising") -- Command a submarine to go to the surface. + self:AddTransition("Cruising", "Dive", "Cruising") -- Command a submarine to dive. + self:AddTransition("Engaging", "Dive", "Engaging") -- Command a submarine to dive. + self:AddTransition("Cruising", "Surface", "Cruising") -- Command a submarine to go to the surface. + self:AddTransition("Engaging", "Surface", "Engaging") -- Command a submarine to go to the surface. ------------------------ --- Pseudo Functions --- ------------------------ - --- Triggers the FSM event "Stop". Stops the NAVYGROUP and all its event handlers. + --- Triggers the FSM event "Cruise". + -- @function [parent=#NAVYGROUP] Cruise -- @param #NAVYGROUP self + -- @param #number Speed Speed in knots until next waypoint is reached. - --- Triggers the FSM event "Stop" after a delay. Stops the NAVYGROUP and all its event handlers. - -- @function [parent=#NAVYGROUP] __Stop + --- Triggers the FSM event "Cruise" after a delay. + -- @function [parent=#NAVYGROUP] __Cruise -- @param #NAVYGROUP self -- @param #number delay Delay in seconds. - - -- TODO: Add pseudo functions. + -- @param #number Speed Speed in knots until next waypoint is reached. + + --- On after "Cruise" event. + -- @function [parent=#NAVYGROUP] OnAfterCruise + -- @param #NAVYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number Speed Speed in knots until next waypoint is reached. + + + + + + + + --- Triggers the FSM event "TurnIntoWind". + -- @function [parent=#NAVYGROUP] TurnIntoWind + -- @param #NAVYGROUP self + -- @param #NAVYGROUP.IntoWind Into wind parameters. + + --- Triggers the FSM event "TurnIntoWind" after a delay. + -- @function [parent=#NAVYGROUP] __TurnIntoWind + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + -- @param #NAVYGROUP.IntoWind Into wind parameters. + + --- On after "TurnIntoWind" event. + -- @function [parent=#NAVYGROUP] OnAfterTurnIntoWind + -- @param #NAVYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #NAVYGROUP.IntoWind Into wind parameters. + + + --- Triggers the FSM event "TurnedIntoWind". + -- @function [parent=#NAVYGROUP] TurnedIntoWind + -- @param #NAVYGROUP self + + --- Triggers the FSM event "TurnedIntoWind" after a delay. + -- @function [parent=#NAVYGROUP] __TurnedIntoWind + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "TurnedIntoWind" event. + -- @function [parent=#NAVYGROUP] OnAfterTurnedIntoWind + -- @param #NAVYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "TurnIntoWindStop". + -- @function [parent=#NAVYGROUP] TurnIntoWindStop + -- @param #NAVYGROUP self + + --- Triggers the FSM event "TurnIntoWindStop" after a delay. + -- @function [parent=#NAVYGROUP] __TurnIntoWindStop + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "TurnIntoWindStop" event. + -- @function [parent=#NAVYGROUP] OnAfterTurnIntoWindStop + -- @param #NAVYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "TurnIntoWindOver". + -- @function [parent=#NAVYGROUP] TurnIntoWindOver + -- @param #NAVYGROUP self + -- @param #NAVYGROUP.IntoWind IntoWindData Data table. + + --- Triggers the FSM event "TurnIntoWindOver" after a delay. + -- @function [parent=#NAVYGROUP] __TurnIntoWindOver + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + -- @param #NAVYGROUP.IntoWind IntoWindData Data table. + + --- On after "TurnIntoWindOver" event. + -- @function [parent=#NAVYGROUP] OnAfterTurnIntoWindOver + -- @param #NAVYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #NAVYGROUP.IntoWind IntoWindData Data table. + + + --- Triggers the FSM event "TurningStarted". + -- @function [parent=#NAVYGROUP] TurningStarted + -- @param #NAVYGROUP self + + --- Triggers the FSM event "TurningStarted" after a delay. + -- @function [parent=#NAVYGROUP] __TurningStarted + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "TurningStarted" event. + -- @function [parent=#NAVYGROUP] OnAfterTurningStarted + -- @param #NAVYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "TurningStopped". + -- @function [parent=#NAVYGROUP] TurningStopped + -- @param #NAVYGROUP self + + --- Triggers the FSM event "TurningStopped" after a delay. + -- @function [parent=#NAVYGROUP] __TurningStopped + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "TurningStopped" event. + -- @function [parent=#NAVYGROUP] OnAfterTurningStopped + -- @param #NAVYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "CollisionWarning". + -- @function [parent=#NAVYGROUP] CollisionWarning + -- @param #NAVYGROUP self + + --- Triggers the FSM event "CollisionWarning" after a delay. + -- @function [parent=#NAVYGROUP] __CollisionWarning + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "CollisionWarning" event. + -- @function [parent=#NAVYGROUP] OnAfterCollisionWarning + -- @param #NAVYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "ClearAhead". + -- @function [parent=#NAVYGROUP] ClearAhead + -- @param #NAVYGROUP self + + --- Triggers the FSM event "ClearAhead" after a delay. + -- @function [parent=#NAVYGROUP] __ClearAhead + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "ClearAhead" event. + -- @function [parent=#NAVYGROUP] OnAfterClearAhead + -- @param #NAVYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Dive". + -- @function [parent=#NAVYGROUP] Dive + -- @param #NAVYGROUP self + -- @param #number Depth Dive depth in meters. Default 50 meters. + -- @param #number Speed Speed in knots until next waypoint is reached. + + --- Triggers the FSM event "Dive" after a delay. + -- @function [parent=#NAVYGROUP] __Dive + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + -- @param #number Depth Dive depth in meters. Default 50 meters. + -- @param #number Speed Speed in knots until next waypoint is reached. + + --- On after "Dive" event. + -- @function [parent=#NAVYGROUP] OnAfterDive + -- @param #NAVYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number Depth Dive depth in meters. Default 50 meters. + -- @param #number Speed Speed in knots until next waypoint is reached. + + + --- Triggers the FSM event "Surface". + -- @function [parent=#NAVYGROUP] Surface + -- @param #NAVYGROUP self + -- @param #number Speed Speed in knots until next waypoint is reached. + + --- Triggers the FSM event "Surface" after a delay. + -- @function [parent=#NAVYGROUP] __Surface + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + -- @param #number Speed Speed in knots until next waypoint is reached. + + --- On after "Surface" event. + -- @function [parent=#NAVYGROUP] OnAfterSurface + -- @param #NAVYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number Speed Speed in knots until next waypoint is reached. -- Init waypoints. - self:InitWaypoints() + self:_InitWaypoints() -- Initialize the group. self:_InitGroup() @@ -136131,13 +153508,16 @@ function NAVYGROUP:New(GroupName) self:HandleEvent(EVENTS.RemoveUnit, self.OnEventRemoveUnit) -- Start the status monitoring. - self:__Status(-1) + self.timerStatus=TIMER:New(self.Status, self):Start(1, 30) -- Start queue update timer. self.timerQueueUpdate=TIMER:New(self._QueueUpdate, self):Start(2, 5) -- Start check zone timer. self.timerCheckZone=TIMER:New(self._CheckInZones, self):Start(2, 60) + + -- Add OPSGROUP to _DATABASE. + _DATABASE:AddOpsGroup(self) return self end @@ -136183,7 +153563,7 @@ end -- @param #NAVYGROUP self -- @return #NAVYGROUP self function NAVYGROUP:SetPathfindingOff() - self:SetPathfinding(true, self.pathCorridor) + self:SetPathfinding(false, self.pathCorridor) return self end @@ -136268,6 +153648,10 @@ function NAVYGROUP:_CreateTurnIntoWind(starttime, stoptime, speed, uturn, offset -- Set start time. local Tstart=UTILS.ClockToSeconds(starttime) + + if uturn==nil then + uturn=true + end -- Set stop time. local Tstop=Tstart+90*60 @@ -136312,7 +153696,7 @@ end -- @param #NAVYGROUP self -- @param #string starttime Start time, e.g. "8:00" for eight o'clock. Default now. -- @param #string stoptime Stop time, e.g. "9:00" for nine o'clock. Default 90 minutes after start time. --- @param #number speed Speed in knots during turn into wind leg. +-- @param #number speed Wind speed on deck in knots during turn into wind leg. Default 20 knots. -- @param #boolean uturn If `true` (or `nil`), carrier wil perform a U-turn and go back to where it came from before resuming its route to the next waypoint. If false, it will go directly to the next waypoint. -- @param #number offset Offset angle in degrees, e.g. to account for an angled runway. Default 0 deg. -- @return #NAVYGROUP.IntoWind Turn into window data table. @@ -136336,7 +153720,6 @@ function NAVYGROUP:RemoveTurnIntoWind(IntoWindData) -- Check if this is a window currently open. if self.intowind and self.intowind.Id==IntoWindData.Id then - --env.info("FF stop in remove") self:TurnIntoWindStop() return end @@ -136401,51 +153784,69 @@ function NAVYGROUP:IsSteamingIntoWind() end end +--- Check if the group is currently recovering aircraft. +-- @param #NAVYGROUP self +-- @return #boolean If true, group is currently recovering. +function NAVYGROUP:IsRecovering() + if self.intowind then + if self.intowind.Recovery==true then + return true + else + return false + end + else + return false + end +end + +--- Check if the group is currently launching aircraft. +-- @param #NAVYGROUP self +-- @return #boolean If true, group is currently launching. +function NAVYGROUP:IsLaunching() + if self.intowind then + if self.intowind.Recovery==false then + return true + else + return false + end + else + return false + end +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Status ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ----- Update status. --- @param #NAVYGROUP self -function NAVYGROUP:onbeforeStatus(From, Event, To) - - if self:IsDead() then - self:T(self.lid..string.format("Onbefore Status DEAD ==> false")) - return false - elseif self:IsStopped() then - self:T(self.lid..string.format("Onbefore Status STOPPED ==> false")) - return false - end - - return true -end - --- Update status. -- @param #NAVYGROUP self -function NAVYGROUP:onafterStatus(From, Event, To) +function NAVYGROUP:Status(From, Event, To) -- FSM state. local fsmstate=self:GetState() + + -- Is group alive? + local alive=self:IsAlive() - if self:IsAlive() then + -- Free path. + local freepath=0 - --- - -- Detection - --- - - -- Check if group has detected any units. - if self.detectionOn then - self:_CheckDetectedUnits() - end - + -- Check if group is exists and is active. + if alive then + -- Update last known position, orientation, velocity. self:_UpdatePosition() + + -- Check if group has detected any units. + self:_CheckDetectedUnits() -- Check if group started or stopped turning. self:_CheckTurning() - - local freepath=UTILS.NMToMeters(10) + + -- Distance to next Waypoint. + local disttoWP=math.min(self:GetDistanceToWaypoint(), UTILS.NMToMeters(10)) + freepath=disttoWP -- Only check if not currently turning. if not self:IsTurning() then @@ -136453,7 +153854,7 @@ function NAVYGROUP:onafterStatus(From, Event, To) -- Check free path ahead. freepath=self:_CheckFreePath(freepath, 100) - if freepath<5000 then + if disttoWP>1 and freepathself.Twaiting+self.dTwait then + self.Twaiting=nil + self.dTwait=nil + if self:_CountPausedMissions()>0 then + self:UnpauseMission() + else + self:Cruise() + end + end + end + end + + else + -- Check damage of elements and group. + self:_CheckDamage() + end + + -- Group exists but can also be inactive. + if alive~=nil then if self.verbose>=1 then + + -- Number of elements. + local nelem=self:CountElements() + local Nelem=#self.elements -- Get number of tasks and missions. local nTaskTot, nTaskSched, nTaskWP=self:CountRemainingTasks() local nMissions=self:CountRemainingMissison() - - local intowind=self:IsSteamingIntoWind() and UTILS.SecondsToClock(self.intowind.Tstop-timer.getAbsTime(), true) or "N/A" - local turning=tostring(self:IsTurning()) - local alt=self.position.y - local speed=UTILS.MpsToKnots(self.velocity) - local speedExpected=UTILS.MpsToKnots(self:GetExpectedSpeed()) --UTILS.MpsToKnots(self.speedWp or 0) + -- ROE and Alarm State. + local roe=self:GetROE() or -1 + local als=self:GetAlarmstate() or -1 + -- Waypoint stuff. local wpidxCurr=self.currentwp local wpuidCurr=self:GetWaypointUIDFromIndex(wpidxCurr) or 0 local wpidxNext=self:GetWaypointIndexNext() or 0 local wpuidNext=self:GetWaypointUIDFromIndex(wpidxNext) or 0 - local wpDist=UTILS.MetersToNM(self:GetDistanceToWaypoint() or 0) - local wpETA=UTILS.SecondsToClock(self:GetTimeToWaypoint() or 0, true) + local wpN=#self.waypoints or 0 + local wpF=tostring(self.passedfinalwp) - -- Current ROE and alarm state. - local roe=self:GetROE() or 0 - local als=self:GetAlarmstate() or 0 - - -- Info text. - local text=string.format("%s [ROE=%d,AS=%d, T/M=%d/%d]: Wp=%d[%d]-->%d[%d] (of %d) Dist=%.1f NM ETA=%s - Speed=%.1f (%.1f) kts, Depth=%.1f m, Hdg=%03d, Turn=%s Collision=%d IntoWind=%s", - fsmstate, roe, als, nTaskTot, nMissions, wpidxCurr, wpuidCurr, wpidxNext, wpuidNext, #self.waypoints or 0, wpDist, wpETA, speed, speedExpected, alt, self.heading, turning, freepath, intowind) - self:I(self.lid..text) + -- Speed. + local speed=UTILS.MpsToKnots(self.velocity or 0) + local speedEx=UTILS.MpsToKnots(self:GetExpectedSpeed()) - if false then - local text="Waypoints:" - for i,wp in pairs(self.waypoints) do - local waypoint=wp --Ops.OpsGroup#OPSGROUP.Waypoint - text=text..string.format("\n%d. UID=%d", i, waypoint.uid) - if i==self.currentwp then - text=text.." current!" - end - end - env.info(text) + -- Altitude. + local alt=self.position and self.position.y or 0 + + -- Heading in degrees. + local hdg=self.heading or 0 + + -- Life points. + local life=self.life or 0 + + -- Total ammo. + local ammo=self:GetAmmoTot().Total + + -- Detected units. + local ndetected=self.detectionOn and tostring(self.detectedunits:Count()) or "Off" + + -- Get cargo weight. + local cargo=0 + for _,_element in pairs(self.elements) do + local element=_element --Ops.OpsGroup#OPSGROUP.Element + cargo=cargo+element.weightCargo end + -- Into wind and turning status. + local intowind=self:IsSteamingIntoWind() and UTILS.SecondsToClock(self.intowind.Tstop-timer.getAbsTime(), true) or "N/A" + local turning=tostring(self:IsTurning()) + + -- Info text. + local text=string.format("%s [%d/%d]: ROE/AS=%d/%d | T/M=%d/%d | Wp=%d[%d]-->%d[%d]/%d [%s] | Life=%.1f | v=%.1f (%d) | Hdg=%03d | Ammo=%d | Detect=%s | Cargo=%.1f | Turn=%s Collision=%d IntoWind=%s", + fsmstate, nelem, Nelem, roe, als, nTaskTot, nMissions, wpidxCurr, wpuidCurr, wpidxNext, wpuidNext, wpN, wpF, life, speed, speedEx, hdg, ammo, ndetected, cargo, turning, freepath, intowind) + self:I(self.lid..text) + end else @@ -136529,7 +153975,7 @@ function NAVYGROUP:onafterStatus(From, Event, To) -- Recovery Windows --- - if self.verbose>=2 then + if alive and self.verbose>=2 and #self.Qintowind>0 then -- Debug output: local text=string.format(self.lid.."Turn into wind time windows:") @@ -136556,6 +154002,26 @@ function NAVYGROUP:onafterStatus(From, Event, To) end + --- + -- Engage Detected Targets + --- + if self:IsCruising() and self.detectionOn and self.engagedetectedOn then + + local targetgroup, targetdist=self:_GetDetectedTarget() + + -- If we found a group, we engage it. + if targetgroup then + self:I(self.lid..string.format("Engaging target group %s at distance %d meters", targetgroup:GetName(), targetdist)) + self:EngageTarget(targetgroup) + end + + end + + --- + -- Cargo + --- + + self:_CheckCargoTransport() --- -- Tasks & Missions @@ -136563,11 +154029,14 @@ function NAVYGROUP:onafterStatus(From, Event, To) self:_PrintTaskAndMissionStatus() - - -- Next status update in 30 seconds. - self:__Status(-30) end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- DCS Events ==> See OPSGROUP +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- See OPSGROUP! + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- FSM Events ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -136577,7 +154046,7 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #NAVYGROUP.Element Element The group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The group element. function NAVYGROUP:onafterElementSpawned(From, Event, To, Element) self:T(self.lid..string.format("Element spawned %s", Element.name)) @@ -136594,8 +154063,32 @@ end function NAVYGROUP:onafterSpawned(From, Event, To) self:T(self.lid..string.format("Group spawned!")) + -- Debug info. + if self.verbose>=1 then + local text=string.format("Initialized Navy Group %s:\n", self.groupname) + text=text..string.format("Unit type = %s\n", self.actype) + text=text..string.format("Speed max = %.1f Knots\n", UTILS.KmphToKnots(self.speedMax)) + text=text..string.format("Speed cruise = %.1f Knots\n", UTILS.KmphToKnots(self.speedCruise)) + text=text..string.format("Weight = %.1f kg\n", self:GetWeightTotal()) + text=text..string.format("Cargo bay = %.1f kg\n", self:GetFreeCargobay()) + text=text..string.format("Has EPLRS = %s\n", tostring(self.isEPLRS)) + text=text..string.format("Is Submarine = %s\n", tostring(self.isSubmarine)) + text=text..string.format("Elements = %d\n", #self.elements) + text=text..string.format("Waypoints = %d\n", #self.waypoints) + text=text..string.format("Radio = %.1f MHz %s %s\n", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu), tostring(self.radio.On)) + text=text..string.format("Ammo = %d (G=%d/R=%d/M=%d/T=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Missiles, self.ammo.Torpedos) + text=text..string.format("FSM state = %s\n", self:GetState()) + text=text..string.format("Is alive = %s\n", tostring(self:IsAlive())) + text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated())) + self:I(self.lid..text) + end + -- Update position. self:_UpdatePosition() + + -- Not dead or destroyed yet. + self.isDead=false + self.isDestroyed=false if self.isAI then @@ -136605,6 +154098,18 @@ 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) + + -- Set default Invisible. + self:SwitchInvisible(self.option.Invisible) + + -- Set default Immortal. + self:SwitchImmortal(self.option.Immortal) + -- Set TACAN beacon. self:_SwitchTACAN() @@ -136613,20 +154118,102 @@ function NAVYGROUP:onafterSpawned(From, Event, To) -- Set radio. if self.radioDefault then - self:SwitchRadio() + -- CAREFUL: This makes DCS crash for some ships like speed boats or Higgins boats! (On a respawn for example). Looks like the command SetFrequency is causing this. + --self:SwitchRadio() else self:SetDefaultRadio(self.radio.Freq, self.radio.Modu, false) end + + -- Update route. + if #self.waypoints>1 then + self:__Cruise(-0.1) + else + self:FullStop() + end end - -- Update route. - if #self.waypoints>1 then - self:Cruise() - else - self:FullStop() +end + +--- On before "UpdateRoute" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number n Next waypoint index. Default is the one coming after that one that has been passed last. +-- @param #number N Waypoint Max waypoint index to be included in the route. Default is the final waypoint. +-- @param #number Speed Speed in knots to the next waypoint. +-- @param #number Depth Depth in meters to the next waypoint. +function NAVYGROUP:onbeforeUpdateRoute(From, Event, To, n, Speed, Depth) + -- Is transition allowed? We assume yes until proven otherwise. + local allowed=true + local trepeat=nil + + if self:IsWaiting() then + self:T(self.lid.."Update route denied. Group is WAITING!") + return false + elseif self:IsInUtero() then + self:T(self.lid.."Update route denied. Group is INUTERO!") + return false + elseif self:IsDead() then + self:T(self.lid.."Update route denied. Group is DEAD!") + return false + elseif self:IsStopped() then + self:T(self.lid.."Update route denied. Group is STOPPED!") + return false + elseif self:IsHolding() then + self:T(self.lid.."Update route denied. Group is holding position!") + return false end + -- Check for a current task. + if self.taskcurrent>0 then + + -- Get the current task. Must not be executing already. + local task=self:GetTaskByID(self.taskcurrent) + + if task then + if task.dcstask.id=="PatrolZone" then + -- For patrol zone, we need to allow the update as we insert new waypoints. + self:T2(self.lid.."Allowing update route for Task: PatrolZone") + elseif task.dcstask.id=="ReconMission" then + -- For recon missions, we need to allow the update as we insert new waypoints. + 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") + 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))) + allowed=false + end + else + -- Now this can happen, if we directly use TaskExecute as the task is not in the task queue and cannot be removed. Therefore, also directly executed tasks should be added to the queue! + self:T(self.lid..string.format("WARNING: before update route taskcurrent=%d (>0!) but no task?!", self.taskcurrent)) + -- Anyhow, a task is running so we do not allow to update the route! + allowed=false + end + end + + -- Not good, because mission will never start. Better only check if there is a current task! + --if self.currentmission then + --end + + -- Only AI flights. + if not self.isAI then + allowed=false + end + + -- Debug info. + self:T2(self.lid..string.format("Onbefore Updateroute in state %s: allowed=%s (repeat in %s)", self:GetState(), tostring(allowed), tostring(trepeat))) + + -- Try again? + if trepeat then + self:__UpdateRoute(trepeat, n) + end + + return allowed + end --- On after "UpdateRoute" event. @@ -136634,57 +154221,80 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #number n Waypoint number. Default is next waypoint. +-- @param #number n Next waypoint index. Default is the one coming after that one that has been passed last. +-- @param #number N Waypoint Max waypoint index to be included in the route. Default is the final waypoint. -- @param #number Speed Speed in knots to the next waypoint. -- @param #number Depth Depth in meters to the next waypoint. -function NAVYGROUP:onafterUpdateRoute(From, Event, To, n, Speed, Depth) +function NAVYGROUP:onafterUpdateRoute(From, Event, To, n, N, Speed, Depth) -- Update route from this waypoint number onwards. n=n or self:GetWaypointIndexNext() - -- Update waypoint tasks, i.e. inject WP tasks into waypoint table. - self:_UpdateWaypointTasks(n) + -- Max index. + N=N or #self.waypoints + N=math.min(N, #self.waypoints) + -- Waypoints. local waypoints={} - -- Waypoint. - local wp=UTILS.DeepCopy(self.waypoints[n]) --Ops.OpsGroup#OPSGROUP.Waypoint + for i=n, N do + + -- Waypoint. + local wp=UTILS.DeepCopy(self.waypoints[i]) --Ops.OpsGroup#OPSGROUP.Waypoint - -- Speed. - if Speed then - -- Take speed specified. - wp.speed=UTILS.KnotsToMps(Speed) - else - -- Take default waypoint speed. But make sure speed>0 if patrol ad infinitum. - if self.adinfinitum and wp.speed<0.1 then - wp.speed=UTILS.KmphToMps(self.speedCruise) + --env.info(string.format("FF i=%d UID=%d n=%d, N=%d", i, wp.uid, n, N)) + + -- Speed. + if Speed then + -- Take speed specified. + wp.speed=UTILS.KnotsToMps(Speed) + else + -- Take default waypoint speed. But make sure speed>0 if patrol ad infinitum. + if wp.speed<0.1 then --self.adinfinitum and + wp.speed=UTILS.KmphToMps(self.speedCruise) + end + end + + -- Depth. + if Depth then + wp.alt=-Depth + elseif self.depth then + wp.alt=-self.depth + else + -- Take default waypoint alt. + wp.alt=wp.alt or 0 + end + + -- Current set speed in m/s. + if i==n then + self.speedWp=wp.speed + self.altWp=wp.alt end - end - if Depth then - wp.alt=-Depth - elseif self.depth then - wp.alt=-self.depth - else - -- Take default waypoint alt. - end + -- Add waypoint. + table.insert(waypoints, wp) - -- Current set speed in m/s. - self.speedWp=wp.speed - - -- Add waypoint. - table.insert(waypoints, wp) + end -- Current waypoint. - local current=self:GetCoordinate():WaypointNaval(UTILS.MpsToKmph(self.speedWp), wp.alt) + local current=self:GetCoordinate():WaypointNaval(UTILS.MpsToKmph(self.speedWp), self.altWp) table.insert(waypoints, 1, current) - if not self.passedfinalwp then + if self:IsEngaging() or not self.passedfinalwp then + + if self.verbose>=10 then + for i=1,#waypoints do + local wp=waypoints[i] --Ops.OpsGroup#OPSGROUP.Waypoint + local text=string.format("%s Waypoint [%d] UID=%d speed=%d", self.groupname, i-1, wp.uid or -1, wp.speed) + self:I(self.lid..text) + COORDINATE:NewFromWaypoint(wp):MarkToAll(text) + end + end -- Debug info. - self:T(self.lid..string.format("Updateing route: WP %d-->%d (%d/%d), Speed=%.1f knots, Depth=%d m", self.currentwp, n, #waypoints, #self.waypoints, UTILS.MpsToKnots(self.speedWp), wp.alt)) + self:T(self.lid..string.format("Updateing route: WP %d-->%d (%d/%d), Speed=%.1f knots, Depth=%d m", self.currentwp, n, #waypoints, #self.waypoints, UTILS.MpsToKnots(self.speedWp), self.altWp)) -- Route group to all defined waypoints remaining. self:Route(waypoints) @@ -136755,8 +154365,8 @@ function NAVYGROUP:onafterTurnIntoWind(From, Event, To, IntoWind) IntoWind.Open=true - IntoWind.Coordinate=self:GetCoordinate() - + IntoWind.Coordinate=self:GetCoordinate(true) + self.intowind=IntoWind -- Wind speed in m/s. @@ -136784,7 +154394,7 @@ function NAVYGROUP:onafterTurnIntoWind(From, Event, To, IntoWind) IntoWind.waypoint=wptiw - if IntoWind.Uturn and self.Debug then + if IntoWind.Uturn and false then IntoWind.Coordinate:MarkToAll("Return coord") end @@ -136842,8 +154452,13 @@ function NAVYGROUP:onafterTurnIntoWindOver(From, Event, To, IntoWindData) -- Detour to where we left the route. self:T(self.lid.."FF Turn Into Wind Over ==> Uturn!") - self:Detour(self.intowind.Coordinate, self:GetSpeedCruise(), 0, true) - + + -- ID of current waypoint. + local uid=self:GetWaypointCurrent().uid + + -- Add temp waypoint. + local wp=self:AddWaypoint(self.intowind.Coordinate, self:GetSpeedCruise(), uid) ; wp.temp=true + else --- @@ -136852,11 +154467,11 @@ function NAVYGROUP:onafterTurnIntoWindOver(From, Event, To, IntoWindData) -- Next waypoint index and speed. local indx=self:GetWaypointIndexNext() - local speed=self:GetWaypointSpeed(indx) + local speed=self:GetSpeedToWaypoint(indx) -- Update route. self:T(self.lid..string.format("FF Turn Into Wind Over ==> Next WP Index=%d at %.1f knots via update route!", indx, speed)) - self:__UpdateRoute(-1, indx, speed) + self:__UpdateRoute(-1, indx, nil, speed) end @@ -136897,10 +154512,14 @@ end -- @param #number Speed Speed in knots until next waypoint is reached. Default is speed set for waypoint. function NAVYGROUP:onafterCruise(From, Event, To, Speed) + -- Not waiting anymore. + self.Twaiting=nil + self.dTwait=nil + -- No set depth. self.depth=nil - self:__UpdateRoute(-1, nil, Speed) + self:__UpdateRoute(-0.1, nil, nil, Speed) end @@ -136915,11 +154534,11 @@ function NAVYGROUP:onafterDive(From, Event, To, Depth, Speed) Depth=Depth or 50 - self:T(self.lid..string.format("Diving to %d meters", Depth)) + self:I(self.lid..string.format("Diving to %d meters", Depth)) self.depth=Depth - self:__UpdateRoute(-1, nil, Speed) + self:__UpdateRoute(-1, nil, nil, Speed) end @@ -136933,7 +154552,7 @@ function NAVYGROUP:onafterSurface(From, Event, To, Speed) self.depth=0 - self:__UpdateRoute(-1, nil, Speed) + self:__UpdateRoute(-1, nil, nil, Speed) end @@ -136972,153 +154591,262 @@ function NAVYGROUP:onafterCollisionWarning(From, Event, To, Distance) self.collisionwarning=true end ---- On after Start event. Starts the NAVYGROUP FSM and event handlers. +--- On after "EngageTarget" event. -- @param #NAVYGROUP self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function NAVYGROUP:onafterStop(From, Event, To) +-- @param Wrapper.Group#GROUP Group the group to be engaged. +function NAVYGROUP:onafterEngageTarget(From, Event, To, Target) + self:T(self.lid.."Engaging Target") - -- Handle events: - self:UnHandleEvent(EVENTS.Birth) - self:UnHandleEvent(EVENTS.Dead) - self:UnHandleEvent(EVENTS.RemoveUnit) + if Target:IsInstanceOf("TARGET") then + self.engage.Target=Target + else + self.engage.Target=TARGET:New(Target) + end + + -- Target coordinate. + self.engage.Coordinate=UTILS.DeepCopy(self.engage.Target:GetCoordinate()) + - -- Call OPSGROUP function. - self:GetParent(self).onafterStop(self, From, Event, To) + local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate, 0.9) + + + -- Backup ROE and alarm state. + self.engage.roe=self:GetROE() + self.engage.alarmstate=self:GetAlarmstate() + + -- Switch ROE and alarm state. + self:SwitchAlarmstate(ENUMS.AlarmState.Auto) + self:SwitchROE(ENUMS.ROE.OpenFire) + + -- ID of current waypoint. + local uid=self:GetWaypointCurrent().uid + + -- Add waypoint after current. + self.engage.Waypoint=self:AddWaypoint(intercoord, nil, uid, Formation, true) + + -- Set if we want to resume route after reaching the detour waypoint. + self.engage.Waypoint.detour=1 + end -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Events DCS -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Event function handling the birth of a unit. +--- Update engage target. -- @param #NAVYGROUP self --- @param Core.Event#EVENTDATA EventData Event data. -function NAVYGROUP:OnEventBirth(EventData) +function NAVYGROUP:_UpdateEngageTarget() - -- 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 + if self.engage.Target and self.engage.Target:IsAlive() then + + -- Get current position vector. + local vec3=self.engage.Target:GetVec3() - if self.respawning then + if vec3 then + + -- Distance to last known position of target. + local dist=UTILS.VecDist3D(vec3, self.engage.Coordinate:GetVec3()) + + -- Check if target moved more than 100 meters. + if dist>100 then + + --env.info("FF Update Engage Target Moved "..self.engage.Target:GetName()) + + -- Update new position. + self.engage.Coordinate:UpdateFromVec3(vec3) + + -- ID of current waypoint. + local uid=self:GetWaypointCurrent().uid + + -- Remove current waypoint + self:RemoveWaypointByID(self.engage.Waypoint.uid) + + local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate, 0.9) - local function reset() - self.respawning=nil + -- Add waypoint after current. + self.engage.Waypoint=self:AddWaypoint(intercoord, nil, uid, Formation, true) + + -- Set if we want to resume route after reaching the detour waypoint. + self.engage.Waypoint.detour=0 + end - -- Reset switch in 1 sec. This should allow all birth events of n>1 groups to have passed. - -- TODO: Can I do this more rigorously? - self:ScheduleOnce(1, reset) - else - - -- Get element. - local element=self:GetElementByName(unitname) - -- Set element to spawned state. - self:T3(self.lid..string.format("EVENT: Element %s born ==> spawned", element.name)) - self:ElementSpawned(element) + -- Could not get position of target (not alive any more?) ==> Disengage. + self:Disengage() + + end + + else + + -- Target not alive any more ==> Disengage. + self:Disengage() + + end + +end + +--- On after "Disengage" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function NAVYGROUP:onafterDisengage(From, Event, To) + self:T(self.lid.."Disengage Target") + + -- Restore previous ROE and alarm state. + self:SwitchROE(self.engage.roe) + self:SwitchAlarmstate(self.engage.alarmstate) + + -- Get current task + local task=self:GetTaskCurrent() + + -- Get if current task is ground attack. + if task and task.dcstask.id==AUFTRAG.SpecialTask.GROUNDATTACK then + self:T(self.lid.."Disengage with current task GROUNDATTACK ==> Task Done!") + self:TaskDone(task) + end + + -- Remove current waypoint + if self.engage.Waypoint then + self:RemoveWaypointByID(self.engage.Waypoint.uid) + end + + -- Check group is done + self:_CheckGroupDone(1) +end + +--- On after "OutOfAmmo" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function NAVYGROUP:onafterOutOfAmmo(From, Event, To) + self:T(self.lid..string.format("Group is out of ammo at t=%.3f", timer.getTime())) + + -- Check if we want to retreat once out of ammo. + if self.retreatOnOutOfAmmo then + self:__Retreat(-1) + return + end + + -- Third, check if we want to RTZ once out of ammo. + 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 + +--- On after "RTZ" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Zone#ZONE Zone The zone to return to. +-- @param #number Formation Formation of the group. +function NAVYGROUP:onafterRTZ(From, Event, To, Zone, Formation) + + -- Zone. + local zone=Zone or self.homezone + + -- Cancel all missions in the queue. + self:CancelAllMissions() + + if zone then + + if self:IsInZone(zone) then + self:Returned() + else + + -- Debug info. + self:T(self.lid..string.format("RTZ to Zone %s", zone:GetName())) - end - - end + local Coordinate=zone:GetRandomCoordinate() -end - ---- Flightgroup event function handling the crash of a unit. --- @param #NAVYGROUP self --- @param Core.Event#EVENTDATA EventData Event data. -function NAVYGROUP:OnEventDead(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:T(self.lid..string.format("EVENT: Unit %s dead!", EventData.IniUnitName)) - - local unit=EventData.IniUnit - local group=EventData.IniGroup - local unitname=EventData.IniUnitName - - -- Get element. - local element=self:GetElementByName(unitname) - - if element then - self:T(self.lid..string.format("EVENT: Element %s dead ==> destroyed", element.name)) - self:ElementDestroyed(element) + -- ID of current waypoint. + local uid=self:GetWaypointCurrentUID() + + -- Add waypoint after current. + local wp=self:AddWaypoint(Coordinate, nil, uid, Formation, true) + + -- Set if we want to resume route after reaching the detour waypoint. + wp.detour=0 + end - + + else + self:T(self.lid.."ERROR: No RTZ zone given!") end end ---- Flightgroup event function handling the crash of a unit. + +--- On after "Returned" event. -- @param #NAVYGROUP self --- @param Core.Event#EVENTDATA EventData Event data. -function NAVYGROUP:OnEventRemoveUnit(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 - - -- Get element. - local element=self:GetElementByName(unitname) - - if element then - self:T(self.lid..string.format("EVENT: Element %s removed ==> dead", element.name)) - self:ElementDead(element) - end +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function NAVYGROUP:onafterReturned(From, Event, To) + -- Debug info. + self:T(self.lid..string.format("Group returned")) + + if self.legion then + -- Debug info. + self:T(self.lid..string.format("Adding group back to warehouse stock")) + + -- Add asset back in 10 seconds. + self.legion:__AddAsset(10, self.group, 1) end end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Routing ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Add an a waypoint to the route. -- @param #NAVYGROUP self --- @param Core.Point#COORDINATE Coordinate The coordinate of the waypoint. Use COORDINATE:SetAltitude(altitude) to define the altitude. +-- @param Core.Point#COORDINATE Coordinate The coordinate of the waypoint. Use `COORDINATE:SetAltitude()` to define the altitude. -- @param #number Speed Speed in knots. Default is default cruise speed or 70% of max speed. -- @param #number AfterWaypointWithID Insert waypoint after waypoint given ID. Default is to insert as last waypoint. --- @param #number Depth Depth at waypoint in meters. Only for submarines. +-- @param #number Depth Depth at waypoint in feet. Only for submarines. -- @param #boolean Updateroute If true or nil, call UpdateRoute. If false, no call. -- @return Ops.OpsGroup#OPSGROUP.Waypoint Waypoint table. function NAVYGROUP:AddWaypoint(Coordinate, Speed, AfterWaypointWithID, Depth, Updateroute) - -- Check if a coordinate was given or at least a positionable. - if not Coordinate:IsInstanceOf("COORDINATE") then - if Coordinate:IsInstanceOf("POSITIONABLE") or Coordinate:IsInstanceOf("ZONE_BASE") then - self:T(self.lid.."WARNING: Coordinate is not a COORDINATE but a POSITIONABLE or ZONE. Trying to get coordinate") - Coordinate=Coordinate:GetCoordinate() - else - self:E(self.lid.."ERROR: Coordinate is neither a COORDINATE nor any POSITIONABLE or ZONE!") - return nil - end - end + -- Create coordinate. + local coordinate=self:_CoordinateFromObject(Coordinate) -- Set waypoint index. local wpnumber=self:GetWaypointIndexAfterID(AfterWaypointWithID) - -- Check if final waypoint is still passed. - if wpnumber>self.currentwp then - self.passedfinalwp=false - end - -- Speed in knots. Speed=Speed or self:GetSpeedCruise() -- Create a Naval waypoint. - local wp=Coordinate:WaypointNaval(UTILS.KnotsToKmph(Speed), Depth) + local wp=coordinate:WaypointNaval(UTILS.KnotsToKmph(Speed), Depth) -- Create waypoint data table. local waypoint=self:_CreateWaypoint(wp) + -- Set altitude. + if Depth then + waypoint.alt=UTILS.FeetToMeters(Depth) + end + -- Add waypoint to table. self:_AddWaypoint(waypoint, wpnumber) @@ -137127,7 +154855,7 @@ function NAVYGROUP:AddWaypoint(Coordinate, Speed, AfterWaypointWithID, Depth, Up -- Update route. if Updateroute==nil or Updateroute==true then - self:_CheckGroupDone(1) + self:__UpdateRoute(-0.01) end return waypoint @@ -137135,31 +154863,24 @@ end --- Initialize group parameters. Also initializes waypoints if self.waypoints is nil. -- @param #NAVYGROUP self +-- @param #table Template Template used to init the group. Default is `self.template`. -- @return #NAVYGROUP self -function NAVYGROUP:_InitGroup() +function NAVYGROUP:_InitGroup(Template) -- First check if group was already initialized. if self.groupinitialized then - self:E(self.lid.."WARNING: Group was already initialized!") + self:T(self.lid.."WARNING: Group was already initialized! Will NOT do it again!") return end -- Get template of group. - self.template=self.group:GetTemplate() + local template=Template or self:_GetTemplate() - -- Define category. - self.isAircraft=false - self.isNaval=true - self.isGround=false - - --TODO: Submarine check - --self.isSubmarine=self.group:IsSubmarine() - -- Ships are always AI. self.isAI=true -- Is (template) group late activated. - self.isLateActivated=self.template.lateActivation + self.isLateActivated=template.lateActivation -- Naval groups cannot be uncontrolled. self.isUncontrolled=false @@ -137167,6 +154888,13 @@ function NAVYGROUP:_InitGroup() -- Max speed in km/h. self.speedMax=self.group:GetSpeedMax() + -- Is group mobile? + if self.speedMax>3.6 then + self.isMobile=true + else + self.isMobile=false + end + -- Cruise speed: 70% of max speed. self.speedCruise=self.speedMax*0.7 @@ -137175,8 +154903,8 @@ function NAVYGROUP:_InitGroup() -- Radio parameters from template. Default is set on spawn if not modified by the user. self.radio.On=true -- Radio is always on for ships. - self.radio.Freq=tonumber(self.template.units[1].frequency)/1000000 - self.radio.Modu=tonumber(self.template.units[1].modulation) + self.radio.Freq=tonumber(template.units[1].frequency)/1000000 + self.radio.Modu=tonumber(template.units[1].modulation) -- Set default formation. No really applicable for ships. self.optionDefault.Formation="Off Road" @@ -137192,63 +154920,19 @@ function NAVYGROUP:_InitGroup() -- Get all units of the group. local units=self.group:GetUnits() + + -- DCS group. + local dcsgroup=Group.getByName(self.groupname) + local size0=dcsgroup:getInitialSize() - for _,_unit in pairs(units) do - local unit=_unit --Wrapper.Unit#UNIT - - -- Get unit template. - local unittemplate=unit:GetTemplate() - - local element={} --#NAVYGROUP.Element - element.name=unit:GetName() - element.unit=unit - element.status=OPSGROUP.ElementStatus.INUTERO - element.typename=unit:GetTypeName() - element.skill=unittemplate.skill or "Unknown" - element.ai=true - element.category=element.unit:GetUnitCategory() - element.categoryname=element.unit:GetCategoryName() - element.size, element.length, element.height, element.width=unit:GetObjectSize() - element.ammo0=self:GetAmmoUnit(unit, false) - - -- Debug text. - if self.verbose>=2 then - local text=string.format("Adding element %s: status=%s, skill=%s, category=%s (%d), size: %.1f (L=%.1f H=%.1f W=%.1f)", - element.name, element.status, element.skill, element.categoryname, element.category, element.size, element.length, element.height, element.width) - self:I(self.lid..text) - end - - -- Add element to table. - table.insert(self.elements, element) - - -- Get Descriptors. - self.descriptors=self.descriptors or unit:GetDesc() - - -- Set type name. - self.actype=self.actype or unit:GetTypeName() - - if unit:IsAlive() then - -- Trigger spawned event. - self:ElementSpawned(element) - end - + -- Quick check. + if #units~=size0 then + self:E(self.lid..string.format("ERROR: Got #units=%d but group consists of %d units!", #units, size0)) end - - - -- Debug info. - if self.verbose>=1 then - local text=string.format("Initialized Navy Group %s:\n", self.groupname) - text=text..string.format("Unit type = %s\n", self.actype) - text=text..string.format("Speed max = %.1f Knots\n", UTILS.KmphToKnots(self.speedMax)) - text=text..string.format("Speed cruise = %.1f Knots\n", UTILS.KmphToKnots(self.speedCruise)) - text=text..string.format("Elements = %d\n", #self.elements) - text=text..string.format("Waypoints = %d\n", #self.waypoints) - text=text..string.format("Radio = %.1f MHz %s %s\n", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu), tostring(self.radio.On)) - text=text..string.format("Ammo = %d (G=%d/R=%d/M=%d/T=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Missiles, self.ammo.Torpedos) - text=text..string.format("FSM state = %s\n", self:GetState()) - text=text..string.format("Is alive = %s\n", tostring(self:IsAlive())) - text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated())) - self:I(self.lid..text) + + -- Add elemets. + for _,unit in pairs(units) do + self:_AddElementByName(unit:GetName()) end -- Init done. @@ -137290,21 +154974,13 @@ function NAVYGROUP:_CheckFreePath(DistanceMax, dx) offsetY=5.01 end - -- Current coordinate. - --local coordinate=self:GetCoordinate():SetAltitude(offsetY, true) - local vec3=self:GetVec3() vec3.y=offsetY -- Current heading. local heading=self:GetHeading() - -- Check from 500 meters in front. - --coordinate=coordinate:Translate(500, heading, true) - local function LoS(dist) - --local checkcoord=coordinate:Translate(dist, heading, true) - --return coordinate:IsLOS(checkcoord, offsetY) local checkvec3=UTILS.VecTranslate(vec3, dist, heading) local los=land.isVisible(vec3, checkvec3) return los @@ -137332,7 +155008,7 @@ function NAVYGROUP:_CheckFreePath(DistanceMax, dx) local los=LoS(x) -- Debug message. - self:T2(self.lid..string.format("N=%d: xmin=%.1f xmax=%.1f x=%.1f d=%.3f los=%s", N, xmin, xmax, x, d, tostring(los))) + self:T(self.lid..string.format("N=%d: xmin=%.1f xmax=%.1f x=%.1f d=%.3f los=%s", N, xmin, xmax, x, d, tostring(los))) if los and d<=eps then return x @@ -137350,8 +155026,9 @@ function NAVYGROUP:_CheckFreePath(DistanceMax, dx) return 0 end + local _check=check() - return check() + return _check end --- Check if group is turning. @@ -137506,6 +155183,9 @@ end -- @param #NAVYGROUP self -- @return #boolean If true, a path was found. function NAVYGROUP:_FindPathToNextWaypoint() + self:T3(self.lid.."Path finding") + + --TODO: Do not create a new ASTAR object each time this function is called but make it self.astar and reuse. Should be better for performance. -- Pathfinding A* local astar=ASTAR:New() @@ -137516,6 +155196,11 @@ function NAVYGROUP:_FindPathToNextWaypoint() -- Next waypoint. local wpnext=self:GetWaypointNext() + -- No next waypoint. + if wpnext==nil then + return + end + -- Next waypoint coordinate. local nextwp=wpnext.coordinate @@ -137532,16 +155217,21 @@ function NAVYGROUP:_FindPathToNextWaypoint() -- Set end coordinate. astar:SetEndCoordinate(nextwp) - + -- Distance to next waypoint. local dist=position:Get2DDistance(nextwp) + -- Check distance >= 5 meters. + if dist<5 then + return + end + local boxwidth=dist*2 local spacex=dist*0.1 local delta=dist/10 -- Create a grid of nodes. We only want nodes of surface type water. - astar:CreateGrid({land.SurfaceType.WATER}, boxwidth, spacex, delta, delta*2, self.Debug) + astar:CreateGrid({land.SurfaceType.WATER}, boxwidth, spacex, delta, delta, self.verbose>10) -- Valid neighbour nodes need to have line of sight. astar:SetValidNeighbourLoS(self.pathCorridor) @@ -137568,7 +155258,9 @@ function NAVYGROUP:_FindPathToNextWaypoint() uid=wp.uid -- Debug: smoke and mark path. - --node.coordinate:MarkToAll(string.format("Path node #%d", i)) + if self.verbose>=10 then + node.coordinate:MarkToAll(string.format("Path node #%d", i)) + end end @@ -137611,7 +155303,6 @@ end -- ### Author: **funkyfranky** -- -- == --- -- @module Ops.ArmyGroup -- @image OPS_ArmyGroup.png @@ -137622,19 +155313,21 @@ end -- @field #boolean formationPerma Formation that is used permanently and overrules waypoint formations. -- @field #boolean isMobile If true, group is mobile. -- @field #ARMYGROUP.Target engage Engage target. --- @field #boolean retreatOnOutOfAmmo If true, the group will automatically retreat when out of ammo. Needs a retreat zone! -- @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 +--- *Your soul may belong to Jesus, but your ass belongs to the marines.* -- Eugene B Sledge -- -- === --- --- ![Banner Image](..\Presentations\OPS\ArmyGroup\_Main.png) --- +-- -- # The ARMYGROUP Concept -- --- This class enhances naval groups. +-- This class enhances ground groups. -- -- @field #ARMYGROUP ARMYGROUP = { @@ -137643,33 +155336,28 @@ ARMYGROUP = { engage = {}, } ---- Army group element. --- @type ARMYGROUP.Element --- @field #string name Name of the element, i.e. the unit. --- @field Wrapper.Unit#UNIT unit The UNIT object. --- @field #string status The element status. --- @field #string typename Type name. --- @field #number length Length of element in meters. --- @field #number width Width of element in meters. --- @field #number height Height of element in meters. - ---- Target +--- Engage Target. -- @type ARMYGROUP.Target -- @field Ops.Target#TARGET Target The target. -- @field Core.Point#COORDINATE Coordinate Last known coordinate of the target. +-- @field Ops.OpsGroup#OPSGROUP.Waypoint Waypoint the waypoint created to go to the target. +-- @field #number Speed Speed in knots. +-- @field #string Formation Formation used in the engagement. +-- @field #number roe ROE backup. +-- @field #number alarmstate Alarm state backup. --- Army Group version. -- @field #string version -ARMYGROUP.version="0.4.0" +ARMYGROUP.version="0.7.9" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Retreat. -- TODO: Suppression of fire. -- TODO: Check if group is mobile. -- TODO: F10 menu. +-- DONE: Retreat. -- DONE: Rearm. Specify a point where to go and wait until ammo is full. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -137678,20 +155366,28 @@ ARMYGROUP.version="0.4.0" --- Create a new ARMYGROUP class object. -- @param #ARMYGROUP self --- @param Wrapper.Group#GROUP Group The group object. Can also be given by its group name as `#string`. +-- @param Wrapper.Group#GROUP group The GROUP object. Can also be given by its group name as `#string`. -- @return #ARMYGROUP self -function ARMYGROUP:New(Group) - +function ARMYGROUP:New(group) + + -- First check if we already have an OPS group for this group. + local og=_DATABASE:GetOpsGroup(group) + if og then + og:I(og.lid..string.format("WARNING: OPS group already exists in data base!")) + return og + end + -- Inherit everything from FSM class. - local self=BASE:Inherit(self, OPSGROUP:New(Group)) -- #ARMYGROUP + local self=BASE:Inherit(self, OPSGROUP:New(group)) -- #ARMYGROUP -- Set some string id for output to DCS.log file. self.lid=string.format("ARMYGROUP %s | ", self.groupname) -- Defaults - self.isArmygroup=true self:SetDefaultROE() self:SetDefaultAlarmstate() + self:SetDefaultEPLRS(self.isEPLRS) + self:SetDefaultEmission() self:SetDetection() self:SetPatrolAdInfinitum(false) self:SetRetreatZones() @@ -137700,39 +155396,279 @@ function ARMYGROUP:New(Group) -- From State --> Event --> To State self:AddTransition("*", "FullStop", "Holding") -- Hold position. self:AddTransition("*", "Cruise", "Cruising") -- Cruise along the given route of waypoints. + + self:AddTransition("*", "RTZ", "Returning") -- Group is returning to (home) zone. + self:AddTransition("Holding", "Returned", "Returned") -- Group is returned to (home) zone, e.g. when unloaded from carrier. + self:AddTransition("Returning", "Returned", "Returned") -- Group is returned to (home) zone. self:AddTransition("*", "Detour", "OnDetour") -- Make a detour to a coordinate and resume route afterwards. self:AddTransition("OnDetour", "DetourReached", "Cruising") -- Group reached the detour coordinate. - self:AddTransition("*", "Retreat", "Retreating") -- - self:AddTransition("Retreating", "Retreated", "Retreated") -- + self:AddTransition("*", "Retreat", "Retreating") -- Order a retreat. + self:AddTransition("Retreating", "Retreated", "Retreated") -- Group retreated. - self:AddTransition("Cruising", "EngageTarget", "Engaging") -- Engage a target - self:AddTransition("Holding", "EngageTarget", "Engaging") -- Engage a target - self:AddTransition("OnDetour", "EngageTarget", "Engaging") -- Engage a target - self:AddTransition("Engaging", "Disengage", "Cruising") -- Engage a target + 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 + self:AddTransition("Engaging", "Disengage", "Cruising") -- Disengage and back to cruising. 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 --- ------------------------ - - --- Triggers the FSM event "Stop". Stops the ARMYGROUP and all its event handlers. + --- Triggers the FSM event "Cruise". + -- @function [parent=#ARMYGROUP] Cruise -- @param #ARMYGROUP self + -- @param #number Speed Speed in knots until next waypoint is reached. + -- @param #number Formation Formation. - --- Triggers the FSM event "Stop" after a delay. Stops the ARMYGROUP and all its event handlers. - -- @function [parent=#ARMYGROUP] __Stop + --- Triggers the FSM event "Cruise" after a delay. + -- @function [parent=#ARMYGROUP] __Cruise -- @param #ARMYGROUP self -- @param #number delay Delay in seconds. - + -- @param #number Speed Speed in knots until next waypoint is reached. + -- @param #number Formation Formation. + + --- On after "Cruise" event. + -- @function [parent=#ARMYGROUP] OnAfterCruise + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number Speed Speed in knots until next waypoint is reached. + -- @param #number Formation Formation. + + + --- Triggers the FSM event "FullStop". + -- @function [parent=#ARMYGROUP] FullStop + -- @param #ARMYGROUP self + + --- Triggers the FSM event "FullStop" after a delay. + -- @function [parent=#ARMYGROUP] __FullStop + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "FullStop" event. + -- @function [parent=#ARMYGROUP] OnAfterFullStop + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "RTZ". + -- @function [parent=#ARMYGROUP] RTZ + -- @param #ARMYGROUP self + + --- Triggers the FSM event "RTZ" after a delay. + -- @function [parent=#ARMYGROUP] __RTZ + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "RTZ" event. + -- @function [parent=#ARMYGROUP] OnAfterRTZ + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Returned". + -- @function [parent=#ARMYGROUP] Returned + -- @param #ARMYGROUP self + + --- Triggers the FSM event "Returned" after a delay. + -- @function [parent=#ARMYGROUP] __Returned + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "Returned" event. + -- @function [parent=#ARMYGROUP] OnAfterReturned + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Detour". + -- @function [parent=#ARMYGROUP] Detour + -- @param #ARMYGROUP self + + --- Triggers the FSM event "Detour" after a delay. + -- @function [parent=#ARMYGROUP] __Detour + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "Detour" event. + -- @function [parent=#ARMYGROUP] OnAfterDetour + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "DetourReached". + -- @function [parent=#ARMYGROUP] DetourReached + -- @param #ARMYGROUP self + + --- Triggers the FSM event "DetourReached" after a delay. + -- @function [parent=#ARMYGROUP] __DetourReached + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "DetourReached" event. + -- @function [parent=#ARMYGROUP] OnAfterDetourReached + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Retreat". + -- @function [parent=#ARMYGROUP] Retreat + -- @param #ARMYGROUP self + + --- Triggers the FSM event "Retreat" after a delay. + -- @function [parent=#ARMYGROUP] __Retreat + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "Retreat" event. + -- @function [parent=#ARMYGROUP] OnAfterRetreat + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Retreated". + -- @function [parent=#ARMYGROUP] Retreated + -- @param #ARMYGROUP self + + --- Triggers the FSM event "Retreated" after a delay. + -- @function [parent=#ARMYGROUP] __Retreated + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "Retreated" event. + -- @function [parent=#ARMYGROUP] OnAfterRetreated + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "EngageTarget". + -- @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. + -- @function [parent=#ARMYGROUP] __EngageTarget + -- @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. + + + --- On after "EngageTarget" event. + -- @function [parent=#ARMYGROUP] OnAfterEngageTarget + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @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. + + + --- Triggers the FSM event "Disengage". + -- @function [parent=#ARMYGROUP] Disengage + -- @param #ARMYGROUP self + + --- Triggers the FSM event "Disengage" after a delay. + -- @function [parent=#ARMYGROUP] __Disengage + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "Disengage" event. + -- @function [parent=#ARMYGROUP] OnAfterDisengage + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Rearm". + -- @function [parent=#ARMYGROUP] Rearm + -- @param #ARMYGROUP self + -- @param Core.Point#COORDINATE Coordinate Coordinate where to rearm. + -- @param #number Formation Formation of the group. + + --- Triggers the FSM event "Rearm" after a delay. + -- @function [parent=#ARMYGROUP] __Rearm + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + -- @param Core.Point#COORDINATE Coordinate Coordinate where to rearm. + -- @param #number Formation Formation of the group. + + --- On after "Rearm" event. + -- @function [parent=#ARMYGROUP] OnAfterRearm + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Point#COORDINATE Coordinate Coordinate where to rearm. + -- @param #number Formation Formation of the group. + + + --- Triggers the FSM event "Rearming". + -- @function [parent=#ARMYGROUP] Rearming + -- @param #ARMYGROUP self + + --- Triggers the FSM event "Rearming" after a delay. + -- @function [parent=#ARMYGROUP] __Rearming + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "Rearming" event. + -- @function [parent=#ARMYGROUP] OnAfterRearming + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Rearmed". + -- @function [parent=#ARMYGROUP] Rearmed + -- @param #ARMYGROUP self + + --- Triggers the FSM event "Rearmed" after a delay. + -- @function [parent=#ARMYGROUP] __Rearmed + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "Rearmed" event. + -- @function [parent=#ARMYGROUP] OnAfterRearmed + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + -- TODO: Add pseudo functions. - -- Init waypoints. - self:InitWaypoints() + self:_InitWaypoints() -- Initialize the group. self:_InitGroup() @@ -137740,18 +155676,20 @@ function ARMYGROUP:New(Group) -- Handle events: 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.RemoveUnit, self.OnEventRemoveUnit) + self:HandleEvent(EVENTS.Hit, self.OnEventHit) -- Start the status monitoring. - self:__Status(-1) + self.timerStatus=TIMER:New(self.Status, self):Start(1, 30) -- Start queue update timer. self.timerQueueUpdate=TIMER:New(self._QueueUpdate, self):Start(2, 5) -- Start check zone timer. self.timerCheckZone=TIMER:New(self._CheckInZones, self):Start(2, 30) + + -- Add OPSGROUP to _DATABASE. + _DATABASE:AddOpsGroup(self) return self end @@ -137777,7 +155715,8 @@ end -- @param #ARMYGROUP self -- @return Core.Point#COORDINATE Coordinate of a road closest to the group. function ARMYGROUP:GetClosestRoad() - return self:GetCoordinate():GetClosestPointToRoad() + local coord=self:GetCoordinate():GetClosestPointToRoad() + return coord end --- Get 2D distance to the closest road. @@ -137813,6 +155752,40 @@ function ARMYGROUP:AddTaskFireAtPoint(Coordinate, Clock, Radius, Nshots, WeaponT return task end +--- Add a *scheduled* task to fire at a given coordinate. +-- @param #ARMYGROUP self +-- @param #string Clock Time when to start the attack. +-- @param #number Heading Heading min in Degrees. +-- @param #number Alpha Shooting angle in Degrees. +-- @param #number Altitude Altitude in meters. +-- @param #number Radius Radius in meters. Default 100 m. +-- @param #number Nshots Number of shots to fire. Default nil. +-- @param #number WeaponType Type of weapon. Default auto. +-- @param #number Prio Priority of the task. +-- @return Ops.OpsGroup#OPSGROUP.Task The task table. +function ARMYGROUP:AddTaskBarrage(Clock, Heading, Alpha, Altitude, Radius, Nshots, WeaponType, Prio) + + Heading=Heading or 0 + + Alpha=Alpha or 60 + + Altitude=Altitude or 100 + + local distance=Altitude/math.tan(math.rad(Alpha)) + + local a=self:GetVec2() + + local vec2=UTILS.Vec2Translate(a, distance, Heading) + + --local coord=COORDINATE:NewFromVec2(vec2):MarkToAll("Fire At Point",ReadOnly,Text) + + local DCStask=CONTROLLABLE.TaskFireAtPoint(nil, vec2, Radius, Nshots, WeaponType, Altitude) + + local task=self:AddTask(DCStask, Clock, nil, Prio) + + return task +end + --- Add a *waypoint* task to fire at a given coordinate. -- @param #ARMYGROUP self -- @param Core.Point#COORDINATE Coordinate Coordinate of the target. @@ -137852,6 +155825,26 @@ function ARMYGROUP:AddTaskAttackGroup(TargetGroup, WeaponExpend, WeaponType, Clo return task end +--- Add a *scheduled* task to transport group(s). +-- @param #ARMYGROUP self +-- @param Core.Set#SET_GROUP GroupSet Set of cargo groups. Can also be a singe @{Wrapper.Group#GROUP} object. +-- @param Core.Zone#ZONE PickupZone Zone where the cargo is picked up. +-- @param Core.Zone#ZONE DeployZone Zone where the cargo is delivered to. +-- @param #string Clock Time when to start the attack. +-- @param #number Prio Priority of the task. +-- @return Ops.OpsGroup#OPSGROUP.Task The task table. +function ARMYGROUP:AddTaskCargoGroup(GroupSet, PickupZone, DeployZone, Clock, Prio) + + local DCStask={} + DCStask.id="CargoTransport" + DCStask.params={} + DCStask.params.cargoqueu=1 + + local task=self:AddTask(DCStask, Clock, nil, Prio) + + return task +end + --- Define a set of possible retreat zones. -- @param #ARMYGROUP self -- @param Core.Set#SET_ZONE RetreatZoneSet The retreat zone set. Default is an empty set. @@ -137870,6 +155863,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. @@ -137897,7 +155931,11 @@ end function ARMYGROUP:IsCombatReady() local combatready=true - if self:IsRearming() or self:IsRetreating() or self.outofAmmo or self:IsEngaging() or self:is("Retreated") or self:IsDead() or self:IsStopped() or self:IsInUtero() then + if self:IsRearming() or self:IsRetreating() or self:IsOutOfAmmo() or self:IsEngaging() or self:IsDead() or self:IsStopped() or self:IsInUtero() then + combatready=false + end + + if self:IsPickingup() or self:IsLoading() or self:IsTransporting() or self:IsLoaded() or self:IsCargo() or self:IsCarrier() then combatready=false end @@ -137909,72 +155947,116 @@ end -- Status ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ----- Update status. --- @param #ARMYGROUP self -function ARMYGROUP:onbeforeStatus(From, Event, To) - - if self:IsDead() then - self:T(self.lid..string.format("Onbefore Status DEAD ==> false")) - return false - elseif self:IsStopped() then - self:T(self.lid..string.format("Onbefore Status STOPPED ==> false")) - return false - end - - return true -end - --- Update status. -- @param #ARMYGROUP self -function ARMYGROUP:onafterStatus(From, Event, To) +function ARMYGROUP:Status() -- FSM state. local fsmstate=self:GetState() - if self:IsAlive() then + -- Is group alive? + local alive=self:IsAlive() - --- - -- Detection - --- - - -- Check if group has detected any units. - if self.detectionOn then - self:_CheckDetectedUnits() - end - - -- Check ammo status. - self:_CheckAmmoStatus() + -- Check that group EXISTS and is ACTIVE. + if alive then -- Update position etc. self:_UpdatePosition() - - -- Check if group got stuck. - self:_CheckStuck() + -- Check if group has detected any units. + self:_CheckDetectedUnits() + + -- Check ammo status. + self:_CheckAmmoStatus() + -- Check damage of elements and group. self:_CheckDamage() + + -- Check if group got stuck. + self:_CheckStuck() -- Update engagement. if self:IsEngaging() then self:_UpdateEngageTarget() end + -- Check if group is waiting. + if self:IsWaiting() then + if self.Twaiting and self.dTwait then + if timer.getAbsTime()>self.Twaiting+self.dTwait then + self.Twaiting=nil + self.dTwait=nil + if self:_CountPausedMissions()>0 then + self:UnpauseMission() + else + self:Cruise() + end + end + end + end + + else + -- Check damage of elements and group. + self:_CheckDamage() + end + + -- Check that group EXISTS. + if alive~=nil then + if self.verbose>=1 then + + -- Number of elements. + local nelem=self:CountElements() + local Nelem=#self.elements -- Get number of tasks and missions. local nTaskTot, nTaskSched, nTaskWP=self:CountRemainingTasks() local nMissions=self:CountRemainingMissison() - local roe=self:GetROE() - local alarm=self:GetAlarmstate() - local speed=UTILS.MpsToKnots(self.velocity) + -- ROE and Alarm State. + local roe=self:GetROE() or -1 + local als=self:GetAlarmstate() or -1 + + -- Waypoint stuff. + local wpidxCurr=self.currentwp + local wpuidCurr=self:GetWaypointUIDFromIndex(wpidxCurr) or 0 + local wpidxNext=self:GetWaypointIndexNext() or 0 + local wpuidNext=self:GetWaypointUIDFromIndex(wpidxNext) or 0 + local wpN=#self.waypoints or 0 + local wpF=tostring(self.passedfinalwp) + + -- Speed. + local speed=UTILS.MpsToKnots(self.velocity or 0) local speedEx=UTILS.MpsToKnots(self:GetExpectedSpeed()) - local formation=self.option.Formation or "unknown" - local ammo=self:GetAmmoTot() + + -- Altitude. + local alt=self.position and self.position.y or 0 + + -- Heading in degrees. + local hdg=self.heading or 0 + + -- TODO: GetFormation function. + local formation=self.option.Formation or "unknown" + + -- Life points. + local life=self.life or 0 + + -- Total ammo. + local ammo=self:GetAmmoTot().Total + + -- Detected units. + local ndetected=self.detectionOn and tostring(self.detectedunits:Count()) or "Off" + + -- Get cargo weight. + local cargo=0 + for _,_element in pairs(self.elements) do + local element=_element --Ops.OpsGroup#OPSGROUP.Element + cargo=cargo+element.weightCargo + end -- Info text. - local text=string.format("%s [ROE-AS=%d-%d T/M=%d/%d]: Wp=%d/%d-->%d (final %s), Life=%.1f, Speed=%.1f (%d), Heading=%03d, Ammo=%d", - fsmstate, roe, alarm, nTaskTot, nMissions, self.currentwp, #self.waypoints, self:GetWaypointIndexNext(), tostring(self.passedfinalwp), self.life or 0, speed, speedEx, self.heading, ammo.Total) + local text=string.format("%s [%d/%d]: ROE/AS=%d/%d | T/M=%d/%d | Wp=%d[%d]-->%d[%d]/%d [%s] | Life=%.1f | v=%.1f (%d) | Hdg=%03d | Ammo=%d | Detect=%s | Cargo=%.1f", + fsmstate, nelem, Nelem, roe, als, nTaskTot, nMissions, wpidxCurr, wpuidCurr, wpidxNext, wpuidNext, wpN, wpF, life, speed, speedEx, hdg, ammo, ndetected, cargo) self:I(self.lid..text) end @@ -137982,11 +156064,64 @@ function ARMYGROUP:onafterStatus(From, Event, To) else -- Info text. - local text=string.format("State %s: Alive=%s", fsmstate, tostring(self:IsAlive())) - self:T2(self.lid..text) - + if self.verbose>=1 then + local text=string.format("State %s: Alive=%s", fsmstate, tostring(self:IsAlive())) + self:I(self.lid..text) + end + end + --- + -- Elements + --- + + if self.verbose>=2 then + local text="Elements:" + for i,_element in pairs(self.elements) do + local element=_element --Ops.OpsGroup#OPSGROUP.Element + + local name=element.name + local status=element.status + local unit=element.unit + local life,life0=self:GetLifePoints(element) + + local life0=element.life0 + + -- Get ammo. + local ammo=self:GetAmmoElement(element) + + -- Output text for element. + text=text..string.format("\n[%d] %s: status=%s, life=%.1f/%.1f, guns=%d, rockets=%d, bombs=%d, missiles=%d, cargo=%d/%d kg", + i, name, status, life, life0, ammo.Guns, ammo.Rockets, ammo.Bombs, ammo.Missiles, element.weightCargo, element.weightMaxCargo) + end + if #self.elements==0 then + text=text.." none!" + end + self:T(self.lid..text) + end + + --- + -- Engage Detected Targets + --- + if self:IsCruising() and self.detectionOn and self.engagedetectedOn then + + local targetgroup, targetdist=self:_GetDetectedTarget() + + -- If we found a group, we engage it. + if targetgroup then + self:T(self.lid..string.format("Engaging target group %s at distance %d meters", targetgroup:GetName(), targetdist)) + self:EngageTarget(targetgroup) + end + + end + + + --- + -- Cargo + --- + + self:_CheckCargoTransport() + --- -- Tasks & Missions @@ -137994,11 +156129,12 @@ function ARMYGROUP:onafterStatus(From, Event, To) self:_PrintTaskAndMissionStatus() - - -- Next status update. - self:__Status(-30) end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- DCS Events ==> See OPSGROUP +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- FSM Events ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -138008,7 +156144,7 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #ARMYGROUP.Element Element The group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The group element. function ARMYGROUP:onafterElementSpawned(From, Event, To, Element) self:T(self.lid..string.format("Element spawned %s", Element.name)) @@ -138025,8 +156161,31 @@ end function ARMYGROUP:onafterSpawned(From, Event, To) self:T(self.lid..string.format("Group spawned!")) + -- Debug info. + if self.verbose>=1 then + local text=string.format("Initialized Army Group %s:\n", self.groupname) + text=text..string.format("Unit type = %s\n", self.actype) + text=text..string.format("Speed max = %.1f Knots\n", UTILS.KmphToKnots(self.speedMax)) + text=text..string.format("Speed cruise = %.1f Knots\n", UTILS.KmphToKnots(self.speedCruise)) + text=text..string.format("Weight = %.1f kg\n", self:GetWeightTotal()) + text=text..string.format("Cargo bay = %.1f kg\n", self:GetFreeCargobay()) + text=text..string.format("Has EPLRS = %s\n", tostring(self.isEPLRS)) + text=text..string.format("Elements = %d\n", #self.elements) + text=text..string.format("Waypoints = %d\n", #self.waypoints) + text=text..string.format("Radio = %.1f MHz %s %s\n", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu), tostring(self.radio.On)) + text=text..string.format("Ammo = %d (G=%d/R=%d/M=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Missiles) + text=text..string.format("FSM state = %s\n", self:GetState()) + text=text..string.format("Is alive = %s\n", tostring(self:IsAlive())) + text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated())) + self:I(self.lid..text) + end + -- Update position. self:_UpdatePosition() + + -- Not dead or destroyed yet. + self.isDead=false + self.isDestroyed=false if self.isAI then @@ -138036,6 +156195,18 @@ function ARMYGROUP: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) + + -- Set default Invisible. + self:SwitchInvisible(self.option.Invisible) + + -- Set default Immortal. + self:SwitchImmortal(self.option.Immortal) + -- Set TACAN to default. self:_SwitchTACAN() @@ -138048,18 +156219,108 @@ function ARMYGROUP:onafterSpawned(From, Event, To) -- Formation if not self.option.Formation then - self.option.Formation=self.optionDefault.Formation + -- Will be set in update route. + --self.option.Formation=self.optionDefault.Formation end + -- Number of waypoints. + local Nwp=#self.waypoints + + -- Update route. + if Nwp>1 and self.isMobile then + self:T(self.lid..string.format("Got %d waypoints on spawn ==> Cruise in -1.0 sec!", Nwp)) + self:__Cruise(-1, nil, self.option.Formation) + else + self:T(self.lid.."No waypoints on spawn ==> Full Stop!") + self:FullStop() + end + end - -- Update route. - if #self.waypoints>1 then - self:Cruise(nil, self.option.Formation or self.optionDefault.Formation) - else - self:FullStop() +end + +--- On before "UpdateRoute" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number n Next waypoint index. Default is the one coming after that one that has been passed last. +-- @param #number N Waypoint Max waypoint index to be included in the route. Default is the final waypoint. +-- @param #number Speed Speed in knots. Default cruise speed. +-- @param #number Formation Formation of the group. +function ARMYGROUP:onbeforeUpdateRoute(From, Event, To, n, N, Speed, Formation) + + -- Is transition allowed? We assume yes until proven otherwise. + local allowed=true + local trepeat=nil + + if self:IsWaiting() then + self:T(self.lid.."Update route denied. Group is WAITING!") + return false + elseif self:IsInUtero() then + self:T(self.lid.."Update route denied. Group is INUTERO!") + return false + elseif self:IsDead() then + self:T(self.lid.."Update route denied. Group is DEAD!") + return false + elseif self:IsStopped() then + self:T(self.lid.."Update route denied. Group is STOPPED!") + return false + 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. + if self.taskcurrent>0 then + + -- Get the current task. Must not be executing already. + local task=self:GetTaskByID(self.taskcurrent) + + if task then + if task.dcstask.id=="PatrolZone" then + -- For patrol zone, we need to allow the update as we insert new waypoints. + self:T2(self.lid.."Allowing update route for Task: PatrolZone") + elseif task.dcstask.id=="ReconMission" then + -- For recon missions, we need to allow the update as we insert new waypoints. + 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") + 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))) + allowed=false + end + else + -- Now this can happen, if we directly use TaskExecute as the task is not in the task queue and cannot be removed. Therefore, also directly executed tasks should be added to the queue! + self:T(self.lid..string.format("WARNING: before update route taskcurrent=%d (>0!) but no task?!", self.taskcurrent)) + -- Anyhow, a task is running so we do not allow to update the route! + allowed=false + end + end + + -- Not good, because mission will never start. Better only check if there is a current task! + --if self.currentmission then + --end + + -- Only AI flights. + if not self.isAI then + allowed=false + end + + -- Debug info. + self:T2(self.lid..string.format("Onbefore Updateroute in state %s: allowed=%s (repeat in %s)", self:GetState(), tostring(allowed), tostring(trepeat))) + + -- Try again? + if trepeat then + self:__UpdateRoute(trepeat, n) + end + + return allowed end --- On after "UpdateRoute" event. @@ -138067,91 +156328,133 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #number n Waypoint number. Default is next waypoint. +-- @param #number n Next waypoint index. Default is the one coming after that one that has been passed last. +-- @param #number N Waypoint Max waypoint index to be included in the route. Default is the final waypoint. -- @param #number Speed Speed in knots. Default cruise speed. -- @param #number Formation Formation of the group. -function ARMYGROUP:onafterUpdateRoute(From, Event, To, n, Speed, Formation) - - -- Debug info. - local text=string.format("Update route n=%s, Speed=%s, Formation=%s", tostring(n), tostring(Speed), tostring(Formation)) - self:T(self.lid..text) +function ARMYGROUP:onafterUpdateRoute(From, Event, To, n, N, Speed, Formation) -- Update route from this waypoint number onwards. n=n or self:GetWaypointIndexNext(self.adinfinitum) - -- Update waypoint tasks, i.e. inject WP tasks into waypoint table. - self:_UpdateWaypointTasks(n) + -- Max index. + N=N or #self.waypoints + N=math.min(N, #self.waypoints) - -- Waypoints. + -- Debug info. + local text=string.format("Update route state=%s: n=%s, N=%s, Speed=%s, Formation=%s", self:GetState(), tostring(n), tostring(N), tostring(Speed), tostring(Formation)) + self:T(self.lid..text) + + -- Waypoints including addtional wp onroad. local waypoints={} -- Next waypoint. - local wp=UTILS.DeepCopy(self.waypoints[n]) --Ops.OpsGroup#OPSGROUP.Waypoint + local wp=self.waypoints[n] --Ops.OpsGroup#OPSGROUP.Waypoint - -- Do we want to drive on road to the next wp? - local onroad=wp.action==ENUMS.Formation.Vehicle.OnRoad - - -- Speed. - if Speed then - wp.speed=UTILS.KnotsToMps(Speed) - else - -- Take default waypoint speed. But make sure speed>0 if patrol ad infinitum. - if self.adinfinitum and wp.speed<0.1 then - wp.speed=UTILS.KmphToMps(self.speedCruise) + -- Formation at the current position. + local formation0=wp.action + if formation0==ENUMS.Formation.Vehicle.OnRoad then + if wp.roadcoord then + if wp.roaddist>10 then + formation0=ENUMS.Formation.Vehicle.OffRoad + end + else + formation0=ENUMS.Formation.Vehicle.OffRoad end end - - -- Formation. - if self.formationPerma then - wp.action=self.formationPerma - elseif Formation then - wp.action=Formation - end - - -- Current set formation. - self.option.Formation=wp.action - - -- Current set speed in m/s. - self.speedWp=wp.speed - - -- Add waypoint in between because this waypoint is "On Road" but lies "Off Road". - if onroad then - - -- The real waypoint is actually off road. - wp.action=ENUMS.Formation.Vehicle.OffRoad - - -- Add "On Road" waypoint in between. - local wproad=wp.roadcoord:WaypointGround(wp.speed, ENUMS.Formation.Vehicle.OnRoad) --Ops.OpsGroup#OPSGROUP.Waypoint - - -- Insert road waypoint. - table.insert(waypoints, wproad) - end - - -- Add waypoint. - table.insert(waypoints, wp) - - -- Apply formation at the current position or it will only be changed when reaching the next waypoint. - local formation=ENUMS.Formation.Vehicle.OffRoad - if wp.action~=ENUMS.Formation.Vehicle.OnRoad then - formation=wp.action - end -- Current point. - local current=self:GetCoordinate():WaypointGround(UTILS.MpsToKmph(self.speedWp), formation) + local current=self:GetCoordinate():WaypointGround(UTILS.MpsToKmph(self.speedWp), formation0) --ENUMS.Formation.Vehicle.OffRoad) table.insert(waypoints, 1, current) + + -- Loop over waypoints. + for j=n, N do + + -- Index of previous waypoint. + local i=j-1 + + -- If we go to the first waypoint j=1 ==> i=0, so we take the last waypoint passed. E.g. when adinfinitum and passed final waypoint. + if i==0 then + i=self.currentwp + end + + -- Next waypoint. + local wp=UTILS.DeepCopy(self.waypoints[j]) --Ops.OpsGroup#OPSGROUP.Waypoint - -- Insert a point on road. - if onroad then - local current=self:GetClosestRoad():WaypointGround(UTILS.MpsToKmph(self.speedWp), ENUMS.Formation.Vehicle.OnRoad) - table.insert(waypoints, 2, current) + -- Previous waypoint. Index is i and not i-1 because we added the current position. + local wp0=self.waypoints[i] --Ops.OpsGroup#OPSGROUP.Waypoint + + --local text=string.format("FF Update: i=%d, wp[i]=%s, wp[i-1]=%s", i, wp.action, wp0.action) + --env.info(text) + + -- Speed. + if Speed then + wp.speed=UTILS.KnotsToMps(tonumber(Speed)) + else + -- Take default waypoint speed. But make sure speed>0 if patrol ad infinitum. + if wp.speed<0.1 then + wp.speed=UTILS.KmphToMps(self.speedCruise) + end + end + + -- Formation. + if self.formationPerma then + wp.action=self.formationPerma + elseif Formation then + wp.action=Formation + end + + -- Add waypoint in between because this waypoint is "On Road" but lies "Off Road". + if wp.action==ENUMS.Formation.Vehicle.OnRoad and wp0.roaddist>=0 then + + --env.info("FF adding waypoint0 on road #"..i) + + -- Add "On Road" waypoint in between. + local wproad=wp0.roadcoord:WaypointGround(UTILS.MpsToKmph(wp.speed), ENUMS.Formation.Vehicle.OnRoad) --Ops.OpsGroup#OPSGROUP.Waypoint + + -- Insert road waypoint. + table.insert(waypoints, wproad) + end + + -- Add waypoint in between because this waypoint is "On Road" but lies "Off Road". + if wp.action==ENUMS.Formation.Vehicle.OnRoad and wp.roaddist>=0 then + + --env.info("FF adding waypoint on road #"..i) + + -- The real waypoint is actually off road. + wp.action=ENUMS.Formation.Vehicle.OffRoad + + -- Add "On Road" waypoint in between. + local wproad=wp.roadcoord:WaypointGround(UTILS.MpsToKmph(wp.speed), ENUMS.Formation.Vehicle.OnRoad) --Ops.OpsGroup#OPSGROUP.Waypoint + + -- Insert road waypoint. + table.insert(waypoints, wproad) + end + + + -- Add waypoint. + table.insert(waypoints, wp) end + -- First (next wp). + local wp=waypoints[1] --Ops.OpsGroup#OPSGROUP.Waypoint + + -- Current set formation. + self.option.Formation=wp.action + + -- Current set speed in m/s. + self.speedWp=wp.speed + -- Debug output. - if false then + if self.verbose>=10 then for i,_wp in pairs(waypoints) do - local wp=_wp - local text=string.format("WP #%d UID=%d type=%s: Speed=%d m/s, alt=%d m, Action=%s", i, wp.uid and wp.uid or 0, wp.type, wp.speed, wp.alt, wp.action) - self:T(text) + local wp=_wp --Ops.OpsGroup#OPSGROUP.Waypoint + + local text=string.format("WP #%d UID=%d Formation=%s: Speed=%d m/s, Alt=%d m, Type=%s", i, wp.uid and wp.uid or -1, wp.action, wp.speed, wp.alt, wp.type) + + local coord=COORDINATE:NewFromWaypoint(wp):MarkToAll(text) + self:I(text) + end end @@ -138170,7 +156473,7 @@ function ARMYGROUP:onafterUpdateRoute(From, Event, To, n, Speed, Formation) -- Passed final WP ==> Full Stop --- - self:E(self.lid..string.format("WARNING: Passed final WP ==> Full Stop!")) + self:T(self.lid..string.format("WARNING: Passed final WP when UpdateRoute() ==> Full Stop!")) self:FullStop() end @@ -138189,26 +156492,13 @@ function ARMYGROUP:onafterGotoWaypoint(From, Event, To, UID, Speed, Formation) local n=self:GetWaypointIndex(UID) - --env.info(string.format("FF AG Goto waypoint UID=%s Index=%s, Speed=%s, Formation=%s", tostring(UID), tostring(n), tostring(Speed), tostring(Formation))) - if n then - - -- TODO: switch to re-enable waypoint tasks. - if false then - local tasks=self:GetTasksWaypoint(n) - - for _,_task in pairs(tasks) do - local task=_task --Ops.OpsGroup#OPSGROUP.Task - task.status=OPSGROUP.TaskStatus.SCHEDULED - end - - end -- Speed to waypoint. Speed=Speed or self:GetSpeedToWaypoint(n) -- Update the route. - self:UpdateRoute(n, Speed, Formation) + self:__UpdateRoute(-0.01, n, nil, Speed, Formation) end @@ -138250,6 +156540,97 @@ function ARMYGROUP:onafterDetour(From, Event, To, Coordinate, Speed, Formation, end +--- On after "OutOfAmmo" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +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 + local truck, dist=self:FindNearestAmmoSupply(30) + if truck then + self:T(self.lid..string.format("Found Ammo Truck %s [%s]", truck:GetName(), truck:GetTypeName())) + local Coordinate=truck:GetCoordinate() + self:__Rearm(-1, Coordinate) + return + end + end + + -- Second, check if we want to retreat once out of ammo. + if self.retreatOnOutOfAmmo then + self:__Retreat(-1) + return + end + + -- Third, check if we want to RTZ once out of ammo. + if self.rtzOnOutOfAmmo then + self:__RTZ(-1) + end + +end + + +--- On before "Rearm" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Coordinate Coordinate where to rearm. +-- @param #number Formation Formation of the group. +function ARMYGROUP:onbeforeRearm(From, Event, To, Coordinate, Formation) + + local dt=nil + local allowed=true + + -- Pause current mission. + if self:IsOnMission() then + self:T(self.lid.."Rearm command but have current mission ==> Pausing mission!") + self:PauseMission() + dt=-0.1 + allowed=false + end + + -- Disengage. + if self:IsEngaging() then + self:T(self.lid.."Rearm command but currently engaging ==> Disengage!") + self:Disengage() + dt=-0.1 + 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)) + self:__Rearm(dt, Coordinate, Formation) + allowed=false + end + + return allowed +end + --- On after "Rearm" event. -- @param #ARMYGROUP self -- @param #string From From state. @@ -138259,6 +156640,9 @@ end -- @param #number Formation Formation of the group. function ARMYGROUP:onafterRearm(From, Event, To, Coordinate, Formation) + -- Debug info. + self:T(self.lid..string.format("Group send to rearm")) + -- ID of current waypoint. local uid=self:GetWaypointCurrent().uid @@ -138270,6 +156654,121 @@ function ARMYGROUP:onafterRearm(From, Event, To, Coordinate, Formation) end +--- On after "Rearmed" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @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 + +end + +--- On before "RTZ" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Zone#ZONE Zone The zone to return to. +-- @param #number Formation Formation of the group. +function ARMYGROUP:onbeforeRTZ(From, Event, To, Zone, Formation) + + -- Zone. + local zone=Zone or self.homezone + + if zone then + + if (not self.isMobile) and (not self:IsInZone(zone)) then + self:Teleport(zone:GetCoordinate(), 0, true) + self:__RTZ(-1, Zone, Formation) + return false + end + + else + return false + end + + return true +end + +--- On after "RTZ" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Zone#ZONE Zone The zone to return to. +-- @param #number Formation Formation of the group. +function ARMYGROUP:onafterRTZ(From, Event, To, Zone, Formation) + + -- Zone. + local zone=Zone or self.homezone + + -- Cancel all missions in the queue. + self:CancelAllMissions() + + if zone then + + if self:IsInZone(zone) then + self:Returned() + else + + -- Debug info. + self:T(self.lid..string.format("RTZ to Zone %s", zone:GetName())) + + local Coordinate=zone:GetRandomCoordinate() + + -- ID of current waypoint. + local uid=self:GetWaypointCurrentUID() + + -- Add waypoint after current. + local wp=self:AddWaypoint(Coordinate, nil, uid, Formation, true) + + -- Set if we want to resume route after reaching the detour waypoint. + wp.detour=0 + + end + + else + self:T(self.lid.."ERROR: No RTZ zone given!") + end + +end + +--- On after "Returned" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARMYGROUP:onafterReturned(From, Event, To) + + -- Debug info. + self:T(self.lid..string.format("Group returned")) + + if self.legion then + -- Debug info. + self:T(self.lid..string.format("Adding group back to warehouse stock")) + + -- Add asset back in 10 seconds. + self.legion:__AddAsset(10, self.group, 1) + end + +end + --- On after "Rearming" event. -- @param #ARMYGROUP self -- @param #string From From state. @@ -138346,6 +156845,9 @@ function ARMYGROUP:onafterRetreat(From, Event, To, Zone, Formation) -- Set if we want to resume route after reaching the detour waypoint. wp.detour=0 + + -- Cancel all missions. + self:CancelAllMissions() end @@ -138373,16 +156875,38 @@ end -- @param #string Event Event. -- @param #string To To state. -- @param Wrapper.Group#GROUP Group the group to be engaged. -function ARMYGROUP:onbeforeEngageTarget(From, Event, To, Target) +-- @param #number Speed Speed in knots. +-- @param #string Formation Formation used in the engagement. Default `ENUMS.Formation.Vehicle.Vee`. +function ARMYGROUP:onbeforeEngageTarget(From, Event, To, Target, Speed, Formation) + + local dt=nil + local allowed=true local ammo=self:GetAmmoTot() if ammo.Total==0 then - self:E(self.lid.."WARNING: Cannot engage TARGET because no ammo left!") + self:T(self.lid.."WARNING: Cannot engage TARGET because no ammo left!") return false end + + -- Pause current mission. + local mission=self:GetMissionCurrent() + + if mission and mission.type~=AUFTRAG.Type.GROUNDATTACK then + self:T(self.lid.."Engage command but have current mission ==> Pausing mission!") + self:PauseMission() + dt=-0.1 + allowed=false + end - return true + -- Try again... + if dt then + self:T(self.lid..string.format("Trying Engage again in %.2f sec", dt)) + self:__EngageTarget(dt, Target) + allowed=false + end + + return allowed end --- On after "EngageTarget" event. @@ -138391,28 +156915,43 @@ end -- @param #string Event Event. -- @param #string To To state. -- @param Wrapper.Group#GROUP Group the group to be engaged. -function ARMYGROUP:onafterEngageTarget(From, Event, To, Target) +-- @param #number Speed Attack speed in knots. +-- @param #string Formation Formation used in the engagement. Default `ENUMS.Formation.Vehicle.Vee`. +function ARMYGROUP:onafterEngageTarget(From, Event, To, Target, Speed, Formation) + self:T(self.lid.."Engaging Target") + -- Make sure this is a target. if Target:IsInstanceOf("TARGET") then self.engage.Target=Target else self.engage.Target=TARGET:New(Target) end - + -- Target coordinate. - self.engage.Coordinate=UTILS.DeepCopy(self.engage.Target:GetCoordinate()) - - -- TODO: Backup current ROE and alarm state and reset after disengage. + 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.95) + + -- Backup ROE and alarm state. + self.engage.roe=self:GetROE() + self.engage.alarmstate=self:GetAlarmstate() -- Switch ROE and alarm state. self:SwitchAlarmstate(ENUMS.AlarmState.Auto) - self:SwitchROE(ENUMS.ROE.WeaponFree) + self:SwitchROE(ENUMS.ROE.OpenFire) -- ID of current waypoint. local uid=self:GetWaypointCurrent().uid + -- Set formation. + self.engage.Formation=Formation or ENUMS.Formation.Vehicle.Vee + + -- Set speed. + self.engage.Speed=Speed + -- Add waypoint after current. - self.engage.Waypoint=self:AddWaypoint(self.engage.Coordinate, nil, uid, Formation, true) + self.engage.Waypoint=self:AddWaypoint(intercoord, self.engage.Speed, uid, self.engage.Formation, true) -- Set if we want to resume route after reaching the detour waypoint. self.engage.Waypoint.detour=1 @@ -138424,35 +156963,51 @@ end function ARMYGROUP:_UpdateEngageTarget() if self.engage.Target and self.engage.Target:IsAlive() then - - --env.info("FF Update Engage Target "..self.engage.Target:GetName()) - local vec3=self.engage.Target:GetCoordinate():GetVec3() + -- Get current position vector. + local vec3=self.engage.Target:GetVec3() + + if vec3 then - local dist=UTILS.VecDist2D(vec3, self.engage.Coordinate:GetVec3()) + -- Distance to last known position of target. + local dist=UTILS.VecDist3D(vec3, self.engage.Coordinate:GetVec3()) + + -- Check if target moved more than 100 meters. + if dist>100 then + + --env.info("FF Update Engage Target Moved "..self.engage.Target:GetName()) + + -- Update new position. + self.engage.Coordinate:UpdateFromVec3(vec3) + + -- ID of current waypoint. + local uid=self:GetWaypointCurrent().uid + + -- Remove current waypoint + self:RemoveWaypointByID(self.engage.Waypoint.uid) + + local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate, 0.9) - if dist>100 then - - --env.info("FF Update Engage Target Moved "..self.engage.Target:GetName()) - - self.engage.Coordinate:UpdateFromVec3(vec3) + -- Add waypoint after current. + self.engage.Waypoint=self:AddWaypoint(intercoord, self.engage.Speed, uid, self.engage.Formation, true) + + -- Set if we want to resume route after reaching the detour waypoint. + self.engage.Waypoint.detour=0 + + end + + else - -- ID of current waypoint. - local uid=self:GetWaypointCurrent().uid - - -- Remove current waypoint - self:RemoveWaypointByID(self.engage.Waypoint.uid) - - -- Add waypoint after current. - self.engage.Waypoint=self:AddWaypoint(self.engage.Coordinate, nil, uid, Formation, true) - - -- Set if we want to resume route after reaching the detour waypoint. - self.engage.Waypoint.detour=0 + -- Could not get position of target (not alive any more?) ==> Disengage. + self:Disengage() end else + + -- Target not alive any more ==> Disengage. self:Disengage() + end end @@ -138463,19 +157018,28 @@ end -- @param #string Event Event. -- @param #string To To state. function ARMYGROUP:onafterDisengage(From, Event, To) - -- TODO: Reset ROE and alarm state. - self:_CheckGroupDone(1) -end + self:T(self.lid.."Disengage Target") ---- On after "Rearmed" event. --- @param #ARMYGROUP self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. -function ARMYGROUP:onafterRearmed(From, Event, To) + -- Restore previous ROE and alarm state. + self:SwitchROE(self.engage.roe) + self:SwitchAlarmstate(self.engage.alarmstate) + + -- Get current task + local task=self:GetTaskCurrent() + + -- Get if current task is ground attack. + if task and task.dcstask.id==AUFTRAG.SpecialTask.GROUNDATTACK then + self:T(self.lid.."Disengage with current task GROUNDATTACK ==> Task Done!") + self:TaskDone(task) + end + + -- Remove current waypoint + if self.engage.Waypoint then + self:RemoveWaypointByID(self.engage.Waypoint.uid) + end + -- Check group is done self:_CheckGroupDone(1) - end --- On after "DetourReached" event. @@ -138484,7 +157048,7 @@ end -- @param #string Event Event. -- @param #string To To state. function ARMYGROUP:onafterDetourReached(From, Event, To) - self:I(self.lid.."Group reached detour coordinate.") + self:T(self.lid.."Group reached detour coordinate") end @@ -138495,6 +157059,9 @@ end -- @param #string To To state. function ARMYGROUP:onafterFullStop(From, Event, To) + -- Debug info. + self:T(self.lid..string.format("Full stop!")) + -- Get current position. local pos=self:GetCoordinate() @@ -138515,129 +157082,31 @@ end -- @param #number Formation Formation. function ARMYGROUP:onafterCruise(From, Event, To, Speed, Formation) - self:__UpdateRoute(-1, nil, Speed, Formation) + -- Not waiting anymore. + self.Twaiting=nil + self.dTwait=nil + + -- Debug info. + self:T(self.lid.."Cruise ==> Update route in 0.01 sec") + + -- Update route. + self:__UpdateRoute(-0.01, nil, nil, Speed, Formation) end ---- On after "Stop" event. +--- On after "Hit" event. -- @param #ARMYGROUP self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function ARMYGROUP:onafterStop(From, Event, To) - - -- Handle events: - self:UnHandleEvent(EVENTS.Birth) - self:UnHandleEvent(EVENTS.Dead) - self:UnHandleEvent(EVENTS.RemoveUnit) +-- @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")) - -- Call OPSGROUP function. - self:GetParent(self).onafterStop(self, From, Event, To) - -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Events DCS -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Event function handling the birth of a unit. --- @param #ARMYGROUP self --- @param Core.Event#EVENTDATA EventData Event data. -function ARMYGROUP:OnEventBirth(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 - - if self.respawning then - - local function reset() - self.respawning=nil - end - - -- Reset switch in 1 sec. This should allow all birth events of n>1 groups to have passed. - -- TODO: Can I do this more rigorously? - self:ScheduleOnce(1, reset) - - else - - -- Get element. - local element=self:GetElementByName(unitname) - - -- Set element to spawned state. - self:T3(self.lid..string.format("EVENT: Element %s born ==> spawned", element.name)) - self:ElementSpawned(element) - - end - - end - -end - ---- Event function handling the crash of a unit. --- @param #ARMYGROUP self --- @param Core.Event#EVENTDATA EventData Event data. -function ARMYGROUP:OnEventDead(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:T(self.lid..string.format("EVENT: Unit %s dead!", EventData.IniUnitName)) - - local unit=EventData.IniUnit - local group=EventData.IniGroup - local unitname=EventData.IniUnitName - - -- Get element. - local element=self:GetElementByName(unitname) - - if element then - self:T(self.lid..string.format("EVENT: Element %s dead ==> destroyed", element.name)) - self:ElementDestroyed(element) - end - - end - -end - ---- Event function handling when a unit is removed from the game. --- @param #ARMYGROUP self --- @param Core.Event#EVENTDATA EventData Event data. -function ARMYGROUP:OnEventRemoveUnit(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 - - -- Get element. - local element=self:GetElementByName(unitname) - - if element then - self:T(self.lid..string.format("EVENT: Element %s removed ==> dead", element.name)) - self:ElementDead(element) - end - - end - -end - ---- 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 + if self.suppressionOn then + env.info(self.lid.."FF suppress") + self:_Suppress() + end end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -138649,25 +157118,35 @@ end -- @param Core.Point#COORDINATE Coordinate The coordinate of the waypoint. -- @param #number Speed Speed in knots. Default is default cruise speed or 70% of max speed. -- @param #number AfterWaypointWithID Insert waypoint after waypoint given ID. Default is to insert as last waypoint. --- @param #number Formation Formation the group will use. +-- @param #string Formation Formation the group will use. -- @param #boolean Updateroute If true or nil, call UpdateRoute. If false, no call. -- @return Ops.OpsGroup#OPSGROUP.Waypoint Waypoint table. function ARMYGROUP:AddWaypoint(Coordinate, Speed, AfterWaypointWithID, Formation, Updateroute) + -- Debug info. + self:T(self.lid..string.format("AddWaypoint Formation = %s",tostring(Formation) or "none")) + + -- Create coordinate. local coordinate=self:_CoordinateFromObject(Coordinate) -- Set waypoint index. local wpnumber=self:GetWaypointIndexAfterID(AfterWaypointWithID) - -- Check if final waypoint is still passed. - if wpnumber>self.currentwp then - self.passedfinalwp=false - end - -- Speed in knots. Speed=Speed or self:GetSpeedCruise() - - -- Create a Naval waypoint. + + -- Formation. + if not Formation then + if self.formationPerma then + Formation = self.formationPerma + elseif self.option.Formation then + Formation = self.option.Formation + else + Formation = "On Road" + end + end + + -- Create a Ground waypoint. local wp=coordinate:WaypointGround(UTILS.KnotsToKmph(Speed), Formation) -- Create waypoint data table. @@ -138689,7 +157168,7 @@ function ARMYGROUP:AddWaypoint(Coordinate, Speed, AfterWaypointWithID, Formation -- Update route. if Updateroute==nil or Updateroute==true then - self:_CheckGroupDone(1) + self:__UpdateRoute(-0.01) end return waypoint @@ -138697,28 +157176,24 @@ end --- Initialize group parameters. Also initializes waypoints if self.waypoints is nil. -- @param #ARMYGROUP self +-- @param #table Template Template used to init the group. Default is `self.template`. -- @return #ARMYGROUP self -function ARMYGROUP:_InitGroup() +function ARMYGROUP:_InitGroup(Template) -- First check if group was already initialized. if self.groupinitialized then - self:E(self.lid.."WARNING: Group was already initialized!") + self:T(self.lid.."WARNING: Group was already initialized! Will NOT do it again!") return end -- Get template of group. - self.template=self.group:GetTemplate() - - -- Define category. - self.isAircraft=false - self.isNaval=false - self.isGround=true + local template=Template or self:_GetTemplate() -- Ground are always AI. self.isAI=true -- Is (template) group late activated. - self.isLateActivated=self.template.lateActivation + self.isLateActivated=template.lateActivation -- Ground groups cannot be uncontrolled. self.isUncontrolled=false @@ -138726,6 +157201,13 @@ function ARMYGROUP:_InitGroup() -- Max speed in km/h. self.speedMax=self.group:GetSpeedMax() + -- Is group mobile? + if self.speedMax>3.6 then + self.isMobile=true + else + self.isMobile=false + end + -- Cruise speed in km/h self.speedCruise=self.speedMax*0.7 @@ -138741,7 +157223,7 @@ function ARMYGROUP:_InitGroup() self:SetDefaultRadio(self.radio.Freq, self.radio.Modu, self.radio.On) -- Set default formation from first waypoint. - self.optionDefault.Formation=self:GetWaypoint(1).action + self.optionDefault.Formation=template.route.points[1].action --self:GetWaypoint(1).action -- Default TACAN off. self:SetDefaultTACAN(nil, nil, nil, nil, true) @@ -138750,65 +157232,22 @@ function ARMYGROUP:_InitGroup() -- Units of the group. local units=self.group:GetUnits() - for _,_unit in pairs(units) do - local unit=_unit --Wrapper.Unit#UNIT - - -- TODO: this is wrong when grouping is used! - local unittemplate=unit:GetTemplate() - - local element={} --#ARMYGROUP.Element - element.name=unit:GetName() - element.unit=unit - element.status=OPSGROUP.ElementStatus.INUTERO - element.typename=unit:GetTypeName() - element.skill=unittemplate.skill or "Unknown" - element.ai=true - element.category=element.unit:GetUnitCategory() - element.categoryname=element.unit:GetCategoryName() - element.size, element.length, element.height, element.width=unit:GetObjectSize() - element.ammo0=self:GetAmmoUnit(unit, false) - element.life0=unit:GetLife0() - element.life=element.life0 - - -- Debug text. - if self.verbose>=2 then - local text=string.format("Adding element %s: status=%s, skill=%s, life=%.3f category=%s (%d), size: %.1f (L=%.1f H=%.1f W=%.1f)", - element.name, element.status, element.skill, element.life, element.categoryname, element.category, element.size, element.length, element.height, element.width) - self:I(self.lid..text) - end + -- DCS group. + local dcsgroup=Group.getByName(self.groupname) + local size0=dcsgroup:getInitialSize() - -- Add element to table. - table.insert(self.elements, element) - - -- Get Descriptors. - self.descriptors=self.descriptors or unit:GetDesc() - - -- Set type name. - self.actype=self.actype or unit:GetTypeName() - - if unit:IsAlive() then - -- Trigger spawned event. - self:ElementSpawned(element) - end - + -- Quick check. + if #units~=size0 then + self:T(self.lid..string.format("ERROR: Got #units=%d but group consists of %d units!", #units, size0)) end + + -- Add elemets. + for _,unit in pairs(units) do + local unitname=unit:GetName() + self:_AddElementByName(unitname) + end + - -- Debug info. - if self.verbose>=1 then - local text=string.format("Initialized Army Group %s:\n", self.groupname) - text=text..string.format("Unit type = %s\n", self.actype) - text=text..string.format("Speed max = %.1f Knots\n", UTILS.KmphToKnots(self.speedMax)) - text=text..string.format("Speed cruise = %.1f Knots\n", UTILS.KmphToKnots(self.speedCruise)) - text=text..string.format("Elements = %d\n", #self.elements) - text=text..string.format("Waypoints = %d\n", #self.waypoints) - text=text..string.format("Radio = %.1f MHz %s %s\n", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu), tostring(self.radio.On)) - text=text..string.format("Ammo = %d (G=%d/R=%d/M=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Missiles) - text=text..string.format("FSM state = %s\n", self:GetState()) - text=text..string.format("Is alive = %s\n", tostring(self:IsAlive())) - text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated())) - self:I(self.lid..text) - end - -- Init done. self.groupinitialized=true @@ -138829,8 +157268,9 @@ function ARMYGROUP:SwitchFormation(Formation, Permanently, NoRouteUpdate) if self:IsAlive() or self:IsInUtero() then - Formation=Formation or self.optionDefault.Formation - + Formation=Formation or (self.optionDefault.Formation or "Off road") + Permanently = Permanently or false + if Permanently then self.formationPerma=Formation else @@ -138838,10 +157278,10 @@ function ARMYGROUP:SwitchFormation(Formation, Permanently, NoRouteUpdate) end -- Set current formation. - self.option.Formation=Formation + self.option.Formation=Formation or "Off road" if self:IsInUtero() then - self:T(self.lid..string.format("Will switch formation to %s (permanently=%s) when group is spawned", self.option.Formation, tostring(Permanently))) + self:T(self.lid..string.format("Will switch formation to %s (permanently=%s) when group is spawned", tostring(self.option.Formation), tostring(Permanently))) else -- Update route with the new formation. @@ -138851,7 +157291,7 @@ function ARMYGROUP:SwitchFormation(Formation, Permanently, NoRouteUpdate) end -- Debug info. - self:T(self.lid..string.format("Switching formation to %s (permanently=%s)", self.option.Formation, tostring(Permanently))) + self:T(self.lid..string.format("Switching formation to %s (permanently=%s)", tostring(self.option.Formation), tostring(Permanently))) end @@ -138864,11 +157304,1715 @@ end -- Misc Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Find the neares ammo supply group within a given radius. +-- @param #ARMYGROUP self +-- @param #number Radius Search radius in NM. Default 30 NM. +-- @return Wrapper.Group#GROUP Closest ammo supplying group or `nil` if no group is in the given radius. +-- @return #number Distance to closest group in meters. +function ARMYGROUP:FindNearestAmmoSupply(Radius) + -- Radius in meters. + Radius=UTILS.NMToMeters(Radius or 30) + + -- Current positon. + local coord=self:GetCoordinate() + + -- Get my coalition. + local myCoalition=self:GetCoalition() + + -- Scanned units. + local units=coord:ScanUnits(Radius) + + -- Find closest + local dmin=math.huge + local truck=nil --Wrapper.Unit#UNIT + for _,_unit in pairs(units.Set) do + local unit=_unit --Wrapper.Unit#UNIT + + -- Check coaliton and if unit can supply ammo. + if unit:IsAlive() and unit:GetCoalition()==myCoalition and unit:IsAmmoSupply() and unit:GetVelocityKMH()<1 then + + -- Distance. + local d=coord:Get2DDistance(unit:GetCoord()) + + -- Check if distance is smaller. + if d 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 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- **Ops** - Cohort encompassed all characteristics of SQUADRONs, PLATOONs and FLOTILLAs. +-- +-- **Main Features:** +-- +-- * Set parameters like livery, skill valid for all cohort members. +-- * Define modex and callsigns. +-- * Define mission types, this cohort can perform (see Ops.Auftrag#AUFTRAG). +-- * Pause/unpause cohort operations. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module Ops.Cohort +-- @image OPS_Cohort.png + + +--- COHORT class. +-- @type COHORT +-- @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 cohort. +-- @field #string templatename Name of the template group. +-- @field #string aircrafttype Type of the units the cohort is using. +-- @field #number category Group category of the assets: `Group.Category.AIRPLANE`, `Group.Category.HELICOPTER`, `Group.Category.GROUND`, `Group.Category.SHIP`, `Group.Category.TRAIN`. +-- @field Wrapper.Group#GROUP templategroup Template group. +-- @field #boolean isAir +-- @field #boolean isGround Is ground. +-- @field #boolean isNaval Is naval. +-- @field #table assets Cohort assets. +-- @field #table missiontypes Capabilities (mission types and performances) of the cohort. +-- @field #number maintenancetime Time in seconds needed for maintenance of a returned flight. +-- @field #number repairtime Time in seconds for each +-- @field #string livery Livery of the cohort. +-- @field #number skill Skill of cohort members. +-- @field Ops.Legion#LEGION legion The LEGION object the cohort belongs to. +-- @field #number Ngroups Number of asset OPS groups this cohort has. +-- @field #number Nkilled Number of destroyed asset groups. +-- @field #number engageRange Mission range in meters. +-- @field #string attribute Generalized attribute of the cohort template group. +-- @field #table descriptors DCS descriptors. +-- @field #table properties DCS attributes. +-- @field #table tacanChannel List of TACAN channels available to the cohort. +-- @field #number radioFreq Radio frequency in MHz the cohort uses. +-- @field #number radioModu Radio modulation the cohort uses. +-- @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 +-- +-- === +-- +-- # The COHORT Concept +-- +-- A COHORT is essential part of a LEGION and consists of **one** unit type. +-- +-- +-- +-- @field #COHORT +COHORT = { + ClassName = "COHORT", + verbose = 0, + lid = nil, + name = nil, + templatename = nil, + assets = {}, + missiontypes = {}, + repairtime = 0, + maintenancetime= 0, + livery = nil, + skill = nil, + legion = nil, + Ngroups = nil, + Ngroups = 0, + engageRange = nil, + tacanChannel = {}, + weightAsset = 99999, + cargobayLimit = 0, + descriptors = {}, + properties = {}, + operations = {}, +} + +--- COHORT class version. +-- @field #string version +COHORT.version="0.3.5" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- DONE: Create FLOTILLA class. +-- DONE: Added check for properties. +-- DONE: Make general so that PLATOON and SQUADRON can inherit this class. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new COHORT object and start the FSM. +-- @param #COHORT self +-- @param #string TemplateGroupName Name of the template group. +-- @param #number Ngroups Number of asset groups of this Cohort. Default 3. +-- @param #string CohortName Name of the cohort. +-- @return #COHORT self +function COHORT:New(TemplateGroupName, Ngroups, CohortName) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #COHORT + + -- Name of the template group. + self.templatename=TemplateGroupName + + -- Cohort name. + self.name=tostring(CohortName or TemplateGroupName) + + -- Set some string id for output to DCS.log file. + self.lid=string.format("COHORT %s | ", self.name) + + -- Template group. + self.templategroup=GROUP:FindByName(self.templatename) + + -- Check if template group exists. + if not self.templategroup then + self:E(self.lid..string.format("ERROR: Template group %s does not exist!", tostring(self.templatename))) + return nil + end + + -- Generalized attribute. + self.attribute=self.templategroup:GetAttribute() + + -- Group category. + self.category=self.templategroup:GetCategory() + + -- Aircraft type. + self.aircrafttype=self.templategroup:GetTypeName() + + -- Get descriptors. + self.descriptors=self.templategroup:GetUnit(1):GetDesc() + + -- Properties (DCS attributes). + self.properties=self.descriptors.attributes + + -- Print properties. + --self:I(self.properties) + + -- Defaults. + self.Ngroups=Ngroups or 3 + self:SetSkill(AI.Skill.GOOD) + + -- Mission range depends on + if self.category==Group.Category.AIRPLANE then + self:SetMissionRange(200) + elseif self.category==Group.Category.HELICOPTER then + self:SetMissionRange(150) + elseif self.category==Group.Category.GROUND then + self:SetMissionRange(75) + elseif self.category==Group.Category.SHIP then + self:SetMissionRange(100) + elseif self.category==Group.Category.TRAIN then + self:SetMissionRange(100) + else + self:SetMissionRange(150) + end + + -- Units. + local units=self.templategroup:GetUnits() + + -- Weight of the whole group. + self.weightAsset=0 + for i,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + local desc=unit:GetDesc() + self.weightAsset=self.weightAsset + (desc.massMax or 666) + if i==1 then + self.cargobayLimit=unit:GetCargoBayFreeWeight() + end + end + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "OnDuty") -- Start FSM. + self:AddTransition("*", "Status", "*") -- Status update. + + self:AddTransition("OnDuty", "Pause", "Paused") -- Pause cohort. + self:AddTransition("Paused", "Unpause", "OnDuty") -- Unpause cohort. + + self:AddTransition("OnDuty", "Relocate", "Relocating") -- Relocate. + self:AddTransition("Relocating", "Relocated", "OnDuty") -- Relocated. + + self:AddTransition("*", "Stop", "Stopped") -- Stop cohort. + + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the COHORT. + -- @function [parent=#COHORT] Start + -- @param #COHORT self + + --- Triggers the FSM event "Start" after a delay. Starts the COHORT. + -- @function [parent=#COHORT] __Start + -- @param #COHORT self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Stop". + -- @param #COHORT self + + --- Triggers the FSM event "Stop" after a delay. Stops the COHORT and all its event handlers. + -- @function [parent=#COHORT] __Stop + -- @param #COHORT self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Status". + -- @function [parent=#COHORT] Status + -- @param #COHORT self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#COHORT] __Status + -- @param #COHORT self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Pause". + -- @function [parent=#COHORT] Pause + -- @param #COHORT self + + --- Triggers the FSM event "Pause" after a delay. + -- @function [parent=#COHORT] __Pause + -- @param #COHORT self + -- @param #number delay Delay in seconds. + + --- On after "Pause" event. + -- @function [parent=#COHORT] OnAfterPause + -- @param #COHORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Unpause". + -- @function [parent=#COHORT] Unpause + -- @param #COHORT self + + --- Triggers the FSM event "Unpause" after a delay. + -- @function [parent=#COHORT] __Unpause + -- @param #COHORT self + -- @param #number delay Delay in seconds. + + --- On after "Unpause" event. + -- @function [parent=#COHORT] OnAfterUnpause + -- @param #COHORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Relocate". + -- @function [parent=#COHORT] Relocate + -- @param #COHORT self + + --- Triggers the FSM event "Relocate" after a delay. + -- @function [parent=#COHORT] __Relocate + -- @param #COHORT self + -- @param #number delay Delay in seconds. + + --- On after "Relocate" event. + -- @function [parent=#COHORT] OnAfterRelocate + -- @param #COHORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Relocated". + -- @function [parent=#COHORT] Relocated + -- @param #COHORT self + + --- Triggers the FSM event "Relocated" after a delay. + -- @function [parent=#COHORT] __Relocated + -- @param #COHORT self + -- @param #number delay Delay in seconds. + + --- On after "Relocated" event. + -- @function [parent=#COHORT] OnAfterRelocated + -- @param #COHORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set livery painted on all cohort units. +-- Note that the livery name in general is different from the name shown in the mission editor. +-- +-- Valid names are the names of the **livery directories**. Check out the folder in your DCS installation for: +-- +-- * Full modules: `DCS World OpenBeta\CoreMods\aircraft\\Liveries\\` +-- * AI units: `DCS World OpenBeta\Bazar\Liveries\\` +-- +-- The folder name `` is the string you want. +-- +-- Or personal liveries you have installed somewhere in your saved games folder. +-- +-- @param #COHORT self +-- @param #string LiveryName Name of the livery. +-- @return #COHORT self +function COHORT:SetLivery(LiveryName) + self.livery=LiveryName + return self +end + +--- Set skill level of all cohort team members. +-- @param #COHORT self +-- @param #string Skill Skill of all flights. +-- @usage mycohort:SetSkill(AI.Skill.EXCELLENT) +-- @return #COHORT self +function COHORT:SetSkill(Skill) + self.skill=Skill + return self +end + +--- Set verbosity level. +-- @param #COHORT self +-- @param #number VerbosityLevel Level of output (higher=more). Default 0. +-- @return #COHORT self +function COHORT:SetVerbosity(VerbosityLevel) + self.verbose=VerbosityLevel or 0 + return self +end + +--- Set turnover and repair time. If an asset returns from a mission, it will need some time until the asset is available for further missions. +-- @param #COHORT self +-- @param #number MaintenanceTime Time in minutes it takes until a flight is combat ready again. Default is 0 min. +-- @param #number RepairTime Time in minutes it takes to repair a flight for each life point taken. Default is 0 min. +-- @return #COHORT self +function COHORT:SetTurnoverTime(MaintenanceTime, RepairTime) + self.maintenancetime=MaintenanceTime and MaintenanceTime*60 or 0 + self.repairtime=RepairTime and RepairTime*60 or 0 + return self +end + +--- Set radio frequency and modulation the cohort uses. +-- @param #COHORT self +-- @param #number Frequency Radio frequency in MHz. Default 251 MHz. +-- @param #number Modulation Radio modulation. Default 0=AM. +-- @return #COHORT self +function COHORT:SetRadio(Frequency, Modulation) + self.radioFreq=Frequency or 251 + self.radioModu=Modulation or radio.modulation.AM + return self +end + +--- Set number of units in groups. +-- @param #COHORT self +-- @param #number nunits Number of units. Default 2. +-- @return #COHORT self +function COHORT:SetGrouping(nunits) + self.ngrouping=nunits or 2 + return self +end + +--- Set mission types this cohort is able to perform. +-- @param #COHORT self +-- @param #table MissionTypes Table of mission types. Can also be passed as a #string if only one type. +-- @param #number Performance Performance describing how good this mission can be performed. Higher is better. Default 50. Max 100. +-- @return #COHORT self +function COHORT:AddMissionCapability(MissionTypes, Performance) + + -- Ensure Missiontypes is a table. + if MissionTypes and type(MissionTypes)~="table" then + MissionTypes={MissionTypes} + end + + -- Set table. + self.missiontypes=self.missiontypes or {} + + for _,missiontype in pairs(MissionTypes) do + + local Capability=self:GetMissionCapability(missiontype) + + -- Check not to add the same twice. + if Capability then + self:E(self.lid.."WARNING: Mission capability already present! No need to add it twice. Will update the performance though!") + Capability.Performance=Performance or 50 + else + + local capability={} --Ops.Auftrag#AUFTRAG.Capability + capability.MissionType=missiontype + capability.Performance=Performance or 50 + table.insert(self.missiontypes, capability) + self:T(self.lid..string.format("Adding mission capability %s, performance=%d", tostring(capability.MissionType), capability.Performance)) + end + end + + -- Debug info. + self:T2(self.missiontypes) + + return self +end + +--- Get missin capability for a given mission type. +-- @param #COHORT self +-- @param #string MissionType Mission type, e.g. `AUFTRAG.Type.BAI`. +-- @return Ops.Auftrag#AUFTRAG.Capability Capability table or `nil` if the capability does not exist. +function COHORT:GetMissionCapability(MissionType) + + for _,_capability in pairs(self.missiontypes) do + local capability=_capability --Ops.Auftrag#AUFTRAG.Capability + if capability.MissionType==MissionType then + return capability + end + end + + return nil +end + +--- Check if cohort assets have a given property (DCS attribute). +-- @param #COHORT self +-- @param #string Property The property. +-- @return #boolean If `true`, cohort assets have the attribute. +function COHORT:HasProperty(Property) + + for _,property in pairs(self.properties) do + if Property==property then + return true + end + end + + return false +end + +--- Get mission types this cohort is able to perform. +-- @param #COHORT self +-- @return #table Table of mission types. Could be empty {}. +function COHORT:GetMissionTypes() + + local missiontypes={} + + for _,Capability in pairs(self.missiontypes) do + local capability=Capability --Ops.Auftrag#AUFTRAG.Capability + table.insert(missiontypes, capability.MissionType) + end + + return missiontypes +end + +--- Get mission capabilities of this cohort. +-- @param #COHORT self +-- @return #table Table of mission capabilities. +function COHORT:GetMissionCapabilities() + return self.missiontypes +end + +--- Get mission performance for a given type of misson. +-- @param #COHORT self +-- @param #string MissionType Type of mission. +-- @return #number Performance or -1. +function COHORT:GetMissionPeformance(MissionType) + + for _,Capability in pairs(self.missiontypes) do + local capability=Capability --Ops.Auftrag#AUFTRAG.Capability + if capability.MissionType==MissionType then + return capability.Performance + end + end + + return -1 +end + +--- Set max mission range. Only missions in a circle of this radius around the cohort base are executed. +-- @param #COHORT self +-- @param #number Range Range in NM. Default 150 NM. +-- @return #COHORT self +function COHORT:SetMissionRange(Range) + self.engageRange=UTILS.NMToMeters(Range or 150) + return self +end + +--- Set call sign. +-- @param #COHORT self +-- @param #number Callsign Callsign from CALLSIGN.Aircraft, e.g. "Chevy" for CALLSIGN.Aircraft.CHEVY. +-- @param #number Index Callsign index, Chevy-**1**. +-- @return #COHORT self +function COHORT:SetCallsign(Callsign, Index) + self.callsignName=Callsign + self.callsignIndex=Index + return self +end + +--- Set modex. +-- @param #COHORT self +-- @param #number Modex A number like 100. +-- @param #string Prefix A prefix string, which is put before the `Modex` number. +-- @param #string Suffix A suffix string, which is put after the `Modex` number. +-- @return #COHORT self +function COHORT:SetModex(Modex, Prefix, Suffix) + self.modex=Modex + self.modexPrefix=Prefix + self.modexSuffix=Suffix + return self +end + +--- Set Legion. +-- @param #COHORT self +-- @param Ops.Legion#LEGION Legion The Legion. +-- @return #COHORT self +function COHORT:SetLegion(Legion) + self.legion=Legion + return self +end + +--- Add asset to cohort. +-- @param #COHORT self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The warehouse asset. +-- @return #COHORT self +function COHORT:AddAsset(Asset) + self:T(self.lid..string.format("Adding asset %s of type %s", Asset.spawngroupname, Asset.unittype)) + Asset.squadname=self.name + Asset.legion=self.legion + Asset.cohort=self + table.insert(self.assets, Asset) + return self +end + +--- Remove asset from chort. +-- @param #COHORT self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset. +-- @return #COHORT self +function COHORT:DelAsset(Asset) + for i,_asset in pairs(self.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + if Asset.uid==asset.uid then + self:T2(self.lid..string.format("Removing asset %s", asset.spawngroupname)) + table.remove(self.assets, i) + break + end + end + return self +end + +--- Remove asset group from cohort. +-- @param #COHORT self +-- @param #string GroupName Name of the asset group. +-- @return #COHORT self +function COHORT:DelGroup(GroupName) + for i,_asset in pairs(self.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + if GroupName==asset.spawngroupname then + self:T2(self.lid..string.format("Removing asset %s", asset.spawngroupname)) + table.remove(self.assets, i) + break + end + end + return self +end + +--- Get name of the cohort. +-- @param #COHORT self +-- @return #string Name of the cohort. +function COHORT:GetName() + return self.name +end + +--- Get radio frequency and modulation. +-- @param #COHORT self +-- @return #number Radio frequency in MHz. +-- @return #number Radio Modulation (0=AM, 1=FM). +function COHORT:GetRadio() + return self.radioFreq, self.radioModu +end + +--- Create a callsign for the asset. +-- @param #COHORT self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The warehouse asset. +-- @return #COHORT self +function COHORT:GetCallsign(Asset) + + if self.callsignName then + + Asset.callsign={} + + for i=1,Asset.nunits do + + local callsign={} + + callsign[1]=self.callsignName + callsign[2]=math.floor(self.callsigncounter / 10) + callsign[3]=self.callsigncounter % 10 + if callsign[3]==0 then + callsign[3]=1 + self.callsigncounter=self.callsigncounter+2 + else + self.callsigncounter=self.callsigncounter+1 + end + + Asset.callsign[i]=callsign + + self:T3({callsign=callsign}) + + --TODO: there is also a table entry .name, which is a string. + end + + + end + +end + +--- Create a modex for the asset. +-- @param #COHORT self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The warehouse asset. +-- @return #COHORT self +function COHORT:GetModex(Asset) + + if self.modex then + + Asset.modex={} + + for i=1,Asset.nunits do + + Asset.modex[i]=string.format("%03d", self.modex+self.modexcounter) + + self.modexcounter=self.modexcounter+1 + + self:T3({modex=Asset.modex[i]}) + + end + + end + +end + + +--- Add TACAN channels to the cohort. Note that channels can only range from 1 to 126. +-- @param #COHORT self +-- @param #number ChannelMin Channel. +-- @param #number ChannelMax Channel. +-- @return #COHORT self +-- @usage mysquad:AddTacanChannel(64,69) -- adds channels 64, 65, 66, 67, 68, 69 +function COHORT:AddTacanChannel(ChannelMin, ChannelMax) + + ChannelMax=ChannelMax or ChannelMin + + if ChannelMin>126 then + self:E(self.lid.."ERROR: TACAN Channel must be <= 126! Will not add to available channels") + return self + end + if ChannelMax>126 then + self:E(self.lid.."WARNING: TACAN Channel must be <= 126! Adjusting ChannelMax to 126") + ChannelMax=126 + end + + for i=ChannelMin,ChannelMax do + self.tacanChannel[i]=true + end + + return self +end + +--- Get an unused TACAN channel. +-- @param #COHORT self +-- @return #number TACAN channel or *nil* if no channel is free. +function COHORT:FetchTacan() + + -- Get the smallest free channel if there is one. + local freechannel=nil + for channel,free in pairs(self.tacanChannel) do + if free then + if freechannel==nil or channel=2 then + local text="Weapon data:" + for _,_weapondata in pairs(self.weaponData) do + local weapondata=_weapondata + text=text..string.format("\n- Bit=%s, Rmin=%d m, Rmax=%d m", tostring(weapondata.BitType), weapondata.RangeMin, weapondata.RangeMax) + end + self:I(self.lid..text) + end + + return self +end + +--- Get weapon range for given bit type. +-- @param #COHORT self +-- @param #number BitType Bit mask of weapon type. +-- @return Ops.OpsGroup#OPSGROUP.WeaponData Weapon data. +function COHORT:GetWeaponData(BitType) + return self.weaponData[tostring(BitType)] +end + +--- Check if cohort is "OnDuty". +-- @param #COHORT self +-- @return #boolean If true, cohort is in state "OnDuty". +function COHORT:IsOnDuty() + return self:Is("OnDuty") +end + +--- Check if cohort is "Stopped". +-- @param #COHORT self +-- @return #boolean If true, cohort is in state "Stopped". +function COHORT:IsStopped() + return self:Is("Stopped") +end + +--- Check if cohort is "Paused". +-- @param #COHORT self +-- @return #boolean If true, cohort is in state "Paused". +function COHORT:IsPaused() + return self:Is("Paused") +end + +--- Check if cohort is "Relocating". +-- @param #COHORT self +-- @return #boolean If true, cohort is relocating. +function COHORT:IsRelocating() + return self:Is("Relocating") +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Start & Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the FLIGHTGROUP FSM and event handlers. +-- @param #COHORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function COHORT:onafterStart(From, Event, To) + + -- Short info. + 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. + self:__Status(-1) +end + +--- Check asset status. +-- @param #COHORT self +function COHORT:_CheckAssetStatus() + + if self.verbose>=2 and #self.assets>0 then + + local text="" + for j,_asset in pairs(self.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Text. + text=text..string.format("\n[%d] %s (%s*%d): ", j, asset.spawngroupname, asset.unittype, asset.nunits) + + if asset.spawned then + + --- + -- Spawned + --- + + -- Mission info. + local mission=self.legion and self.legion:GetAssetCurrentMission(asset) or false + if mission then + local distance=asset.flightgroup and UTILS.MetersToNM(mission:GetTargetDistance(asset.flightgroup.group:GetCoordinate())) or 0 + text=text..string.format("Mission %s - %s: Status=%s, Dist=%.1f NM", mission.name, mission.type, mission.status, distance) + else + text=text.."Mission None" + end + + -- Flight status. + text=text..", Flight: " + if asset.flightgroup and asset.flightgroup:IsAlive() then + local status=asset.flightgroup:GetState() + text=text..string.format("%s", status) + + if asset.flightgroup:IsFlightgroup() then + local fuelmin=asset.flightgroup:GetFuelMin() + local fuellow=asset.flightgroup:IsFuelLow() + local fuelcri=asset.flightgroup:IsFuelCritical() + text=text..string.format("Fuel=%d", fuelmin) + if fuelcri then + text=text.." (Critical!)" + elseif fuellow then + text=text.." (Low)" + end + end + + local lifept, lifept0=asset.flightgroup:GetLifePoints() + text=text..string.format(", Life=%d/%d", lifept, lifept0) + + local ammo=asset.flightgroup:GetAmmoTot() + text=text..string.format(", Ammo=%d [G=%d, R=%d, B=%d, M=%d]", ammo.Total,ammo.Guns, ammo.Rockets, ammo.Bombs, ammo.Missiles) + else + text=text.."N/A" + end + + -- Payload info. + if asset.flightgroup:IsFlightgroup() then + local payload=asset.payload and table.concat(self.legion:GetPayloadMissionTypes(asset.payload), ", ") or "None" + text=text..", Payload={"..payload.."}" + end + + else + + --- + -- In Stock + --- + + text=text..string.format("In Stock") + + if self:IsRepaired(asset) then + text=text..", Combat Ready" + else + + text=text..string.format(", Repaired in %d sec", self:GetRepairTime(asset)) + + if asset.damage then + text=text..string.format(" (Damage=%.1f)", asset.damage) + end + end + + if asset.Treturned then + local T=timer.getAbsTime()-asset.Treturned + text=text..string.format(", Returned for %d sec", T) + end + + end + end + self:T(self.lid..text) + end + +end + +--- On after "Stop" event. +-- @param #COHORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function COHORT:onafterStop(From, Event, To) + + -- Debug info. + self:T(self.lid.."STOPPING Cohort and removing all assets!") + + -- Remove all assets. + for i=#self.assets,1,-1 do + local asset=self.assets[i] + self:DelAsset(asset) + end + + -- Clear call scheduler. + self.CallScheduler:Clear() + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check if there is a cohort that can execute a given mission. +-- We check the mission type, the refuelling system, mission range. +-- @param #COHORT self +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @return #boolean If true, Cohort can do that type of mission. +function COHORT:CanMission(Mission) + + local cando=true + + -- On duty?= + if not self:IsOnDuty() then + self:T(self.lid..string.format("Cohort in not OnDuty but in state %s. Cannot do mission %s with target %s", self:GetState(), Mission.name, Mission:GetTargetName())) + return false + end + + -- Check mission type. WARNING: This assumes that all assets of the cohort can do the same mission types! + if not AUFTRAG.CheckMissionType(Mission.type, self:GetMissionTypes()) then + self:T(self.lid..string.format("INFO: Cohort cannot do mission type %s (%s, %s)", Mission.type, Mission.name, Mission:GetTargetName())) + return false + end + + -- Check that tanker mission has the correct refuelling system. + if Mission.type==AUFTRAG.Type.TANKER then + + if Mission.refuelSystem and Mission.refuelSystem==self.tankerSystem then + -- Correct refueling system. + else + self:T(self.lid..string.format("INFO: Wrong refueling system requested=%s != %s=available", tostring(Mission.refuelSystem), tostring(self.tankerSystem))) + return false + end + + end + + -- Distance to target. + local TargetDistance=Mission:GetTargetDistance(self.legion:GetCoordinate()) + + -- Max engage range. + local engagerange=Mission.engageRange and math.max(self.engageRange, Mission.engageRange) or self.engageRange + + -- Set range is valid. Mission engage distance can overrule the cohort engage range. + if TargetDistance>engagerange then + self:T(self.lid..string.format("INFO: Cohort is not in range. Target dist=%d > %d NM max mission Range", UTILS.MetersToNM(TargetDistance), UTILS.MetersToNM(engagerange))) + return false + end + + return true +end + +--- Count assets in legion warehouse stock. +-- @param #COHORT self +-- @param #boolean InStock If `true`, only assets that are in the warehouse stock/inventory are counted. If `false`, only assets that are NOT in stock (i.e. spawned) are counted. If `nil`, all assets are counted. +-- @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 #number Number of assets. +function COHORT:CountAssets(InStock, MissionTypes, Attributes) + + local N=0 + 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.spawned then + if InStock==false or InStock==nil then + N=N+1 --Spawned but we also count the spawned ones. + end + else + if InStock==true or InStock==nil then + N=N+1 --This is in stock. + end + end + end + end + end + + 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. +-- @param #number Npayloads Number of payloads available. +-- @return #table Assets that can do the required mission. +-- @return #number Number of payloads still available after recruiting the assets. +function COHORT:RecruitAssets(MissionType, Npayloads) + + -- Debug info. + self:T2(self.lid..string.format("Recruiting asset for Mission type=%s", MissionType)) + + -- Recruited assets. + local assets={} + + -- Loop over assets. + for _,_asset in pairs(self.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Get info. + local isRequested=asset.requested + local isReserved=asset.isReserved + local isSpawned=asset.spawned + local isOnMission=self.legion:IsAssetOnMission(asset) + + local opsgroup=asset.flightgroup + + -- Debug info. + self:T(self.lid..string.format("Asset %s: requested=%s, reserved=%s, spawned=%s, onmission=%s", + asset.spawngroupname, tostring(isRequested), tostring(isReserved), tostring(isSpawned), tostring(isOnMission))) + + -- First check that asset is not requested or reserved. This could happen if multiple requests are processed simultaniously. + if not (isRequested or isReserved) then + + -- Check if asset is currently on a mission (STARTED or QUEUED). + if self.legion:IsAssetOnMission(asset) then + --- + -- Asset is already on a mission. + --- + + -- Check if this asset is currently on a mission (STARTED or EXECUTING). + if MissionType==AUFTRAG.Type.RELOCATECOHORT then + + -- Relocation: Take all assets. Mission will be cancelled. + table.insert(assets, asset) + + elseif self.legion:IsAssetOnMission(asset, AUFTRAG.Type.NOTHING) then + + -- Assets on mission NOTHING are considered. + table.insert(assets, asset) + + elseif self.legion:IsAssetOnMission(asset, AUFTRAG.Type.GCICAP) and MissionType==AUFTRAG.Type.INTERCEPT then + + -- Check if the payload of this asset is compatible with the mission. + -- Note: we do not check the payload as an asset that is on a GCICAP mission should be able to do an INTERCEPT as well! + self:T(self.lid..string.format("Adding asset on GCICAP mission for an INTERCEPT mission")) + table.insert(assets, asset) + + elseif self.legion:IsAssetOnMission(asset, AUFTRAG.Type.ONGUARD) and (MissionType==AUFTRAG.Type.ARTY or MissionType==AUFTRAG.Type.GROUNDATTACK) then + + if not opsgroup:IsOutOfAmmo() then + self:T(self.lid..string.format("Adding asset on ONGUARD mission for an XXX mission")) + table.insert(assets, asset) + end + + elseif self.legion:IsAssetOnMission(asset, AUFTRAG.Type.PATROLZONE) and (MissionType==AUFTRAG.Type.ARTY or MissionType==AUFTRAG.Type.GROUNDATTACK) then + + if not opsgroup:IsOutOfAmmo() then + self:T(self.lid..string.format("Adding asset on PATROLZONE mission for an XXX mission")) + table.insert(assets, asset) + end + + 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)) + table.insert(assets, asset) + + end + + else + --- + -- Asset as NO current mission + --- + + if asset.spawned then + --- + -- Asset is already SPAWNED (could be uncontrolled on the airfield or inbound after another mission) + --- + + -- Opsgroup. + local flightgroup=asset.flightgroup + + + if flightgroup and flightgroup:IsAlive() and not (flightgroup:IsDead() or flightgroup:IsStopped()) then + + --self:I("OpsGroup is alive") + + -- Assume we are ready and check if any condition tells us we are not. + local combatready=true + + -- Check if in a state where we really do not want to fight any more. + if flightgroup:IsFlightgroup() then + + --- + -- FLIGHTGROUP combat ready? + --- + + -- No more attacks if fuel is already low. Safety first! + if flightgroup:IsFuelLow() then + combatready=false + end + + if MissionType==AUFTRAG.Type.INTERCEPT and not flightgroup:CanAirToAir() then + combatready=false + else + local excludeguns=MissionType==AUFTRAG.Type.BOMBING or MissionType==AUFTRAG.Type.BOMBRUNWAY or MissionType==AUFTRAG.Type.BOMBCARPET or MissionType==AUFTRAG.Type.SEAD or MissionType==AUFTRAG.Type.ANTISHIP + if excludeguns and not flightgroup:CanAirToGround(excludeguns) then + combatready=false + end + end + + if flightgroup:IsHolding() or flightgroup:IsLanding() or flightgroup:IsLanded() or flightgroup:IsArrived() then + combatready=false + end + if asset.payload and not AUFTRAG.CheckMissionCapability(MissionType, asset.payload.capabilities) then + combatready=false + end + + else + + --- + -- ARMY/NAVYGROUP combat ready? + --- + + -- Disable this for now as it can cause problems - at least with transport and cargo assets. + --self:I("Attribute is: "..asset.attribute) + if flightgroup:IsArmygroup() then + -- check for fighting assets + if asset.attribute == WAREHOUSE.Attribute.GROUND_ARTILLERY or + asset.attribute == WAREHOUSE.Attribute.GROUND_TANK or + asset.attribute == WAREHOUSE.Attribute.GROUND_INFANTRY or + asset.attribute == WAREHOUSE.Attribute.GROUND_AAA or + asset.attribute == WAREHOUSE.Attribute.GROUND_SAM + then + combatready=true + end + else + combatready=false + end + + -- Not ready when rearming, retreating or returning! + if flightgroup:IsRearming() or flightgroup:IsRetreating() or flightgroup:IsReturning() then + combatready=false + end + + end + + -- Not ready when currently acting as ops transport carrier. + if flightgroup:IsLoading() or flightgroup:IsTransporting() or flightgroup:IsUnloading() or flightgroup:IsPickingup() or flightgroup:IsCarrier() then + combatready=false + end + -- Not ready when currently acting as ops transport cargo. + if flightgroup:IsCargo() or flightgroup:IsBoarding() or flightgroup:IsAwaitingLift() then + combatready=false + end + + -- This asset is "combatready". + if combatready then + self:T(self.lid.."Adding SPAWNED asset to ANOTHER mission as it is COMBATREADY") + table.insert(assets, asset) + end + + end + + else + + --- + -- Asset is still in STOCK + --- + + -- Check that we have payloads and asset is repaired. + if Npayloads>0 and self:IsRepaired(asset) then + + -- Add this asset to the selection. + table.insert(assets, asset) + + -- Reduce number of payloads so we only return the number of assets that could do the job. + Npayloads=Npayloads-1 + + end + + end + end + + end -- not requested check + end -- loop over assets + + self:T2(self.lid..string.format("Recruited %d assets for Mission type=%s", #assets, MissionType)) + + return assets, Npayloads +end + + +--- Get the time an asset needs to be repaired. +-- @param #COHORT self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset. +-- @return #number Time in seconds until asset is repaired. +function COHORT:GetRepairTime(Asset) + + if Asset.Treturned then + + local t=self.maintenancetime + t=t+Asset.damage*self.repairtime + + -- Seconds after returned. + local dt=timer.getAbsTime()-Asset.Treturned + + local T=t-dt + + return T + else + return 0 + end + +end + +--- Get max mission range. We add the largest weapon range, e.g. for arty or naval if weapon data is available. +-- @param #COHORT self +-- @param #table WeaponTypes (Optional) Weapon bit type(s) to add to the total range. Default is the max weapon type available. +-- @return #number Range in meters. +function COHORT:GetMissionRange(WeaponTypes) + + if WeaponTypes and type(WeaponTypes)~="table" then + WeaponTypes={WeaponTypes} + end + + local function checkWeaponType(Weapon) + local weapon=Weapon --Ops.OpsGroup#OPSGROUP.WeaponData + if WeaponTypes and #WeaponTypes>0 then + for _,weapontype in pairs(WeaponTypes) do + if weapontype==weapon.BitType then + return true + end + end + return false + end + return true + end + + -- Get max weapon range. + local WeaponRange=0 + for _,_weapon in pairs(self.weaponData or {}) do + local weapon=_weapon --Ops.OpsGroup#OPSGROUP.WeaponData + + if weapon.RangeMax>WeaponRange and checkWeaponType(weapon) then + WeaponRange=weapon.RangeMax + end + end + + return self.engageRange+WeaponRange +end + +--- Checks if a mission type is contained in a table of possible types. +-- @param #COHORT self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset. +-- @return #boolean If true, the requested mission type is part of the possible mission types. +function COHORT:IsRepaired(Asset) + + if Asset.Treturned then + local Tnow=timer.getAbsTime() + local Trepaired=Asset.Treturned+self.maintenancetime + if Tnow>=Trepaired then + return true + else + return false + end + + else + return true + end + +end + +--- Check if the cohort attribute matches the given attribute(s). +-- @param #COHORT self +-- @param #table Attributes The requested attributes. See `WAREHOUSE.Attribute` enum. Can also be passed as a single attribute `#string`. +-- @return #boolean If true, the cohort has the requested attribute. +function COHORT:CheckAttribute(Attributes) + + if type(Attributes)~="table" then + Attributes={Attributes} + end + + for _,attribute in pairs(Attributes) do + if attribute==self.attribute then + return true + end + end + + return false +end + +--- Check ammo. +-- @param #COHORT self +-- @return Ops.OpsGroup#OPSGROUP.Ammo Ammo. +function COHORT:_CheckAmmo() + + -- Get units of group. + local units=self.templategroup:GetUnits() + + -- Init counter. + local nammo=0 + local nguns=0 + local nshells=0 + local nrockets=0 + local nmissiles=0 + local nmissilesAA=0 + local nmissilesAG=0 + local nmissilesAS=0 + local nmissilesSA=0 + local nmissilesBM=0 + local nmissilesCR=0 + local ntorps=0 + local nbombs=0 + + + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + + -- Output. + local text=string.format("Unit %s:\n", unit:GetName()) + + -- Get ammo table. + local ammotable=unit:GetAmmo() + + if ammotable then + + -- Debug info. + self:T3(ammotable) + + -- Loop over all weapons. + for w=1,#ammotable do + + -- Weapon table. + local weapon=ammotable[w] + + -- Descriptors. + local Desc=weapon["desc"] + + -- Warhead. + local Warhead=Desc["warhead"] + + -- Number of current weapon. + local Nammo=weapon["count"] + + -- Get the weapon category: shell=0, missile=1, rocket=2, bomb=3, torpedo=4 + local Category=Desc["category"] + + -- Get missile category: Weapon.MissileCategory AAM=1, SAM=2, BM=3, ANTI_SHIP=4, CRUISE=5, OTHER=6 + local MissileCategory = (Category==Weapon.Category.MISSILE) and Desc.missileCategory or nil + + -- Type name of current weapon. + local TypeName=Desc["typeName"] + + -- WeaponName + local weaponString = UTILS.Split(TypeName,"%.") + local WeaponName = weaponString[#weaponString] + + + -- Range in meters. Seems only to exist for missiles (not shells). + local Rmin=Desc["rangeMin"] or 0 + local Rmax=Desc["rangeMaxAltMin"] or 0 + + -- Caliber in mm. + local Caliber=Warhead and Warhead["caliber"] or 0 + + + -- We are specifically looking for shells or rockets here. + if Category==Weapon.Category.SHELL then + --- + -- SHELL + --- + + -- Add up all shells. + if Caliber<70 then + nguns=nguns+Nammo + else + nshells=nshells+Nammo + end + + -- Debug info. + text=text..string.format("- %d shells [%s]: caliber=%d mm, range=%d - %d meters\n", Nammo, WeaponName, Caliber, Rmin, Rmax) + + elseif Category==Weapon.Category.ROCKET then + --- + -- ROCKET + --- + + -- Add up all rockets. + nrockets=nrockets+Nammo + + -- Debug info. + text=text..string.format("- %d rockets [%s]: caliber=%d mm, range=%d - %d meters\n", Nammo, WeaponName, Caliber, Rmin, Rmax) + + elseif Category==Weapon.Category.BOMB then + --- + -- BOMB + --- + + -- Add up all rockets. + nbombs=nbombs+Nammo + + -- Debug info. + text=text..string.format("- %d bombs [%s]: caliber=%d mm, range=%d - %d meters\n", Nammo, WeaponName, Caliber, Rmin, Rmax) + + elseif Category==Weapon.Category.MISSILE then + --- + -- MISSILE + --- + + -- Add up all cruise missiles (category 5) + if MissileCategory==Weapon.MissileCategory.AAM then + nmissiles=nmissiles+Nammo + nmissilesAA=nmissilesAA+Nammo + -- Auto add range for AA missles. Useless here as this is not an aircraft. + if Rmax>0 then + self:AddWeaponRange(UTILS.MetersToNM(Rmin), UTILS.MetersToNM(Rmax), ENUMS.WeaponFlag.AnyAA) + end + elseif MissileCategory==Weapon.MissileCategory.SAM then + nmissiles=nmissiles+Nammo + nmissilesSA=nmissilesSA+Nammo + -- Dont think there is a bit type for SAM. + if Rmax>0 then + --self:AddWeaponRange(Rmin, Rmax, ENUMS.WeaponFlag.AnyASM) + end + elseif MissileCategory==Weapon.MissileCategory.ANTI_SHIP then + nmissiles=nmissiles+Nammo + nmissilesAS=nmissilesAS+Nammo + -- Auto add weapon range for anti-ship missile. + if Rmax>0 then + self:AddWeaponRange(UTILS.MetersToNM(Rmin), UTILS.MetersToNM(Rmax), ENUMS.WeaponFlag.AntiShipMissile) + end + elseif MissileCategory==Weapon.MissileCategory.BM then + nmissiles=nmissiles+Nammo + nmissilesBM=nmissilesBM+Nammo + -- Don't think there is a good bit type for ballistic missiles. + if Rmax>0 then + --self:AddWeaponRange(Rmin, Rmax, ENUMS.WeaponFlag.AnyASM) + end + elseif MissileCategory==Weapon.MissileCategory.CRUISE then + nmissiles=nmissiles+Nammo + nmissilesCR=nmissilesCR+Nammo + -- Auto add weapon range for cruise missile. + if Rmax>0 then + self:AddWeaponRange(UTILS.MetersToNM(Rmin), UTILS.MetersToNM(Rmax), ENUMS.WeaponFlag.CruiseMissile) + end + elseif MissileCategory==Weapon.MissileCategory.OTHER then + nmissiles=nmissiles+Nammo + nmissilesAG=nmissilesAG+Nammo + end + + -- Debug info. + text=text..string.format("- %d %s missiles [%s]: caliber=%d mm, range=%d - %d meters\n", Nammo, self:_MissileCategoryName(MissileCategory), WeaponName, Caliber, Rmin, Rmax) + + elseif Category==Weapon.Category.TORPEDO then + + -- Add up all rockets. + ntorps=ntorps+Nammo + + -- Debug info. + text=text..string.format("- %d torpedos [%s]: caliber=%d mm, range=%d - %d meters\n", Nammo, WeaponName, Caliber, Rmin, Rmax) + + else + + -- Debug info. + text=text..string.format("- %d unknown ammo of type %s (category=%d, missile category=%s)\n", Nammo, TypeName, Category, tostring(MissileCategory)) + + end + + end + end + + -- Debug text and send message. + if self.verbose>=5 then + self:I(self.lid..text) + else + self:T2(self.lid..text) + end + + end + + -- Total amount of ammunition. + nammo=nguns+nshells+nrockets+nmissiles+nbombs+ntorps + + local ammo={} --Ops.OpsGroup#OPSGROUP.Ammo + ammo.Total=nammo + ammo.Guns=nguns + ammo.Shells=nshells + ammo.Rockets=nrockets + ammo.Bombs=nbombs + ammo.Torpedos=ntorps + ammo.Missiles=nmissiles + ammo.MissilesAA=nmissilesAA + ammo.MissilesAG=nmissilesAG + ammo.MissilesAS=nmissilesAS + ammo.MissilesCR=nmissilesCR + ammo.MissilesBM=nmissilesBM + ammo.MissilesSA=nmissilesSA + + return ammo +end + +--- Returns a name of a missile category. +-- @param #COHORT self +-- @param #number categorynumber Number of missile category from weapon missile category enumerator. See https://wiki.hoggitworld.com/view/DCS_Class_Weapon +-- @return #string Missile category name. +function COHORT:_MissileCategoryName(categorynumber) + local cat="unknown" + if categorynumber==Weapon.MissileCategory.AAM then + cat="air-to-air" + elseif categorynumber==Weapon.MissileCategory.SAM then + cat="surface-to-air" + elseif categorynumber==Weapon.MissileCategory.BM then + cat="ballistic" + elseif categorynumber==Weapon.MissileCategory.ANTI_SHIP then + cat="anti-ship" + elseif categorynumber==Weapon.MissileCategory.CRUISE then + cat="cruise" + elseif categorynumber==Weapon.MissileCategory.OTHER then + cat="other" + end + return cat +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 + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + --- **Ops** - Airwing Squadron. -- -- **Main Features:** @@ -138907,7 +159051,6 @@ end -- @field #number modexcounter Counter to incease modex number for assets. -- @field #string callsignName Callsign name. -- @field #number callsigncounter Counter to increase callsign names for new assets. --- @field Ops.AirWing#AIRWING airwing The AIRWING object the squadron belongs to. -- @field #number Ngroups Number of asset flight groups this squadron has. -- @field #number engageRange Mission range in meters. -- @field #string attribute Generalized attribute of the squadron template group. @@ -138916,56 +159059,43 @@ end -- @field #table tacanChannel List of TACAN channels available to the squadron. -- @field #number radioFreq Radio frequency in MHz the squad uses. -- @field #number radioModu Radio modulation the squad uses. --- @field #number takeoffType Take of type. --- @extends Core.Fsm#FSM +-- @field #string takeoffType Take of type. +-- @field #table parkingIDs Parking IDs for this squadron. +-- @field #boolean despawnAfterLanding Aircraft are despawned after landing. +-- @field #boolean despawnAfterHolding Aircraft are despawned after holding. +-- @extends Ops.Cohort#COHORT ---- *It is unbelievable what a squadron of twelve aircraft did to tip the balance.* -- Adolf Galland +--- *It is unbelievable what a squadron of twelve aircraft did to tip the balance* -- Adolf Galland -- -- === -- --- ![Banner Image](..\Presentations\OPS\Squadron\_Main.png) --- -- # The SQUADRON Concept -- --- A SQUADRON is essential part of an AIRWING and consists of **one** type of aircraft. --- --- +-- A SQUADRON is essential part of an @{Ops.Airwing#AIRWING} and consists of **one** type of aircraft. +-- +-- -- -- @field #SQUADRON SQUADRON = { ClassName = "SQUADRON", verbose = 0, - lid = nil, - name = nil, - templatename = nil, - aircrafttype = nil, - assets = {}, - missiontypes = {}, - repairtime = 0, - maintenancetime= 0, - livery = nil, - skill = nil, modex = nil, modexcounter = 0, callsignName = nil, callsigncounter= 11, - airwing = nil, - Ngroups = nil, - engageRange = nil, tankerSystem = nil, refuelSystem = nil, - tacanChannel = {}, } --- SQUADRON class version. -- @field #string version -SQUADRON.version="0.5.2" +SQUADRON.version="0.8.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Parking spots for squadrons? +-- DONE: Parking spots for squadrons? -- DONE: Engage radius. -- DONE: Modex. -- DONE: Call signs. @@ -138978,102 +159108,28 @@ SQUADRON.version="0.5.2" -- @param #SQUADRON self -- @param #string TemplateGroupName Name of the template group. -- @param #number Ngroups Number of asset groups of this squadron. Default 3. --- @param #string SquadronName Name of the squadron, e.g. "VFA-37". +-- @param #string SquadronName Name of the squadron, e.g. "VFA-37". Must be **unique**! -- @return #SQUADRON self function SQUADRON:New(TemplateGroupName, Ngroups, SquadronName) -- Inherit everything from FSM class. - local self=BASE:Inherit(self, FSM:New()) -- #SQUADRON + local self=BASE:Inherit(self, COHORT:New(TemplateGroupName, Ngroups, SquadronName)) -- #SQUADRON - -- Name of the template group. - self.templatename=TemplateGroupName - - -- Squadron name. - self.name=tostring(SquadronName or TemplateGroupName) - - -- Set some string id for output to DCS.log file. - self.lid=string.format("SQUADRON %s | ", self.name) - - -- Template group. - self.templategroup=GROUP:FindByName(self.templatename) - - -- Check if template group exists. - if not self.templategroup then - self:E(self.lid..string.format("ERROR: Template group %s does not exist!", tostring(self.templatename))) - return nil - end - - -- Defaults. - self.Ngroups=Ngroups or 3 - self:SetMissionRange() - self:SetSkill(AI.Skill.GOOD) - --self:SetVerbosity(0) - -- Everyone can ORBIT. self:AddMissionCapability(AUFTRAG.Type.ORBIT) - -- Generalized attribute. - self.attribute=self.templategroup:GetAttribute() - - -- Aircraft type. - self.aircrafttype=self.templategroup:GetTypeName() - + -- Is air. + self.isAir=true + -- Refueling system. self.refuelSystem=select(2, self.templategroup:GetUnit(1):IsRefuelable()) self.tankerSystem=select(2, self.templategroup:GetUnit(1):IsTanker()) - - -- Start State. - self:SetStartState("Stopped") - - -- Add FSM transitions. - -- From State --> Event --> To State - self:AddTransition("Stopped", "Start", "OnDuty") -- Start FSM. - self:AddTransition("*", "Status", "*") -- Status update. - - self:AddTransition("OnDuty", "Pause", "Paused") -- Pause squadron. - self:AddTransition("Paused", "Unpause", "OnDuty") -- Unpause squadron. - - self:AddTransition("*", "Stop", "Stopped") -- Stop squadron. - - ------------------------ --- Pseudo Functions --- ------------------------ - --- Triggers the FSM event "Start". Starts the SQUADRON. Initializes parameters and starts event handlers. - -- @function [parent=#SQUADRON] Start - -- @param #SQUADRON self - - --- Triggers the FSM event "Start" after a delay. Starts the SQUADRON. Initializes parameters and starts event handlers. - -- @function [parent=#SQUADRON] __Start - -- @param #SQUADRON self - -- @param #number delay Delay in seconds. - - --- Triggers the FSM event "Stop". Stops the SQUADRON and all its event handlers. - -- @param #SQUADRON self - - --- Triggers the FSM event "Stop" after a delay. Stops the SQUADRON and all its event handlers. - -- @function [parent=#SQUADRON] __Stop - -- @param #SQUADRON self - -- @param #number delay Delay in seconds. - - --- Triggers the FSM event "Status". - -- @function [parent=#SQUADRON] Status - -- @param #SQUADRON self - - --- Triggers the FSM event "Status" after a delay. - -- @function [parent=#SQUADRON] __Status - -- @param #SQUADRON self - -- @param #number delay Delay in seconds. - - - -- Debug trace. - if false then - BASE:TraceOnOff(true) - BASE:TraceClass(self.ClassName) - BASE:TraceLevel(1) - end + -- See COHORT class return self end @@ -139082,68 +159138,6 @@ end -- User functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Set livery painted on all squadron aircraft. --- Note that the livery name in general is different from the name shown in the mission editor. --- --- Valid names are the names of the **livery directories**. Check out the folder in your DCS installation for: --- --- * Full modules: `DCS World OpenBeta\CoreMods\aircraft\\Liveries\\` --- * AI units: `DCS World OpenBeta\Bazar\Liveries\\` --- --- The folder name `` is the string you want. --- --- Or personal liveries you have installed somewhere in your saved games folder. --- --- @param #SQUADRON self --- @param #string LiveryName Name of the livery. --- @return #SQUADRON self -function SQUADRON:SetLivery(LiveryName) - self.livery=LiveryName - return self -end - ---- Set skill level of all squadron team members. --- @param #SQUADRON self --- @param #string Skill Skill of all flights. --- @usage mysquadron:SetSkill(AI.Skill.EXCELLENT) --- @return #SQUADRON self -function SQUADRON:SetSkill(Skill) - self.skill=Skill - return self -end - ---- Set verbosity level. --- @param #SQUADRON self --- @param #number VerbosityLevel Level of output (higher=more). Default 0. --- @return #SQUADRON self -function SQUADRON:SetVerbosity(VerbosityLevel) - self.verbose=VerbosityLevel or 0 - return self -end - ---- Set turnover and repair time. If an asset returns from a mission to the airwing, it will need some time until the asset is available for further missions. --- @param #SQUADRON self --- @param #number MaintenanceTime Time in minutes it takes until a flight is combat ready again. Default is 0 min. --- @param #number RepairTime Time in minutes it takes to repair a flight for each life point taken. Default is 0 min. --- @return #SQUADRON self -function SQUADRON:SetTurnoverTime(MaintenanceTime, RepairTime) - self.maintenancetime=MaintenanceTime and MaintenanceTime*60 or 0 - self.repairtime=RepairTime and RepairTime*60 or 0 - return self -end - ---- Set radio frequency and modulation the squad uses. --- @param #SQUADRON self --- @param #number Frequency Radio frequency in MHz. Default 251 MHz. --- @param #number Modulation Radio modulation. Default 0=AM. --- @usage mysquadron:SetSkill(AI.Skill.EXCELLENT) --- @return #SQUADRON self -function SQUADRON:SetRadio(Frequency, Modulation) - self.radioFreq=Frequency or 251 - self.radioModu=Modulation or radio.modulation.AM - return self -end - --- Set number of units in groups. -- @param #SQUADRON self -- @param #number nunits Number of units. Must be >=1 and <=4. Default 2. @@ -139155,11 +159149,23 @@ function SQUADRON:SetGrouping(nunits) return self end +--- Set valid parking spot IDs. Assets of this squad are only allowed to be spawned at these parking spots. **Note** that the IDs are different from the ones displayed in the mission editor! +-- @param #SQUADRON self +-- @param #table ParkingIDs Table of parking ID numbers or a single `#number`. +-- @return #SQUADRON self +function SQUADRON:SetParkingIDs(ParkingIDs) + if type(ParkingIDs)~="table" then + ParkingIDs={ParkingIDs} + end + self.parkingIDs=ParkingIDs + return self +end + --- Set takeoff type. All assets of this squadron will be spawned with cold (default) or hot engines. -- Spawning on runways is not supported. -- @param #SQUADRON self --- @param #string TakeoffType Take off type: "Cold" (default) or "Hot" with engines on. +-- @param #string TakeoffType Take off type: "Cold" (default) or "Hot" with engines on or "Air" for spawning in air. -- @return #SQUADRON self function SQUADRON:SetTakeoffType(TakeoffType) TakeoffType=TakeoffType or "Cold" @@ -139167,13 +159173,15 @@ function SQUADRON:SetTakeoffType(TakeoffType) self.takeoffType=COORDINATE.WaypointType.TakeOffParkingHot elseif TakeoffType:lower()=="cold" then self.takeoffType=COORDINATE.WaypointType.TakeOffParking + elseif TakeoffType:lower()=="air" then + self.takeoffType=COORDINATE.WaypointType.TurningPoint else self.takeoffType=COORDINATE.WaypointType.TakeOffParking end return self end ---- Set takeoff type cold (default). +--- Set takeoff type cold (default). All assets of this squadron will be spawned with engines off (cold). -- @param #SQUADRON self -- @return #SQUADRON self function SQUADRON:SetTakeoffCold() @@ -139181,7 +159189,7 @@ function SQUADRON:SetTakeoffCold() return self end ---- Set takeoff type hot. +--- Set takeoff type hot. All assets of this squadron will be spawned with engines on (hot). -- @param #SQUADRON self -- @return #SQUADRON self function SQUADRON:SetTakeoffHot() @@ -139189,114 +159197,42 @@ function SQUADRON:SetTakeoffHot() return self end - ---- Set mission types this squadron is able to perform. +--- Set takeoff type air. All assets of this squadron will be spawned in air above the airbase. -- @param #SQUADRON self --- @param #table MissionTypes Table of mission types. Can also be passed as a #string if only one type. --- @param #number Performance Performance describing how good this mission can be performed. Higher is better. Default 50. Max 100. -- @return #SQUADRON self -function SQUADRON:AddMissionCapability(MissionTypes, Performance) - - -- Ensure Missiontypes is a table. - if MissionTypes and type(MissionTypes)~="table" then - MissionTypes={MissionTypes} - end - - -- Set table. - self.missiontypes=self.missiontypes or {} - - for _,missiontype in pairs(MissionTypes) do - - -- Check not to add the same twice. - if self:CheckMissionCapability(missiontype, self.missiontypes) then - self:E(self.lid.."WARNING: Mission capability already present! No need to add it twice.") - -- TODO: update performance. - else - - local capability={} --Ops.Auftrag#AUFTRAG.Capability - capability.MissionType=missiontype - capability.Performance=Performance or 50 - table.insert(self.missiontypes, capability) - - end - end - - -- Debug info. - self:T2(self.missiontypes) - +function SQUADRON:SetTakeoffAir() + self:SetTakeoffType("Air") return self end ---- Get mission types this squadron is able to perform. +--- Set despawn after landing. Aircraft will be despawned after the landing event. +-- Can help to avoid DCS AI taxiing issues. -- @param #SQUADRON self --- @return #table Table of mission types. Could be empty {}. -function SQUADRON:GetMissionTypes() - - local missiontypes={} - - for _,Capability in pairs(self.missiontypes) do - local capability=Capability --Ops.Auftrag#AUFTRAG.Capability - table.insert(missiontypes, capability.MissionType) +-- @param #boolean Switch If `true` (default), activate despawn after landing. +-- @return #SQUADRON self +function SQUADRON:SetDespawnAfterLanding(Switch) + if Switch then + self.despawnAfterLanding=Switch + else + self.despawnAfterLanding=true end - - return missiontypes + return self end ---- Get mission capabilities of this squadron. +--- Set despawn after holding. Aircraft will be despawned when they arrive at their holding position at the airbase. +-- Can help to avoid DCS AI taxiing issues. -- @param #SQUADRON self --- @return #table Table of mission capabilities. -function SQUADRON:GetMissionCapabilities() - return self.missiontypes -end - ---- Get mission performance for a given type of misson. --- @param #SQUADRON self --- @param #string MissionType Type of mission. --- @return #number Performance or -1. -function SQUADRON:GetMissionPeformance(MissionType) - - for _,Capability in pairs(self.missiontypes) do - local capability=Capability --Ops.Auftrag#AUFTRAG.Capability - if capability.MissionType==MissionType then - return capability.Performance - end +-- @param #boolean Switch If `true` (default), activate despawn after holding. +-- @return #SQUADRON self +function SQUADRON:SetDespawnAfterHolding(Switch) + if Switch then + self.despawnAfterHolding=Switch + else + self.despawnAfterHolding=true end - - return -1 -end - ---- Set max mission range. Only missions in a circle of this radius around the squadron airbase are executed. --- @param #SQUADRON self --- @param #number Range Range in NM. Default 100 NM. --- @return #SQUADRON self -function SQUADRON:SetMissionRange(Range) - self.engageRange=UTILS.NMToMeters(Range or 100) return self end ---- Set call sign. --- @param #SQUADRON self --- @param #number Callsign Callsign from CALLSIGN.Aircraft, e.g. "Chevy" for CALLSIGN.Aircraft.CHEVY. --- @param #number Index Callsign index, Chevy-**1**. --- @return #SQUADRON self -function SQUADRON:SetCallsign(Callsign, Index) - self.callsignName=Callsign - self.callsignIndex=Index - return self -end - ---- Set modex. --- @param #SQUADRON self --- @param #number Modex A number like 100. --- @param #string Prefix A prefix string, which is put before the `Modex` number. --- @param #string Suffix A suffix string, which is put after the `Modex` number. --- @return #SQUADRON self -function SQUADRON:SetModex(Modex, Prefix, Suffix) - self.modex=Modex - self.modexPrefix=Prefix - self.modexSuffix=Suffix - return self -end --- Set low fuel threshold. -- @param #SQUADRON self @@ -139325,201 +159261,17 @@ end -- @param Ops.AirWing#AIRWING Airwing The airwing. -- @return #SQUADRON self function SQUADRON:SetAirwing(Airwing) - self.airwing=Airwing + self.legion=Airwing return self end ---- Add airwing asset to squadron. +--- Get airwing. -- @param #SQUADRON self --- @param Ops.AirWing#AIRWING.SquadronAsset Asset The airwing asset. --- @return #SQUADRON self -function SQUADRON:AddAsset(Asset) - self:T(self.lid..string.format("Adding asset %s of type %s", Asset.spawngroupname, Asset.unittype)) - Asset.squadname=self.name - table.insert(self.assets, Asset) - return self +-- @return Ops.AirWing#AIRWING The airwing. +function SQUADRON:GetAirwing(Airwing) + return self.legion end ---- Remove airwing asset from squadron. --- @param #SQUADRON self --- @param Ops.AirWing#AIRWING.SquadronAsset Asset The airwing asset. --- @return #SQUADRON self -function SQUADRON:DelAsset(Asset) - for i,_asset in pairs(self.assets) do - local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset - if Asset.uid==asset.uid then - self:T2(self.lid..string.format("Removing asset %s", asset.spawngroupname)) - table.remove(self.assets, i) - break - end - end - return self -end - ---- Remove airwing asset group from squadron. --- @param #SQUADRON self --- @param #string GroupName Name of the asset group. --- @return #SQUADRON self -function SQUADRON:DelGroup(GroupName) - for i,_asset in pairs(self.assets) do - local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset - if GroupName==asset.spawngroupname then - self:T2(self.lid..string.format("Removing asset %s", asset.spawngroupname)) - table.remove(self.assets, i) - break - end - end - return self -end - ---- Get name of the squadron --- @param #SQUADRON self --- @return #string Name of the squadron. -function SQUADRON:GetName() - return self.name -end - ---- Get radio frequency and modulation. --- @param #SQUADRON self --- @return #number Radio frequency in MHz. --- @return #number Radio Modulation (0=AM, 1=FM). -function SQUADRON:GetRadio() - return self.radioFreq, self.radioModu -end - ---- Create a callsign for the asset. --- @param #SQUADRON self --- @param Ops.AirWing#AIRWING.SquadronAsset Asset The airwing asset. --- @return #SQUADRON self -function SQUADRON:GetCallsign(Asset) - - if self.callsignName then - - Asset.callsign={} - - for i=1,Asset.nunits do - - local callsign={} - - callsign[1]=self.callsignName - callsign[2]=math.floor(self.callsigncounter / 10) - callsign[3]=self.callsigncounter % 10 - if callsign[3]==0 then - callsign[3]=1 - self.callsigncounter=self.callsigncounter+2 - else - self.callsigncounter=self.callsigncounter+1 - end - - Asset.callsign[i]=callsign - - self:T3({callsign=callsign}) - - --TODO: there is also a table entry .name, which is a string. - end - - - end - -end - ---- Create a modex for the asset. --- @param #SQUADRON self --- @param Ops.AirWing#AIRWING.SquadronAsset Asset The airwing asset. --- @return #SQUADRON self -function SQUADRON:GetModex(Asset) - - if self.modex then - - Asset.modex={} - - for i=1,Asset.nunits do - - Asset.modex[i]=string.format("%03d", self.modex+self.modexcounter) - - self.modexcounter=self.modexcounter+1 - - self:T3({modex=Asset.modex[i]}) - - end - - end - -end - - ---- Add TACAN channels to the squadron. Note that channels can only range from 1 to 126. --- @param #SQUADRON self --- @param #number ChannelMin Channel. --- @param #number ChannelMax Channel. --- @return #SQUADRON self --- @usage mysquad:AddTacanChannel(64,69) -- adds channels 64, 65, 66, 67, 68, 69 -function SQUADRON:AddTacanChannel(ChannelMin, ChannelMax) - - ChannelMax=ChannelMax or ChannelMin - - if ChannelMin>126 then - self:E(self.lid.."ERROR: TACAN Channel must be <= 126! Will not add to available channels") - return self - end - if ChannelMax>126 then - self:E(self.lid.."WARNING: TACAN Channel must be <= 126! Adjusting ChannelMax to 126") - ChannelMax=126 - end - - for i=ChannelMin,ChannelMax do - self.tacanChannel[i]=true - end - - return self -end - ---- Get an unused TACAN channel. --- @param #SQUADRON self --- @return #number TACAN channel or *nil* if no channel is free. -function SQUADRON:FetchTacan() - - for channel,free in pairs(self.tacanChannel) do - if free then - self:T(self.lid..string.format("Checking out Tacan channel %d", channel)) - self.tacanChannel[channel]=false - return channel - end - end - - return nil -end - ---- "Return" a used TACAN channel. --- @param #SQUADRON self --- @param #number channel The channel that is available again. -function SQUADRON:ReturnTacan(channel) - self:T(self.lid..string.format("Returning Tacan channel %d", channel)) - self.tacanChannel[channel]=true -end - ---- Check if squadron is "OnDuty". --- @param #SQUADRON self --- @return #boolean If true, squdron is in state "OnDuty". -function SQUADRON:IsOnDuty() - return self:Is("OnDuty") -end - ---- Check if squadron is "Stopped". --- @param #SQUADRON self --- @return #boolean If true, squdron is in state "Stopped". -function SQUADRON:IsStopped() - return self:Is("Stopped") -end - ---- Check if squadron is "Paused". --- @param #SQUADRON self --- @return #boolean If true, squdron is in state "Paused". -function SQUADRON:IsPaused() - return self:Is("Paused") -end - - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Start & Status ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -139556,10 +159308,10 @@ function SQUADRON:onafterStatus(From, Event, To) local skill=self.skill and tostring(self.skill) or "N/A" local NassetsTot=#self.assets - local NassetsInS=self:CountAssetsInStock() + local NassetsInS=self:CountAssets(true) local NassetsQP=0 ; local NassetsP=0 ; local NassetsQ=0 - if self.airwing then - NassetsQP, NassetsP, NassetsQ=self.airwing:CountAssetsOnMission(nil, self) + if self.legion then + NassetsQP, NassetsP, NassetsQ=self.legion:CountAssetsOnMission(nil, self) end -- Short info. @@ -139577,382 +159329,3185 @@ function SQUADRON:onafterStatus(From, Event, To) end end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Check asset status. --- @param #SQUADRON self -function SQUADRON:_CheckAssetStatus() +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - if self.verbose>=2 and #self.assets>0 then - - local text="" - for j,_asset in pairs(self.assets) do - local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset - - -- Text. - text=text..string.format("\n[%d] %s (%s*%d): ", j, asset.spawngroupname, asset.unittype, asset.nunits) - - if asset.spawned then - - --- - -- Spawned - --- - - -- Mission info. - local mission=self.airwing and self.airwing:GetAssetCurrentMission(asset) or false - if mission then - local distance=asset.flightgroup and UTILS.MetersToNM(mission:GetTargetDistance(asset.flightgroup.group:GetCoordinate())) or 0 - text=text..string.format("Mission %s - %s: Status=%s, Dist=%.1f NM", mission.name, mission.type, mission.status, distance) - else - text=text.."Mission None" - end - - -- Flight status. - text=text..", Flight: " - if asset.flightgroup and asset.flightgroup:IsAlive() then - local status=asset.flightgroup:GetState() - local fuelmin=asset.flightgroup:GetFuelMin() - local fuellow=asset.flightgroup:IsFuelLow() - local fuelcri=asset.flightgroup:IsFuelCritical() - - text=text..string.format("%s Fuel=%d", status, fuelmin) - if fuelcri then - text=text.." (Critical!)" - elseif fuellow then - text=text.." (Low)" - end - - local lifept, lifept0=asset.flightgroup:GetLifePoints() - text=text..string.format(", Life=%d/%d", lifept, lifept0) - - local ammo=asset.flightgroup:GetAmmoTot() - text=text..string.format(", Ammo=%d [G=%d, R=%d, B=%d, M=%d]", ammo.Total,ammo.Guns, ammo.Rockets, ammo.Bombs, ammo.Missiles) - else - text=text.."N/A" - end +--- **Ops** - Brigade Platoon. +-- +-- **Main Features:** +-- +-- * Set parameters like livery, skill valid for all platoon members. +-- * Define modex and callsigns. +-- * Define mission types, this platoon can perform (see Ops.Auftrag#AUFTRAG). +-- * Pause/unpause platoon operations. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Ops.Platoon +-- @image OPS_Platoon.png - -- Payload info. - local payload=asset.payload and table.concat(self.airwing:GetPayloadMissionTypes(asset.payload), ", ") or "None" - text=text..", Payload={"..payload.."}" - - else - - --- - -- In Stock - --- - - text=text..string.format("In Stock") - - if self:IsRepaired(asset) then - text=text..", Combat Ready" - else - - text=text..string.format(", Repaired in %d sec", self:GetRepairTime(asset)) - if asset.damage then - text=text..string.format(" (Damage=%.1f)", asset.damage) - end - end - - if asset.Treturned then - local T=timer.getAbsTime()-asset.Treturned - text=text..string.format(", Returned for %d sec", T) - end - - end - end - self:I(self.lid..text) - end +--- PLATOON class. +-- @type PLATOON +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity level. +-- @field Ops.OpsGroup#OPSGROUP.WeaponData weaponData Weapon data table with key=BitType. +-- @extends Ops.Cohort#COHORT +--- *Some cool cohort quote* -- Known Author +-- +-- === +-- +-- # The PLATOON Concept +-- +-- A PLATOON is essential part of an BRIGADE. +-- +-- +-- +-- @field #PLATOON +PLATOON = { + ClassName = "PLATOON", + verbose = 0, + weaponData = {}, +} + +--- PLATOON class version. +-- @field #string version +PLATOON.version="0.1.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: A lot. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new PLATOON object and start the FSM. +-- @param #PLATOON self +-- @param #string TemplateGroupName Name of the template group. +-- @param #number Ngroups Number of asset groups of this platoon. Default 3. +-- @param #string PlatoonName Name of the platoon. Must be **unique**! +-- @return #PLATOON self +function PLATOON:New(TemplateGroupName, Ngroups, PlatoonName) + + -- Inherit everything from COHORT class. + local self=BASE:Inherit(self, COHORT:New(TemplateGroupName, Ngroups, PlatoonName)) -- #PLATOON + + -- All platoons get mission type Nothing. + self:AddMissionCapability(AUFTRAG.Type.NOTHING, 50) + + -- Is ground. + self.isGround=true + + -- Get ammo. + self.ammo=self:_CheckAmmo() + + return self end ---- On after "Stop" event. --- @param #SQUADRON self +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + -- TODO: Platoon specific user functions. + +--- Set brigade of this platoon. +-- @param #PLATOON self +-- @param Ops.Brigade#BRIGADE Brigade The brigade. +-- @return #PLATOON self +function PLATOON:SetBrigade(Brigade) + self.legion=Brigade + return self +end + +--- Get brigade of this platoon. +-- @param #PLATOON self +-- @return Ops.Brigade#BRIGADE The brigade. +function PLATOON:GetBrigade() + return self.legion +end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Start & Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--[[ +--- On after Start event. Starts the FLIGHTGROUP FSM and event handlers. +-- @param #PLATOON self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function SQUADRON:onafterStop(From, Event, To) +function PLATOON:onafterStart(From, Event, To) - self:I(self.lid.."STOPPING Squadron!") + -- Short info. + local text=string.format("Starting %s v%s %s", self.ClassName, self.version, self.name) + self:I(self.lid..text) - -- Remove all assets. - for i=#self.assets,1,-1 do - local asset=self.assets[i] - self:DelAsset(asset) - end + -- Start the status monitoring. + self:__Status(-1) +end +]] - self.CallScheduler:Clear() +--- On after "Status" event. +-- @param #PLATOON self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function PLATOON:onafterStatus(From, Event, To) + + if self.verbose>=1 then + + -- FSM state. + local fsmstate=self:GetState() + local callsign=self.callsignName and UTILS.GetCallsignName(self.callsignName) or "N/A" + local skill=self.skill and tostring(self.skill) or "N/A" + + local NassetsTot=#self.assets + local NassetsInS=self:CountAssets(true) + local NassetsQP=0 ; local NassetsP=0 ; local NassetsQ=0 + if self.legion then + NassetsQP, NassetsP, NassetsQ=self.legion:CountAssetsOnMission(nil, self) + end + + -- Short info. + local text=string.format("%s [Type=%s, Call=%s, Skill=%s]: Assets Total=%d, Stock=%d, Mission=%d [Active=%d, Queue=%d]", + fsmstate, self.aircrafttype, callsign, skill, NassetsTot, NassetsInS, NassetsQP, NassetsP, NassetsQ) + self:T(self.lid..text) + + -- Weapon data info. + if self.verbose>=3 and self.weaponData then + local text="Weapon Data:" + for bit,_weapondata in pairs(self.weaponData) do + local weapondata=_weapondata --Ops.OpsGroup#OPSGROUP.WeaponData + text=text..string.format("\n- Bit=%s: Rmin=%.1f km, Rmax=%.1f km", bit, weapondata.RangeMin/1000, weapondata.RangeMax/1000) + end + self:I(self.lid..text) + end + + -- Check if group has detected any units. + self:_CheckAssetStatus() + + end + + if not self:IsStopped() then + self:__Status(-60) + end end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Misc Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Check if there is a squadron that can execute a given mission. --- We check the mission type, the refuelling system, engagement range --- @param #SQUADRON self --- @param Ops.Auftrag#AUFTRAG Mission The mission. --- @return #boolean If true, Squadron can do that type of mission. -function SQUADRON:CanMission(Mission) - - local cando=true - - -- On duty?= - if not self:IsOnDuty() then - self:T(self.lid..string.format("Squad in not OnDuty but in state %s. Cannot do mission %s with target %s", self:GetState(), Mission.name, Mission:GetTargetName())) - return false +-- TODO: Misc functions. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- **Ops** - Legion Warehouse. +-- +-- Parent class of Airwings, Brigades and Fleets. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module Ops.Legion +-- @image OPS_Legion.png + + +--- LEGION class. +-- @type LEGION +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity of output. +-- @field #string lid Class id string for output to DCS log file. +-- @field #table missionqueue Mission queue table. +-- @field #table transportqueue Transport queue. +-- @field #table cohorts Cohorts of this legion. +-- @field Ops.Commander#COMMANDER commander Commander of this legion. +-- @field Ops.Chief#CHIEF chief Chief of this legion. +-- @extends Functional.Warehouse#WAREHOUSE + +--- *Per aspera ad astra.* +-- +-- === +-- +-- # The LEGION Concept +-- +-- The LEGION class contains all functions that are common for the AIRWING, BRIGADE and FLEET classes, which inherit the LEGION class. +-- +-- An LEGION consists of multiple COHORTs. These cohorts "live" in a WAREHOUSE, i.e. a physical structure that can be destroyed or captured. +-- +-- ** The LEGION class is not meant to be used directly. Use AIRWING, BRIGADE or FLEET instead! ** +-- +-- @field #LEGION +LEGION = { + ClassName = "LEGION", + verbose = 0, + lid = nil, + missionqueue = {}, + transportqueue = {}, + cohorts = {}, +} + +--- LEGION class version. +-- @field #string version +LEGION.version="0.3.4" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- DONE: Create FLEED class. +-- DONE: Relocate cohorts. +-- DONE: Aircraft will not start hot on Alert5. +-- DONE: OPS transport. +-- DONE: Make general so it can be inherited by AIRWING and BRIGADE classes. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new LEGION class object. +-- @param #LEGION self +-- @param #string WarehouseName Name of the warehouse STATIC or UNIT object representing the warehouse. +-- @param #string LegionName Name of the legion. Must be **unique**! +-- @return #LEGION self +function LEGION:New(WarehouseName, LegionName) + + -- Inherit everything from WAREHOUSE class. + local self=BASE:Inherit(self, WAREHOUSE:New(WarehouseName, LegionName)) -- #LEGION + + -- Nil check. + if not self then + BASE:E(string.format("ERROR: Could not find warehouse %s!", WarehouseName)) + return nil end - -- Check mission type. WARNING: This assumes that all assets of the squad can do the same mission types! - if not self:CheckMissionType(Mission.type, self:GetMissionTypes()) then - self:T(self.lid..string.format("INFO: Squad cannot do mission type %s (%s, %s)", Mission.type, Mission.name, Mission:GetTargetName())) - return false - end + -- Set some string id for output to DCS.log file. + self.lid=string.format("LEGION %s | ", self.alias) - -- Check that tanker mission - if Mission.type==AUFTRAG.Type.TANKER then + -- Defaults: + self:SetMarker(false) - if Mission.refuelSystem and Mission.refuelSystem==self.tankerSystem then - -- Correct refueling system. - else - self:T(self.lid..string.format("INFO: Wrong refueling system requested=%s != %s=available", tostring(Mission.refuelSystem), tostring(self.tankerSystem))) - return false - end + -- Dead and crash events are handled via opsgroups. + self:UnHandleEvent(EVENTS.Crash) + self:UnHandleEvent(EVENTS.Dead) + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("*", "MissionRequest", "*") -- Add a (mission) request to the warehouse. + self:AddTransition("*", "MissionCancel", "*") -- Cancel mission. + self:AddTransition("*", "MissionAssign", "*") -- Recruit assets, add to queue and request immediately. - end + self:AddTransition("*", "TransportRequest", "*") -- Add a (mission) request to the warehouse. + self:AddTransition("*", "TransportCancel", "*") -- Cancel transport. + self:AddTransition("*", "TransportAssign", "*") -- Recruit assets, add to queue and request immediately. - -- Distance to target. - local TargetDistance=Mission:GetTargetDistance(self.airwing:GetCoordinate()) + self:AddTransition("*", "OpsOnMission", "*") -- An OPSGROUP was send on a Mission (AUFTRAG). - -- Max engage range. - local engagerange=Mission.engageRange and math.max(self.engageRange, Mission.engageRange) or self.engageRange - - -- Set range is valid. Mission engage distance can overrule the squad engage range. - if TargetDistance>engagerange then - self:I(self.lid..string.format("INFO: Squad is not in range. Target dist=%d > %d NM max mission Range", UTILS.MetersToNM(TargetDistance), UTILS.MetersToNM(engagerange))) - return false - end + self:AddTransition("*", "LegionAssetReturned", "*") -- An asset returned (from a mission) to the Legion warehouse. - return true + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the LEGION. Initializes parameters and starts event handlers. + -- @function [parent=#LEGION] Start + -- @param #LEGION self + + --- Triggers the FSM event "Start" after a delay. Starts the LEGION. Initializes parameters and starts event handlers. + -- @function [parent=#LEGION] __Start + -- @param #LEGION self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the LEGION and all its event handlers. + -- @param #LEGION self + + --- Triggers the FSM event "Stop" after a delay. Stops the LEGION and all its event handlers. + -- @function [parent=#LEGION] __Stop + -- @param #LEGION self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "MissionCancel". + -- @function [parent=#LEGION] MissionCancel + -- @param #LEGION self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "MissionAssign". + -- @function [parent=#LEGION] MissionAssign + -- @param #LEGION self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Legions The legion(s) from which the mission assets are requested. + + --- Triggers the FSM event "MissionAssign" after a delay. + -- @function [parent=#LEGION] __MissionAssign + -- @param #LEGION self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Legions The legion(s) from which the mission assets are requested. + + --- On after "MissionAssign" event. + -- @function [parent=#LEGION] OnAfterMissionAssign + -- @param #LEGION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Legions The legion(s) from which the mission assets are requested. + + + --- Triggers the FSM event "MissionRequest". + -- @function [parent=#LEGION] MissionRequest + -- @param #LEGION self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "MissionRequest" after a delay. + -- @function [parent=#LEGION] __MissionRequest + -- @param #LEGION self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "MissionRequest" event. + -- @function [parent=#LEGION] OnAfterMissionRequest + -- @param #LEGION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "MissionCancel" after a delay. + -- @function [parent=#LEGION] __MissionCancel + -- @param #LEGION self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "MissionCancel" event. + -- @function [parent=#LEGION] OnAfterMissionCancel + -- @param #LEGION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "TransportAssign". + -- @function [parent=#LEGION] TransportAssign + -- @param #LEGION self + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + -- @param #table Legions The legion(s) to which this transport is assigned. + + --- Triggers the FSM event "TransportAssign" after a delay. + -- @function [parent=#LEGION] __TransportAssign + -- @param #LEGION self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + -- @param #table Legions The legion(s) to which this transport is assigned. + + --- On after "TransportAssign" event. + -- @function [parent=#LEGION] OnAfterTransportAssign + -- @param #LEGION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + -- @param #table Legions The legion(s) to which this transport is assigned. + + + --- Triggers the FSM event "TransportRequest". + -- @function [parent=#LEGION] TransportRequest + -- @param #LEGION self + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- Triggers the FSM event "TransportRequest" after a delay. + -- @function [parent=#LEGION] __TransportRequest + -- @param #LEGION self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- On after "TransportRequest" event. + -- @function [parent=#LEGION] OnAfterTransportRequest + -- @param #LEGION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + + --- Triggers the FSM event "TransportCancel". + -- @function [parent=#LEGION] TransportCancel + -- @param #LEGION self + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- Triggers the FSM event "TransportCancel" after a delay. + -- @function [parent=#LEGION] __TransportCancel + -- @param #LEGION self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- On after "TransportCancel" event. + -- @function [parent=#LEGION] OnAfterTransportCancel + -- @param #LEGION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + + --- Triggers the FSM event "OpsOnMission". + -- @function [parent=#LEGION] OpsOnMission + -- @param #LEGION self + -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "OpsOnMission" after a delay. + -- @function [parent=#LEGION] __OpsOnMission + -- @param #LEGION self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "OpsOnMission" event. + -- @function [parent=#LEGION] OnAfterOpsOnMission + -- @param #LEGION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "LegionAssetReturned". + -- @function [parent=#LEGION] LegionAssetReturned + -- @param #LEGION self + -- @param Ops.Cohort#COHORT Cohort The cohort the asset belongs to. + -- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset that returned. + + --- Triggers the FSM event "LegionAssetReturned" after a delay. + -- @function [parent=#LEGION] __LegionAssetReturned + -- @param #LEGION self + -- @param #number delay Delay in seconds. + -- @param Ops.Cohort#COHORT Cohort The cohort the asset belongs to. + -- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset that returned. + + --- On after "LegionAssetReturned" event. Triggered when an asset group returned to its Legion. + -- @function [parent=#LEGION] OnAfterLegionAssetReturned + -- @param #LEGION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Cohort#COHORT Cohort The cohort the asset belongs to. + -- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset that returned. + + + return self end ---- Count assets in airwing (warehous) stock. --- @param #SQUADRON self --- @return #number Assets not spawned. -function SQUADRON:CountAssetsInStock() +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - local N=0 - for _,_asset in pairs(self.assets) do - local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset - if asset.spawned then +--- Set verbosity level. +-- @param #LEGION self +-- @param #number VerbosityLevel Level of output (higher=more). Default 0. +-- @return #LEGION self +function LEGION:SetVerbosity(VerbosityLevel) + self.verbose=VerbosityLevel or 0 + return self +end + +--- Add a mission for the legion. It will pick the best available assets for the mission and lauch it when ready. +-- @param #LEGION self +-- @param Ops.Auftrag#AUFTRAG Mission Mission for this legion. +-- @return #LEGION self +function LEGION:AddMission(Mission) + + -- Set status to QUEUED. This event is only allowed for the first legion that calls it. + Mission:Queued() + + -- Set legion status. + Mission:SetLegionStatus(self, AUFTRAG.Status.QUEUED) + + -- Add legion to mission. + Mission:AddLegion(self) + + -- Set target for ALERT 5. + if Mission.type==AUFTRAG.Type.ALERT5 then + Mission:_TargetFromObject(self:GetCoordinate()) + end + + -- Add mission to queue. + table.insert(self.missionqueue, Mission) + + -- Info text. + local text=string.format("Added mission %s (type=%s). Starting at %s. Stopping at %s", + tostring(Mission.name), tostring(Mission.type), UTILS.SecondsToClock(Mission.Tstart, true), Mission.Tstop and UTILS.SecondsToClock(Mission.Tstop, true) or "INF") + self:T(self.lid..text) + + return self +end + +--- Remove mission from queue. +-- @param #LEGION self +-- @param Ops.Auftrag#AUFTRAG Mission Mission to be removed. +-- @return #LEGION self +function LEGION:RemoveMission(Mission) + + for i,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission.auftragsnummer==Mission.auftragsnummer then + mission:RemoveLegion(self) + table.remove(self.missionqueue, i) + break + end + + end + + return self +end + +--- Add transport assignment to queue. +-- @param #LEGION self +-- @param Ops.OpsTransport#OPSTRANSPORT OpsTransport Transport assignment. +-- @return #LEGION self +function LEGION:AddOpsTransport(OpsTransport) + + -- Is not queued at a legion. + OpsTransport:Queued() + + -- Set legion status. + OpsTransport:SetLegionStatus(self, AUFTRAG.Status.QUEUED) + + -- Add mission to queue. + table.insert(self.transportqueue, OpsTransport) + + -- Add this legion to the transport. + OpsTransport:AddLegion(self) + + -- Info text. + local text=string.format("Added Transport %s. Starting at %s-%s", + tostring(OpsTransport.uid), UTILS.SecondsToClock(OpsTransport.Tstart, true), OpsTransport.Tstop and UTILS.SecondsToClock(OpsTransport.Tstop, true) or "INF") + self:T(self.lid..text) + + return self +end + +--- Add cohort to cohort table of this legion. +-- @param #LEGION self +-- @param Ops.Cohort#COHORT Cohort The cohort to be added. +-- @return #LEGION self +function LEGION:AddCohort(Cohort) + + if self:IsCohort(Cohort.name) then + + self:E(self.lid..string.format("ERROR: A cohort with name %s already exists in this legion. Cohorts must have UNIQUE names!")) + + else + + -- Add cohort to legion. + table.insert(self.cohorts, Cohort) + + end + + return self +end + +--- Remove cohort from cohor table of this legion. +-- @param #LEGION self +-- @param Ops.Cohort#COHORT Cohort The cohort to be added. +-- @return #LEGION self +function LEGION:DelCohort(Cohort) + + for i=#self.cohorts,1,-1 do + local cohort=self.cohorts[i] --Ops.Cohort#COHORT + if cohort.name==Cohort.name then + self:T(self.lid..string.format("Removing Cohort %s", tostring(cohort.name))) + table.remove(self.cohorts, i) + end + end + + return self +end + + +--- Relocate a cohort to another legion. +-- Assets in stock are spawned and routed to the new legion. +-- If assets are spawned, running missions will be cancelled. +-- Cohort assets will not be available until relocation is finished. +-- @param #LEGION self +-- @param Ops.Cohort#COHORT Cohort The cohort to be relocated. +-- @param Ops.Legion#LEGION Legion The legion where the cohort is relocated to. +-- @param #number Delay Delay in seconds before relocation takes place. Default `nil`, *i.e.* ASAP. +-- @param #number NcarriersMin Min number of transport carriers in case the troops should be transported. Default `nil` for no transport. +-- @param #number NcarriersMax Max number of transport carriers. +-- @param #table TransportLegions Legion(s) assigned for transportation. Default is that transport assets can only be recruited from this legion. +-- @return #LEGION self +function LEGION:RelocateCohort(Cohort, Legion, Delay, NcarriersMin, NcarriersMax, TransportLegions) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, LEGION.RelocateCohort, self, Cohort, Legion, 0, NcarriersMin, NcarriersMax, TransportLegions) + else + + -- Add cohort to legion. + if Legion:IsCohort(Cohort.name) then + self:E(self.lid..string.format("ERROR: Cohort %s is already part of new legion %s ==> CANNOT Relocate!", Cohort.name, Legion.alias)) + return self + else + table.insert(Legion.cohorts, Cohort) + end + + -- Check that cohort is part of this legion + if not self:IsCohort(Cohort.name) then + self:E(self.lid..string.format("ERROR: Cohort %s is NOT part of this legion %s ==> CANNOT Relocate!", Cohort.name, self.alias)) + return self + end + + -- Check that legions are different. + if self.alias==Legion.alias then + self:E(self.lid..string.format("ERROR: old legion %s is same as new legion %s ==> CANNOT Relocate!", self.alias, Legion.alias)) + return self + end + + -- Trigger Relocate event. + Cohort:Relocate() + + -- Create a relocation mission. + local mission=AUFTRAG:_NewRELOCATECOHORT(Legion, Cohort) + + if false then + --- Disabled for now. + + -- Add assets to mission. + mission:_AddAssets(Cohort.assets) + + -- Debug info. + self:I(self.lid..string.format("Relocating Cohort %s [nassets=%d] to legion %s", Cohort.name, #Cohort.assets, Legion.alias)) + + -- Assign mission to this legion. + self:MissionAssign(mission, {self}) else + + -- Assign cohort to mission. + mission:AssignCohort(Cohort) + + -- All assets required. + mission:SetRequiredAssets(#Cohort.assets) + + -- Set transportation. + if NcarriersMin and NcarriersMin>0 then + mission:SetRequiredTransport(Legion.spawnzone, NcarriersMin, NcarriersMax) + end + + -- Assign transport legions. + if TransportLegions then + for _,legion in pairs(TransportLegions) do + mission:AssignTransportLegion(legion) + end + end + + -- Set mission range very large. Mission designer should know... + mission:SetMissionRange(10000) + + -- Add mission. + self:AddMission(mission) + end + + end + + return self +end + +--- Get cohort by name. +-- @param #LEGION self +-- @param #string CohortName Name of the platoon. +-- @return Ops.Cohort#COHORT The Cohort object. +function LEGION:_GetCohort(CohortName) + + for _,_cohort in pairs(self.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + if cohort.name==CohortName then + return cohort + end + + end + + return nil +end + +--- Check if cohort is part of this legion. +-- @param #LEGION self +-- @param #string CohortName Name of the platoon. +-- @return #boolean If `true`, cohort is part of this legion. +function LEGION:IsCohort(CohortName) + + for _,_cohort in pairs(self.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + if cohort.name==CohortName then + return true + end + + end + + return false +end + +--- Get cohort of an asset. +-- @param #LEGION self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset. +-- @return Ops.Cohort#COHORT The Cohort object. +function LEGION:_GetCohortOfAsset(Asset) + local cohort=self:_GetCohort(Asset.squadname) + return cohort +end + + +--- Check if a BRIGADE class is calling. +-- @param #LEGION self +-- @return #boolean If true, this is a BRIGADE. +function LEGION:IsBrigade() + local is=self.ClassName==BRIGADE.ClassName + return is +end + +--- Check if the AIRWING class is calling. +-- @param #LEGION self +-- @return #boolean If true, this is an AIRWING. +function LEGION:IsAirwing() + local is=self.ClassName==AIRWING.ClassName + return is +end + +--- Check if the FLEET class is calling. +-- @param #LEGION self +-- @return #boolean If true, this is a FLEET. +function LEGION:IsFleet() + local is=self.ClassName==FLEET.ClassName + return is +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Start & Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Start LEGION FSM. +-- @param #LEGION self +function LEGION:onafterStart(From, Event, To) + + -- Start parent Warehouse. + self:GetParent(self, LEGION).onafterStart(self, From, Event, To) + + -- Info. + self:T3(self.lid..string.format("Starting LEGION v%s", LEGION.version)) + +end + +--- Check mission queue and assign ONE mission. +-- @param #LEGION self +-- @return #boolean If `true`, a mission was found and requested. +function LEGION:CheckMissionQueue() + + -- Number of missions. + local Nmissions=#self.missionqueue + + -- Treat special cases. + if Nmissions==0 then + return nil + end + + -- Loop over missions in queue. + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission:IsNotOver() and mission:IsReadyToCancel() then + mission:Cancel() + end + end + + -- Check that runway is operational and that carrier is not recovering. + if self:IsAirwing() then + if self:IsRunwayOperational()==false then + return nil + end + local airboss=self.airboss --Ops.Airboss#AIRBOSS + if airboss then + if not airboss:IsIdle() then + return nil + end + end + end + + -- Sort results table wrt prio and start time. + local function _sort(a, b) + local taskA=a --Ops.Auftrag#AUFTRAG + local taskB=b --Ops.Auftrag#AUFTRAG + return (taskA.prio Request and return. + self:TransportRequest(transport) + return true + end + + end + end + + -- No transport found. + return nil +end + + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Events +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "MissionAssign" event. Mission is added to a LEGION mission queue and already requested. Needs assets to be added to the mission already. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @param #table Legions The LEGIONs. +function LEGION:onafterMissionAssign(From, Event, To, Mission, Legions) + + for _,_Legion in pairs(Legions) do + local Legion=_Legion --Ops.Legion#LEGION + + -- Debug info. + self:T(self.lid..string.format("Assigning mission %s (%s) to legion %s", Mission.name, Mission.type, Legion.alias)) + + -- Add mission to legion. + Legion:AddMission(Mission) + + -- Directly request the mission as the assets have already been selected. + Legion:MissionRequest(Mission) + + end + +end + + +--- On after "MissionRequest" event. Performs a self request to the warehouse for the mission assets. Sets mission status to REQUESTED. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Auftrag#AUFTRAG Mission The requested mission. +function LEGION:onafterMissionRequest(From, Event, To, Mission) + + -- Debug info. + self:T(self.lid..string.format("MissionRequest for mission %s [%s]", Mission:GetName(), Mission:GetType())) + + -- Set mission status from QUEUED to REQUESTED. + Mission:Requested() + + -- Set legion status. Ensures that it is not considered in the next selection. + Mission:SetLegionStatus(self, AUFTRAG.Status.REQUESTED) + + --- + -- Some assets might already be spawned and even on a different mission (orbit). + -- Need to dived to set into spawned and instock assets and handle the other + --- + + -- Assets to be requested. + local Assetlist={} + + for _,_asset in pairs(Mission.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Check that this asset belongs to this Legion warehouse. + if asset.wid==self.uid then + + if asset.spawned then + + --- + -- Spawned Assets + --- + + if asset.flightgroup then + + -- Add new mission. + asset.flightgroup:AddMission(Mission) + + --- + -- Special Missions + --- + + -- Get current mission. + local currM=asset.flightgroup:GetMissionCurrent() + + if currM then + + -- Cancel? + local cancel=false + -- Pause? + local pause=false + + -- Check if mission is INTERCEPT and asset is currently on GCI mission. If so, GCI is paused. + if currM.type==AUFTRAG.Type.GCICAP and Mission.type==AUFTRAG.Type.INTERCEPT then + pause=true + elseif (currM.type==AUFTRAG.Type.ONGUARD or currM.type==AUFTRAG.Type.PATROLZONE) and (Mission.type==AUFTRAG.Type.ARTY or Mission.type==AUFTRAG.Type.GROUNDATTACK) then + pause=true + elseif currM.type==AUFTRAG.Type.NOTHING then + pause=true + end + + -- Cancel current ALERT5 mission. + if currM.type==AUFTRAG.Type.ALERT5 then + cancel=true + end + + -- Cancel current mission for relcation. + if Mission.type==AUFTRAG.Type.RELOCATECOHORT then + cancel=true + + -- Get request ID. + local requestID=currM.requestID[self.alias] + + -- Get request. + local request=self:GetRequestByID(requestID) + + if request then + self:T2(self.lid.."Removing group from cargoset") + request.cargogroupset:Remove(asset.spawngroupname, true) + else + self:E(self.lid.."ERROR: no request for spawned asset!") + end + + end + + -- Cancel mission. + if cancel then + self:T(self.lid..string.format("Cancel current mission %s [%s] to send group on mission %s [%s]", currM.name, currM.type, Mission.name, Mission.type)) + asset.flightgroup:MissionCancel(currM) + elseif pause then + self:T(self.lid..string.format("Pausing current mission %s [%s] to send group on mission %s [%s]", currM.name, currM.type, Mission.name, Mission.type)) + asset.flightgroup:PauseMission() + end + + -- Not reserved any more. + asset.isReserved=false + + end + + -- Trigger event. + self:__OpsOnMission(2, asset.flightgroup, Mission) + + else + self:E(self.lid.."ERROR: OPSGROUP for asset does NOT exist but it seems to be SPAWNED (asset.spawned=true)!") + end + + else + + --- + -- Stock Assets + --- + + -- These assets need to be requested and spawned. + table.insert(Assetlist, asset) + + end + + end + end + + -- Add request to legion warehouse. + if #Assetlist>0 then + + --local text=string.format("Requesting assets for mission %s:", Mission.name) + for i,_asset in pairs(Assetlist) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Set asset to requested! Important so that new requests do not use this asset! + asset.requested=true + + -- Spawned asset are not requested. + if asset.spawned then + asset.requested=false + end + + -- Not reserved and more. + asset.isReserved=false + + -- Set mission task so that the group is spawned with the right one. + if Mission.missionTask then + asset.missionTask=Mission.missionTask + end + + -- Set takeoff type to parking for ALERT5 missions. We dont want them to take off without a proper mission if squadron start is hot. + if Mission.type==AUFTRAG.Type.ALERT5 then + asset.takeoffType=COORDINATE.WaypointType.TakeOffParking + end + + end + + -- Set assignment. + -- TODO: Get/set functions for assignment string. + local assignment=string.format("Mission-%d", Mission.auftragsnummer) + + -- Add request to legion warehouse. + self:AddRequest(self, WAREHOUSE.Descriptor.ASSETLIST, Assetlist, #Assetlist, nil, nil, Mission.prio, assignment) + + -- The queueid has been increased in the onafterAddRequest function. So we can simply use it here. + Mission.requestID[self.alias]=self.queueid + + -- Get request. + local request=self:GetRequestByID(self.queueid) + + -- Debug info. + self:T(self.lid..string.format("Mission %s [%s] got Request ID=%d", Mission:GetName(), Mission:GetType(), self.queueid)) + + -- Request ship. + if request then + if self:IsShip() then + self:T(self.lid.."Warehouse physical structure is SHIP. Requestes assets will be late activated!") + request.lateActivation=true + end + end + + end + +end + +--- On after "TransportAssign" event. Transport is added to a LEGION transport queue and assets are requested from the LEGION warehouse. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. +-- @param #table Legions The legion(s) to which the transport is assigned. +function LEGION:onafterTransportAssign(From, Event, To, Transport, Legions) + + for _,_Legion in pairs(Legions) do + local Legion=_Legion --Ops.Legion#LEGION + + -- Debug info. + self:T(self.lid..string.format("Assigning transport %d to legion %s", Transport.uid, Legion.alias)) + + -- Add mission to legion. + Legion:AddOpsTransport(Transport) + + -- Directly request the mission as the assets have already been selected. + Legion:TransportRequest(Transport) + + end + +end + +--- On after "TransportRequest" event. Performs a self request to the warehouse for the transport assets. Sets transport status to REQUESTED. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsTransport#OPSTRANSPORT Opstransport The requested mission. +function LEGION:onafterTransportRequest(From, Event, To, OpsTransport) + + -- List of assets that will be requested. + local AssetList={} + + --TODO: Find spawned assets on ALERT 5 mission OPSTRANSPORT. + + --local text=string.format("Requesting assets for mission %s:", Mission.name) + for i,_asset in pairs(OpsTransport.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Check that this asset belongs to this Legion warehouse. + if asset.wid==self.uid then + + -- Set asset to requested! Important so that new requests do not use this asset! + asset.requested=true + asset.isReserved=false + + -- Set transport mission task. + asset.missionTask=ENUMS.MissionTask.TRANSPORT + + -- Add asset to list. + table.insert(AssetList, asset) + end + end + + if #AssetList>0 then + + -- Set mission status from QUEUED to REQUESTED. + OpsTransport:Requested() + + -- Set legion status. Ensures that it is not considered in the next selection. + OpsTransport:SetLegionStatus(self, OPSTRANSPORT.Status.REQUESTED) + + -- TODO: Get/set functions for assignment string. + local assignment=string.format("Transport-%d", OpsTransport.uid) + + -- Add request to legion warehouse. + self:AddRequest(self, WAREHOUSE.Descriptor.ASSETLIST, AssetList, #AssetList, nil, nil, OpsTransport.prio, assignment) + + -- The queueid has been increased in the onafterAddRequest function. So we can simply use it here. + OpsTransport.requestID[self.alias]=self.queueid + end + +end + +--- On after "TransportCancel" event. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport to be cancelled. +function LEGION:onafterTransportCancel(From, Event, To, Transport) + + -- Info message. + self:T(self.lid..string.format("Cancel transport UID=%d", Transport.uid)) + + -- Set status to cancelled. + Transport:SetLegionStatus(self, OPSTRANSPORT.Status.CANCELLED) + + for i=#Transport.assets, 1, -1 do + local asset=Transport.assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Asset should belong to this legion. + if asset.wid==self.uid then + + local opsgroup=asset.flightgroup + + if opsgroup then + opsgroup:TransportCancel(Transport) + end + + -- Delete awaited transport. + local cargos=Transport:GetCargoOpsGroups(false) + for _,_cargo in pairs(cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP + + -- Remover my lift. + cargo:_DelMyLift(Transport) + + -- Legion of cargo group + local legion=cargo.legion + + -- Add asset back to legion. + if legion then + legion:T(self.lid..string.format("Adding cargo group %s back to legion", cargo:GetName())) + legion:__AddAsset(0.1, cargo.group, 1) + end + end + + -- Remove asset from mission. + Transport:DelAsset(asset) + + -- Not requested any more (if it was). + asset.requested=nil + asset.isReserved=nil + + end + end + + -- Remove queued request (if any). + if Transport.requestID[self.alias] then + self:_DeleteQueueItemByID(Transport.requestID[self.alias], self.queue) + end + +end + + +--- On after "MissionCancel" event. Cancels the missions of all flightgroups. Deletes request from warehouse queue. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Auftrag#AUFTRAG Mission The mission to be cancelled. +function LEGION:onafterMissionCancel(From, Event, To, Mission) + + -- Info message. + self:T(self.lid..string.format("Cancel mission %s", Mission.name)) + + -- Set status to cancelled. + Mission:SetLegionStatus(self, AUFTRAG.Status.CANCELLED) + + for i=#Mission.assets, 1, -1 do + local asset=Mission.assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Asset should belong to this legion. + if asset.wid==self.uid then + + local opsgroup=asset.flightgroup + + if opsgroup then + opsgroup:MissionCancel(Mission) + end + + -- Remove asset from mission. + Mission:DelAsset(asset) + + -- Not requested any more (if it was). + asset.requested=nil + asset.isReserved=nil + + end + end + + -- Remove queued request (if any). + if Mission.requestID[self.alias] then + self:_DeleteQueueItemByID(Mission.requestID[self.alias], self.queue) + end + +end + +--- On after "OpsOnMission". +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP OpsGroup Ops group on mission +-- @param Ops.Auftrag#AUFTRAG Mission The requested mission. +function LEGION:onafterOpsOnMission(From, Event, To, OpsGroup, Mission) + -- Debug info. + self:T2(self.lid..string.format("Group %s on mission %s [%s]", OpsGroup:GetName(), Mission:GetName(), Mission:GetType())) + + if self:IsAirwing() then + -- Trigger event for Airwings. + self:FlightOnMission(OpsGroup, Mission) + elseif self:IsBrigade() then + -- Trigger event for Brigades. + self:ArmyOnMission(OpsGroup, Mission) + else + -- Trigger event for Fleets. + self:NavyOnMission(OpsGroup, Mission) + end + + -- Load group as cargo because it cannot swim! We pause the mission. + if self:IsBrigade() and self:IsShip() then + OpsGroup:PauseMission() + self.warehouseOpsGroup:Load(OpsGroup, self.warehouseOpsElement) + end + + -- Trigger event for chief. + if self.chief then + self.chief:OpsOnMission(OpsGroup, Mission) + end + + -- Trigger event for commander. + if self.commander then + self.commander:OpsOnMission(OpsGroup, Mission) + end + +end + +--- On after "NewAsset" event. Asset is added to the given cohort (asset assignment). +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The asset that has just been added. +-- @param #string assignment The (optional) assignment for the asset. +function LEGION:onafterNewAsset(From, Event, To, asset, assignment) + + -- Call parent WAREHOUSE function first. + self:GetParent(self, LEGION).onafterNewAsset(self, From, Event, To, asset, assignment) + + -- Debug text. + local text=string.format("New asset %s with assignment %s and request assignment %s", asset.spawngroupname, tostring(asset.assignment), tostring(assignment)) + self:T(self.lid..text) + + -- Get cohort. + local cohort=self:_GetCohort(asset.assignment) + + -- Check if asset is already part of the squadron. If an asset returns, it will be added again! We check that asset.assignment is also assignment. + if cohort then + + if asset.assignment==assignment then + + --- + -- Asset is added to the COHORT for the first time + --- + + local nunits=#asset.template.units + + -- Debug text. + local text=string.format("Adding asset to cohort %s: assignment=%s, type=%s, attribute=%s, nunits=%d ngroup=%s", cohort.name, assignment, asset.unittype, asset.attribute, nunits, tostring(cohort.ngrouping)) + self:T(self.lid..text) + + -- Adjust number of elements in the group. + if cohort.ngrouping then + local template=asset.template + + local N=math.max(#template.units, cohort.ngrouping) + + -- We need to recalc the total weight and cargo bay. + asset.weight=0 + asset.cargobaytot=0 + + -- Handle units. + for i=1,N do + + -- Unit template. + local unit = template.units[i] + + -- If grouping is larger than units present, copy first unit. + if i>nunits then + table.insert(template.units, UTILS.DeepCopy(template.units[1])) + asset.cargobaytot=asset.cargobaytot+asset.cargobay[1] + asset.weight=asset.weight+asset.weights[1] + template.units[i].x=template.units[1].x+5*(i-nunits) + template.units[i].y=template.units[1].y+5*(i-nunits) + else + if i<=cohort.ngrouping then + asset.weight=asset.weight+asset.weights[i] + asset.cargobaytot=asset.cargobaytot+asset.cargobay[i] + end + end + + -- Remove units if original template contains more than in grouping. + if i>cohort.ngrouping then + template.units[i]=nil + end + end + + -- Set number of units. + asset.nunits=cohort.ngrouping + + -- Debug info. + self:T(self.lid..string.format("After regrouping: Nunits=%d, weight=%.1f cargobaytot=%.1f kg", #asset.template.units, asset.weight, asset.cargobaytot)) + end + + -- Set takeoff type. + asset.takeoffType=cohort.takeoffType~=nil and cohort.takeoffType or self.takeoffType + + -- Set parking IDs. + asset.parkingIDs=cohort.parkingIDs + + -- Create callsign and modex (needs to be after grouping). + cohort:GetCallsign(asset) + cohort:GetModex(asset) + + -- Set spawn group name. This has to include "AID-" for warehouse. + asset.spawngroupname=string.format("%s_AID-%d", cohort.name, asset.uid) + + -- Add asset to cohort. + cohort:AddAsset(asset) + + else + + --- + -- Asset is returned to the COHORT + --- + + self:T(self.lid..string.format("Asset returned to legion ==> calling LegionAssetReturned event")) + + -- Set takeoff type in case it was overwritten for an ALERT5 mission. + asset.takeoffType=cohort.takeoffType + + -- Trigger event. + self:LegionAssetReturned(cohort, asset) + + end + + end +end + +--- On after "LegionAssetReturned" event. Triggered when an asset group returned to its legion. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Cohort#COHORT Cohort The cohort the asset belongs to. +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset that returned. +function LEGION:onafterLegionAssetReturned(From, Event, To, Cohort, Asset) + -- Debug message. + self:I(self.lid..string.format("Asset %s from Cohort %s returned! asset.assignment=\"%s\"", Asset.spawngroupname, Cohort.name, tostring(Asset.assignment))) + + -- Stop flightgroup. + if Asset.flightgroup and not Asset.flightgroup:IsStopped() then + Asset.flightgroup:Stop() + end + + -- Return payload. + if Asset.flightgroup:IsFlightgroup() then + self:ReturnPayloadFromAsset(Asset) + end + + -- Return tacan channel. + if Asset.tacan then + Cohort:ReturnTacan(Asset.tacan) + end + + -- Set timestamp. + Asset.Treturned=timer.getAbsTime() + +end + + +--- On after "AssetSpawned" event triggered when an asset group is spawned into the cruel world. +-- Creates a new flightgroup element and adds the mission to the flightgroup queue. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Group#GROUP group The group spawned. +-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The asset that was spawned. +-- @param Functional.Warehouse#WAREHOUSE.Pendingitem request The request of the dead asset. +function LEGION:onafterAssetSpawned(From, Event, To, group, asset, request) + self:T({From, Event, To, group:GetName(), asset.assignment, request.assignment}) + + -- Call parent warehouse function first. + self:GetParent(self, LEGION).onafterAssetSpawned(self, From, Event, To, group, asset, request) + + -- Get the COHORT of the asset. + local cohort=self:_GetCohortOfAsset(asset) + + -- Check if we have a cohort or if this was some other request. + if cohort then + + -- Debug info. + self:T(self.lid..string.format("Cohort asset spawned %s", asset.spawngroupname)) + + -- Create a flight group. + local flightgroup=self:_CreateFlightGroup(asset) + + --- + -- Asset + --- + + -- Set asset flightgroup. + asset.flightgroup=flightgroup + + -- Not requested any more. + asset.requested=nil + + -- Did not return yet. + asset.Treturned=nil + + + --- + -- Cohort + --- + + -- Get TACAN channel. + local Tacan=cohort:FetchTacan() + if Tacan then + asset.tacan=Tacan + flightgroup:SwitchTACAN(Tacan, Morse, UnitName, Band) + end + + -- Set radio frequency and modulation + local radioFreq, radioModu=cohort:GetRadio() + if radioFreq then + flightgroup:SwitchRadio(radioFreq, radioModu) + end + + if cohort.fuellow then + flightgroup:SetFuelLowThreshold(cohort.fuellow) + end + + if cohort.fuellowRefuel then + flightgroup:SetFuelLowRefuel(cohort.fuellowRefuel) + end + + -- Assignment. + local assignment=request.assignment + + -- Set pathfinding for naval groups. + if self:IsFleet() then + flightgroup:SetPathfinding(self.pathfinding) + end + + if string.find(assignment, "Mission-") then + + --- + -- Mission + --- + + local uid=UTILS.Split(assignment, "-")[2] + + -- Get Mission (if any). + local mission=self:GetMissionByID(uid) + + local despawnLanding=cohort.despawnAfterLanding~=nil and cohort.despawnAfterLanding or self.despawnAfterLanding + if despawnLanding then + flightgroup:SetDespawnAfterLanding() + end + + local despawnHolding=cohort.despawnAfterHolding~=nil and cohort.despawnAfterHolding or self.despawnAfterHolding + if despawnHolding then + flightgroup:SetDespawnAfterHolding() + end + + -- Add mission to flightgroup queue. + if mission then + + if Tacan then + --mission:SetTACAN(Tacan, Morse, UnitName, Band) + end + + -- Add mission to flightgroup queue. If mission has an OPSTRANSPORT attached, all added OPSGROUPS are added as CARGO for a transport. + flightgroup:AddMission(mission) + + -- RTZ on out of ammo. + if self:IsBrigade() or self:IsFleet() then + flightgroup:SetReturnOnOutOfAmmo() + end + + -- Trigger event. + self:__OpsOnMission(5, flightgroup, mission) + + else + + if Tacan then + --flightgroup:SwitchTACAN(Tacan, Morse, UnitName, Band) + end + + end + + -- Add group to the detection set of the CHIEF (INTEL). + local chief=self.chief or (self.commander and self.commander.chief or nil) --Ops.Chief#CHIEF + if chief then + self:T(self.lid..string.format("Adding group %s to agents of CHIEF", group:GetName())) + chief.detectionset:AddGroup(asset.flightgroup.group) + end + + elseif string.find(assignment, "Transport-") then + + --- + -- Transport + --- + + local uid=UTILS.Split(assignment, "-")[2] + + -- Get Mission (if any). + local transport=self:GetTransportByID(uid) + + -- Add mission to flightgroup queue. + if transport then + flightgroup:AddOpsTransport(transport) + end + + end + + end + +end + +--- On after "AssetDead" event triggered when an asset group died. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The asset that is dead. +-- @param Functional.Warehouse#WAREHOUSE.Pendingitem request The request of the dead asset. +function LEGION:onafterAssetDead(From, Event, To, asset, request) + + -- Call parent warehouse function first. + self:GetParent(self, LEGION).onafterAssetDead(self, From, Event, To, asset, request) + + -- Remove group from the detection set of the CHIEF (INTEL). + if self.commander and self.commander.chief then + self.commander.chief.detectionset:RemoveGroupsByName({asset.spawngroupname}) + end + + -- Remove asset from mission is done via Mission:AssetDead() call from flightgroup onafterFlightDead function + -- Remove asset from squadron same +end + +--- On after "Destroyed" event. Remove assets from cohorts. Stop cohorts. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function LEGION:onafterDestroyed(From, Event, To) + + -- Debug message. + self:T(self.lid.."Legion warehouse destroyed!") + + -- Cancel all missions. + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + mission:Cancel() + end + + -- Remove all cohort assets. + for _,_cohort in pairs(self.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + -- Stop Cohort. This also removes all assets. + cohort:Stop() + end + + -- Call parent warehouse function first. + self:GetParent(self, LEGION).onafterDestroyed(self, From, Event, To) + +end + + +--- On after "Request" event. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Functional.Warehouse#WAREHOUSE.Queueitem Request Information table of the request. +function LEGION:onafterRequest(From, Event, To, Request) + + if Request.toself then + + -- Assets + local assets=Request.cargoassets + + -- Get Mission + local Mission=self:GetMissionByID(Request.assignment) + + if Mission and assets then + + for _,_asset in pairs(assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + -- This would be the place to modify the asset table before the asset is spawned. + end + + end + + end + + -- Call parent warehouse function after assets have been adjusted. + self:GetParent(self, LEGION).onafterRequest(self, From, Event, To, Request) + +end + +--- On after "SelfRequest" event. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Set#SET_GROUP groupset The set of asset groups that was delivered to the warehouse itself. +-- @param Functional.Warehouse#WAREHOUSE.Pendingitem request Pending self request. +function LEGION:onafterSelfRequest(From, Event, To, groupset, request) + + -- Call parent warehouse function first. + self:GetParent(self, LEGION).onafterSelfRequest(self, From, Event, To, groupset, request) + + -- Get Mission + local mission=self:GetMissionByID(request.assignment) + + for _,_asset in pairs(request.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + end + + for _,_group in pairs(groupset:GetSet()) do + local group=_group --Wrapper.Group#GROUP + end + +end + +--- On after "RequestSpawned" event. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Functional.Warehouse#WAREHOUSE.Pendingitem Request Information table of the request. +-- @param Core.Set#SET_GROUP CargoGroupSet Set of cargo groups. +-- @param Core.Set#SET_GROUP TransportGroupSet Set of transport groups if any. +function LEGION:onafterRequestSpawned(From, Event, To, Request, CargoGroupSet, TransportGroupSet) + + -- Call parent warehouse function. + self:GetParent(self, LEGION).onafterRequestSpawned(self, From, Event, To, Request, CargoGroupSet, TransportGroupSet) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Mission Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new flight group after an asset was spawned. +-- @param #LEGION self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The asset. +-- @return Ops.FlightGroup#FLIGHTGROUP The created flightgroup object. +function LEGION:_CreateFlightGroup(asset) + + -- Create flightgroup. + local opsgroup=nil --Ops.OpsGroup#OPSGROUP + + if self:IsAirwing() then + + --- + -- FLIGHTGROUP + --- + + opsgroup=FLIGHTGROUP:New(asset.spawngroupname) + + elseif self:IsBrigade() then + + --- + -- ARMYGROUP + --- + + opsgroup=ARMYGROUP:New(asset.spawngroupname) + + elseif self:IsFleet() then + + --- + -- NAVYGROUP + --- + + opsgroup=NAVYGROUP:New(asset.spawngroupname) + + else + self:E(self.lid.."ERROR: not airwing or brigade!") + end + + -- Set legion. + opsgroup:_SetLegion(self) + + -- Set cohort. + opsgroup.cohort=self:_GetCohortOfAsset(asset) + + -- Set home base. + opsgroup.homebase=self.airbase + + -- Set home zone. + opsgroup.homezone=self.spawnzone + + -- Set weapon data. + if opsgroup.cohort.weaponData then + local text="Weapon data for group:" + opsgroup.weaponData=opsgroup.weaponData or {} + for bittype,_weapondata in pairs(opsgroup.cohort.weaponData) do + local weapondata=_weapondata --Ops.OpsGroup#OPSGROUP.WeaponData + opsgroup.weaponData[bittype]=UTILS.DeepCopy(weapondata) -- Careful with the units. + text=text..string.format("\n- Bit=%s: Rmin=%.1f km, Rmax=%.1f km", bittype, weapondata.RangeMin/1000, weapondata.RangeMax/1000) + end + self:T3(self.lid..text) + end + + return opsgroup +end + + +--- Check if an asset is currently on a mission (STARTED or EXECUTING). +-- @param #LEGION self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The asset. +-- @param #table MissionTypes Types on mission to be checked. Default all. +-- @return #boolean If true, asset has at least one mission of that type in the queue. +function LEGION:IsAssetOnMission(asset, MissionTypes) + + if MissionTypes then + if type(MissionTypes)~="table" then + MissionTypes={MissionTypes} + end + else + -- Check all possible types. + MissionTypes=AUFTRAG.Type + end + + if asset.flightgroup and asset.flightgroup:IsAlive() then + + -- Loop over mission queue. + for _,_mission in pairs(asset.flightgroup.missionqueue or {}) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission:IsNotOver() then + + -- Get flight status. + local status=mission:GetGroupStatus(asset.flightgroup) + + -- Only if mission is started or executing. + if (status==AUFTRAG.GroupStatus.STARTED or status==AUFTRAG.GroupStatus.EXECUTING) and AUFTRAG.CheckMissionType(mission.type, MissionTypes) then + return true + end + + end + + end + + end + + return false +end + +--- Get the current mission of the asset. +-- @param #LEGION self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The asset. +-- @return Ops.Auftrag#AUFTRAG Current mission or *nil*. +function LEGION:GetAssetCurrentMission(asset) + + if asset.flightgroup then + return asset.flightgroup:GetMissionCurrent() + end + + return nil +end + +--- Count payloads in stock. +-- @param #LEGION self +-- @param #table MissionTypes Types on mission to be checked. Default *all* possible types `AUFTRAG.Type`. +-- @param #table UnitTypes Types of units. +-- @param #table Payloads Specific payloads to be counted only. +-- @return #number Count of available payloads in stock. +function LEGION:CountPayloadsInStock(MissionTypes, UnitTypes, Payloads) + + if MissionTypes then + if type(MissionTypes)=="string" then + MissionTypes={MissionTypes} + end + end + + if UnitTypes then + if type(UnitTypes)=="string" then + UnitTypes={UnitTypes} + end + end + + local function _checkUnitTypes(payload) + if UnitTypes then + for _,unittype in pairs(UnitTypes) do + if unittype==payload.aircrafttype then + return true + end + end + else + -- Unit type was not specified. + return true + end + return false + end + + local function _checkPayloads(payload) + if Payloads then + for _,Payload in pairs(Payloads) do + if Payload.uid==payload.uid then + return true + end + end + else + -- Payload was not specified. + return nil + end + return false + end + + local n=0 + for _,_payload in pairs(self.payloads or {}) do + local payload=_payload --Ops.Airwing#AIRWING.Payload + + for _,MissionType in pairs(MissionTypes) do + + local specialpayload=_checkPayloads(payload) + local compatible=AUFTRAG.CheckMissionCapability(MissionType, payload.capabilities) + + local goforit = specialpayload or (specialpayload==nil and compatible) + + if goforit and _checkUnitTypes(payload) then + + if payload.unlimited then + -- Payload is unlimited. Return a BIG number. + return 999 + else + n=n+payload.navail + end + + end + + end + end + + return n +end + +--- Count missions in mission queue. +-- @param #LEGION self +-- @param #table MissionTypes Types on mission to be checked. Default *all* possible types `AUFTRAG.Type`. +-- @return #number Number of missions that are not over yet. +function LEGION:CountMissionsInQueue(MissionTypes) + + MissionTypes=MissionTypes or AUFTRAG.Type + + local N=0 + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + -- Check if this mission type is requested. + if mission:IsNotOver() and AUFTRAG.CheckMissionType(mission.type, MissionTypes) then N=N+1 end + end return N end ---- Get assets for a mission. --- @param #SQUADRON self --- @param Ops.Auftrag#AUFTRAG Mission The mission. --- @param #number Nplayloads Number of payloads available. --- @return #table Assets that can do the required mission. -function SQUADRON:RecruitAssets(Mission, Npayloads) +--- Count total number of assets of the legion. +-- @param #LEGION self +-- @param #boolean InStock If true, only assets that are in the warehouse stock/inventory are counted. +-- @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 #number Amount of asset groups in stock. +function LEGION:CountAssets(InStock, MissionTypes, Attributes) - -- Number of payloads available. - Npayloads=Npayloads or self.airwing:CountPayloadsInStock(Mission.type, self.aircrafttype, Mission.payloads) + local N=0 + + for _,_cohort in pairs(self.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + N=N+cohort:CountAssets(InStock, MissionTypes, Attributes) + end + + return N +end + +--- Get OPSGROUPs that are spawned and alive. +-- @param #LEGION self +-- @param #table MissionTypes (Optional) Get only assest that can perform certain mission type(s). Default is all types. +-- @param #table Attributes (Optional) Get only assest that have a certain attribute(s), e.g. `WAREHOUSE.Attribute.AIR_BOMBER`. +-- @return Core.Set#SET_OPSGROUP The set of OPSGROUPs. Can be empty if no groups are spawned or alive! +function LEGION:GetOpsGroups(MissionTypes, Attributes) + + local setLegion=SET_OPSGROUP:New() + + for _,_cohort in pairs(self.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + -- Get cohort set. + local setCohort=cohort:GetOpsGroups(MissionTypes, Attributes) + + -- Debug info. + self:T2(self.lid..string.format("Found %d opsgroups of cohort %s", setCohort:Count(), cohort.name)) + + -- Add to legion set. + setLegion:AddSet(setCohort) + end + + return setLegion +end + +--- Count total number of assets in LEGION warehouse stock that also have a payload. +-- @param #LEGION self +-- @param #boolean Payloads (Optional) Specifc payloads to consider. Default all. +-- @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 #number Amount of asset groups in stock. +function LEGION:CountAssetsWithPayloadsInStock(Payloads, MissionTypes, Attributes) + + -- Total number counted. + local N=0 + + -- Number of payloads in stock per aircraft type. + local Npayloads={} + + -- First get payloads for aircraft types of squadrons. + for _,_cohort in pairs(self.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + if Npayloads[cohort.aircrafttype]==nil then + Npayloads[cohort.aircrafttype]=self:CountPayloadsInStock(MissionTypes, cohort.aircrafttype, Payloads) + self:T3(self.lid..string.format("Got Npayloads=%d for type=%s",Npayloads[cohort.aircrafttype], cohort.aircrafttype)) + end + end + + for _,_cohort in pairs(self.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + -- Number of assets in stock. + local n=cohort:CountAssets(true, MissionTypes, Attributes) + + -- Number of payloads. + local p=Npayloads[cohort.aircrafttype] or 0 + + -- Only the smaller number of assets or paylods is really available. + local m=math.min(n, p) + + -- Add up what we have. Could also be zero. + N=N+m + + -- Reduce number of available payloads. + Npayloads[cohort.aircrafttype]=Npayloads[cohort.aircrafttype]-m + end + + return N +end + +--- Count assets on mission. +-- @param #LEGION self +-- @param #table MissionTypes Types on mission to be checked. Default all. +-- @param Ops.Cohort#COHORT Cohort Only count assets of this cohort. Default count assets of all cohorts. +-- @return #number Number of pending and queued assets. +-- @return #number Number of pending assets. +-- @return #number Number of queued assets. +function LEGION:CountAssetsOnMission(MissionTypes, Cohort) + + local Nq=0 + local Np=0 + + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + -- Check if this mission type is requested. + if AUFTRAG.CheckMissionType(mission.type, MissionTypes or AUFTRAG.Type) then + + for _,_asset in pairs(mission.assets or {}) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Ensure asset belongs to this letion. + if asset.wid==self.uid then + + if Cohort==nil or Cohort.name==asset.squadname then + + local request, isqueued=self:GetRequestByID(mission.requestID[self.alias]) + + if isqueued then + Nq=Nq+1 + else + Np=Np+1 + end + + end + + end + end + end + end + + return Np+Nq, Np, Nq +end + +--- Count assets on mission. +-- @param #LEGION self +-- @param #table MissionTypes Types on mission to be checked. Default all. +-- @return #table Assets on pending requests. +function LEGION:GetAssetsOnMission(MissionTypes) local assets={} + local Np=0 - -- Loop over assets. - for _,_asset in pairs(self.assets) do - local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset - - - -- Check if asset is currently on a mission (STARTED or QUEUED). - if self.airwing:IsAssetOnMission(asset) then + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG - --- - -- Asset is already on a mission. - --- + -- Check if this mission type is requested. + if AUFTRAG.CheckMissionType(mission.type, MissionTypes) then - -- Check if this asset is currently on a GCICAP mission (STARTED or EXECUTING). - if self.airwing:IsAssetOnMission(asset, AUFTRAG.Type.GCICAP) and Mission.type==AUFTRAG.Type.INTERCEPT then - - -- Check if the payload of this asset is compatible with the mission. - -- Note: we do not check the payload as an asset that is on a GCICAP mission should be able to do an INTERCEPT as well! - self:I(self.lid.."Adding asset on GCICAP mission for an INTERCEPT mission") - table.insert(assets, asset) + for _,_asset in pairs(mission.assets or {}) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem - end - - else - - --- - -- Asset as NO current mission - --- + -- Ensure asset belongs to this legion. + if asset.wid==self.uid then - if asset.spawned then - - --- - -- Asset is already SPAWNED (could be uncontrolled on the airfield or inbound after another mission) - --- - - local flightgroup=asset.flightgroup - - -- Firstly, check if it has the right payload. - if self:CheckMissionCapability(Mission.type, asset.payload.capabilities) and flightgroup and flightgroup:IsAlive() then - - -- Assume we are ready and check if any condition tells us we are not. - local combatready=true - - if Mission.type==AUFTRAG.Type.INTERCEPT then - combatready=flightgroup:CanAirToAir() - else - local excludeguns=Mission.type==AUFTRAG.Type.BOMBING or Mission.type==AUFTRAG.Type.BOMBRUNWAY or Mission.type==AUFTRAG.Type.BOMBCARPET or Mission.type==AUFTRAG.Type.SEAD or Mission.type==AUFTRAG.Type.ANTISHIP - combatready=flightgroup:CanAirToGround(excludeguns) - end - - -- No more attacks if fuel is already low. Safety first! - if flightgroup:IsFuelLow() then - combatready=false - end - - -- Check if in a state where we really do not want to fight any more. - if flightgroup:IsHolding() or flightgroup:IsLanding() or flightgroup:IsLanded() or flightgroup:IsArrived() or flightgroup:IsDead() or flightgroup:IsStopped() then - combatready=false - end - - -- This asset is "combatready". - if combatready then - self:I(self.lid.."Adding SPAWNED asset to ANOTHER mission as it is COMBATREADY") - table.insert(assets, asset) - end - - end - - else - - --- - -- Asset is still in STOCK - --- - - -- Check that asset is not already requested for another mission. - if Npayloads>0 and self:IsRepaired(asset) and (not asset.requested) then - - -- Add this asset to the selection. table.insert(assets, asset) - -- Reduce number of payloads so we only return the number of assets that could do the job. - Npayloads=Npayloads-1 - end - - end - end - end -- loop over assets + + end + end + end return assets end +--- Get the unit types of this legion. These are the unit types of all assigned cohorts. +-- @param #LEGION self +-- @param #boolean onlyactive Count only the active ones. +-- @param #table cohorts Table of cohorts. Default all. +-- @return #table Table of unit types. +function LEGION:GetAircraftTypes(onlyactive, cohorts) ---- Get the time an asset needs to be repaired. --- @param #SQUADRON self --- @param Ops.AirWing#AIRWING.SquadronAsset Asset The asset. --- @return #number Time in seconds until asset is repaired. -function SQUADRON:GetRepairTime(Asset) + -- Get all unit types that can do the job. + local unittypes={} - if Asset.Treturned then - - local t=self.maintenancetime - t=t+Asset.damage*self.repairtime - - -- Seconds after returned. - local dt=timer.getAbsTime()-Asset.Treturned - - local T=t-dt - - return T - else - return 0 + -- Loop over all cohorts. + for _,_cohort in pairs(cohorts or self.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + if (not onlyactive) or cohort:IsOnDuty() then + + local gotit=false + for _,unittype in pairs(unittypes) do + if cohort.aircrafttype==unittype then + gotit=true + break + end + end + if not gotit then + table.insert(unittypes, cohort.aircrafttype) + end + + end end + return unittypes +end + +--- Count payloads of all cohorts for all unit types. +-- @param #LEGION self +-- @param #string MissionType Mission type. +-- @param #table Cohorts Cohorts included. +-- @param #table Payloads (Optional) Special payloads. +-- @return #table Table of payloads for each unit type. +function LEGION:_CountPayloads(MissionType, Cohorts, Payloads) + + -- Number of payloads in stock per aircraft type. + local Npayloads={} + + -- First get payloads for aircraft types of squadrons. + for _,_cohort in pairs(Cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + -- We only need that element once. + if Npayloads[cohort.aircrafttype]==nil then + + -- Count number of payloads in stock for the cohort aircraft type. + Npayloads[cohort.aircrafttype]=cohort.legion:IsAirwing() and self:CountPayloadsInStock(MissionType, cohort.aircrafttype, Payloads) or 999 + + -- Debug info. + self:T2(self.lid..string.format("Got N=%d payloads for mission type=%s and unit type=%s", Npayloads[cohort.aircrafttype], MissionType, cohort.aircrafttype)) + end + end + + return Npayloads +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Recruiting Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Recruit assets for a given mission. +-- @param #LEGION self +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @return #boolean If `true` enough assets could be recruited. +-- @return #table Recruited assets. +-- @return #table Legions of recruited assets. +function LEGION:RecruitAssetsForMission(Mission) + + -- Get required assets. + local NreqMin, NreqMax=Mission:GetRequiredAssets() + + -- Target position vector. + local TargetVec2=Mission:GetTargetVec2() + + -- Payloads. + local Payloads=Mission.payloads + + -- Get special escort legions and/or 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 + + -- No escort cohorts/legions given ==> take own cohorts. + if #Cohorts==0 then + Cohorts=self.cohorts + end + + -- Recuit assets. + local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, Mission.type, Mission.alert5MissionType, NreqMin, NreqMax, TargetVec2, Payloads, + Mission.engageRange, Mission.refuelSystem, nil, nil, nil, Mission.attributes, Mission.properties, {Mission.engageWeaponType}) + + return recruited, assets, legions +end + +--- Recruit assets for a given OPS transport. +-- @param #LEGION self +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The OPS transport. +-- @return #boolean If `true`, enough assets could be recruited. +-- @return #table assets Recruited assets. +-- @return #table legions Legions of recruited assets. +function LEGION:RecruitAssetsForTransport(Transport) + + -- Get all undelivered cargo ops groups. + local cargoOpsGroups=Transport:GetCargoOpsGroups(false) + + local weightGroup=0 + local TotalWeight=nil + + -- At least one group should be spawned. + if #cargoOpsGroups>0 then + + -- Calculate the max weight so we know which cohorts can provide carriers. + TotalWeight=0 + for _,_opsgroup in pairs(cargoOpsGroups) do + local opsgroup=_opsgroup --Ops.OpsGroup#OPSGROUP + local weight=opsgroup:GetWeightTotal() + if weight>weightGroup then + weightGroup=weight + end + TotalWeight=TotalWeight+weight + end + else + -- No cargo groups! + return false + end + + + -- TODO: Special transport cohorts/legions. + + -- Target is the deploy zone. + local TargetVec2=Transport:GetDeployZone():GetVec2() + + -- Number of required carriers. + local NreqMin,NreqMax=Transport:GetRequiredCarriers() + + + -- Recruit assets and legions. + local recruited, assets, legions=LEGION.RecruitCohortAssets(self.cohorts, AUFTRAG.Type.OPSTRANSPORT, nil, NreqMin, NreqMax, TargetVec2, nil, nil, nil, weightGroup, TotalWeight) + + return recruited, assets, legions +end + +--- Recruit assets performing an escort mission for a given asset. +-- @param #LEGION self +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @param #table Assets Table of assets. +-- @return #boolean If `true`, enough assets could be recruited or no escort was required in the first place. +function LEGION:RecruitAssetsForEscort(Mission, Assets) + + -- Is an escort requested in the first place? + if Mission.NescortMin and Mission.NescortMax and (Mission.NescortMin>0 or Mission.NescortMax>0) then + + -- Debug info. + self:T(self.lid..string.format("Requested escort for mission %s [%s]. Required assets=%d-%d", Mission:GetName(), Mission:GetType(), Mission.NescortMin,Mission.NescortMax)) + + -- Get special escort legions and/or 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 escort cohorts/legions given ==> take own cohorts. + if #Cohorts==0 then + Cohorts=self.cohorts + end + + -- Call LEGION function but provide COMMANDER as self. + local assigned=LEGION.AssignAssetsForEscort(self, Cohorts, Assets, Mission.NescortMin, Mission.NescortMax, Mission.escortMissionType, Mission.escortTargetTypes) + + return assigned + end + + return true +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Recruiting and Optimization Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Recruit assets from Cohorts for the given parameters. **NOTE** that we set the `asset.isReserved=true` flag so it cant be recruited by anyone else. +-- @param #table Cohorts Cohorts included. +-- @param #string MissionTypeRecruit Mission type for recruiting the cohort assets. +-- @param #string MissionTypeOpt Mission type for which the assets are optimized. Default is the same as `MissionTypeRecruit`. +-- @param #number NreqMin Minimum number of required assets. +-- @param #number NreqMax Maximum number of required assets. +-- @param DCS#Vec2 TargetVec2 Target position as 2D vector. +-- @param #table Payloads Special payloads. +-- @param #number RangeMax Max range in meters. +-- @param #number RefuelSystem Refuelsystem. +-- @param #number CargoWeight Cargo weight for recruiting transport carriers. +-- @param #number TotalWeight Total cargo weight in kg. +-- @param #table Categories Group categories. +-- @param #table Attributes Group attributes. See `GROUP.Attribute.` +-- @param #table Properties DCS attributes. +-- @param #table WeaponTypes Bit of weapon types. +-- @return #boolean If `true` enough assets could be recruited. +-- @return #table Recruited assets. **NOTE** that we set the `asset.isReserved=true` flag so it cant be recruited by anyone else. +-- @return #table Legions of recruited assets. +function LEGION.RecruitCohortAssets(Cohorts, MissionTypeRecruit, MissionTypeOpt, NreqMin, NreqMax, TargetVec2, Payloads, RangeMax, RefuelSystem, CargoWeight, TotalWeight, Categories, Attributes, Properties, WeaponTypes) + + -- The recruited assets. + local Assets={} + + -- Legions of recruited assets. + local Legions={} + + -- Set MissionTypeOpt to Recruit if nil. + if MissionTypeOpt==nil then + MissionTypeOpt=MissionTypeRecruit + end + + --- Function to check category. + local function CheckCategory(_cohort) + local cohort=_cohort --Ops.Cohort#COHORT + if Categories and #Categories>0 then + for _,category in pairs(Categories) do + if category==cohort.category then + return true + end + end + else + return true + end + end + + --- Function to check attribute. + local function CheckAttribute(_cohort) + local cohort=_cohort --Ops.Cohort#COHORT + if Attributes and #Attributes>0 then + for _,attribute in pairs(Attributes) do + if attribute==cohort.attribute then + return true + end + end + else + return true + end + end + + --- Function to check property. + local function CheckProperty(_cohort) + local cohort=_cohort --Ops.Cohort#COHORT + if Properties and #Properties>0 then + for _,Property in pairs(Properties) do + for property,value in pairs(cohort.properties) do + if Property==property then + return true + end + end + end + else + return true + end + end + + --- Function to check weapon type. + local function CheckWeapon(_cohort) + local cohort=_cohort --Ops.Cohort#COHORT + if WeaponTypes and #WeaponTypes>0 then + for _,WeaponType in pairs(WeaponTypes) do + if WeaponType==ENUMS.WeaponFlag.Auto then + return true + else + for _,_weaponData in pairs(cohort.weaponData or {}) do + local weaponData=_weaponData --Ops.OpsGroup#OPSGROUP.WeaponData + if weaponData.BitType==WeaponType then + return true + end + end + end + end + return false + else + return true + end + end + + -- Loops over cohorts. + for _,_cohort in pairs(Cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + -- Distance to target. + local TargetDistance=TargetVec2 and UTILS.VecDist2D(TargetVec2, cohort.legion:GetVec2()) or 0 + + -- Is in range? + local Rmax=cohort:GetMissionRange(WeaponTypes) + local InRange=(RangeMax and math.max(RangeMax, Rmax) or Rmax) >= TargetDistance + + -- Has the requested refuelsystem? + local Refuel=RefuelSystem~=nil and (RefuelSystem==cohort.tankerSystem) or true + + -- STRANGE: Why did the above line did not give the same result?! Above Refuel is always true! + local Refuel=true + if RefuelSystem then + if cohort.tankerSystem then + Refuel=RefuelSystem==cohort.tankerSystem + else + Refuel=false + end + end + + -- Is capable of the mission type? + local Capable=AUFTRAG.CheckMissionCapability({MissionTypeRecruit}, cohort.missiontypes) + + -- Can carry the cargo? + local CanCarry=CargoWeight and cohort.cargobayLimit>=CargoWeight or true + + -- Right category. + local RightCategory=CheckCategory(cohort) + + -- Right attribute. + local RightAttribute=CheckAttribute(cohort) + + -- Right property (DCS attribute). + local RightProperty=CheckProperty(cohort) + + -- Right weapon type. + local RightWeapon=CheckWeapon(cohort) + + -- Cohort ready to execute mission. + local Ready=cohort:IsOnDuty() + if MissionTypeRecruit==AUFTRAG.Type.RELOCATECOHORT then + Ready=cohort:IsRelocating() + Capable=true + end + + -- Debug info. + 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). + if Ready and Capable and InRange and Refuel and CanCarry and RightCategory and RightAttribute and RightProperty and RightWeapon then + + -- Recruit assets from cohort. + local assets, npayloads=cohort:RecruitAssets(MissionTypeRecruit, 999) + + -- Add assets to the list. + for _,asset in pairs(assets) do + table.insert(Assets, asset) + end + + end + + end + + -- Now we have a long list with assets. + LEGION._OptimizeAssetSelection(Assets, MissionTypeOpt, TargetVec2, false) + + + -- Get payloads for air assets. + for _,_asset in pairs(Assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Only assets that have no payload. Should be only spawned assets! + if asset.legion:IsAirwing() and not asset.payload then + + -- Fetch payload for asset. This can be nil! + asset.payload=asset.legion:FetchPayloadFromStock(asset.unittype, MissionTypeOpt, Payloads) + + end + end + + -- Remove assets that dont have a payload. + for i=#Assets,1,-1 do + local asset=Assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + if asset.legion:IsAirwing() and not asset.payload then + table.remove(Assets, i) + end + end + + -- Now find the best asset for the given payloads. + LEGION._OptimizeAssetSelection(Assets, MissionTypeOpt, TargetVec2, true) + + -- Number of assets. At most NreqMax. + local Nassets=math.min(#Assets, NreqMax) + + if #Assets>=NreqMin then + + --- + -- Found enough assets + --- + + -- Add assets to mission. + local cargobay=0 + for i=1,Nassets do + local asset=Assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + + asset.isReserved=true + + Legions[asset.legion.alias]=asset.legion + + if TotalWeight then + + -- Number of + local N=math.floor(asset.cargobaytot/asset.nunits / CargoWeight)*asset.nunits + --env.info(string.format("cargobaytot=%d, cargoweight=%d ==> N=%d", asset.cargobaytot, CargoWeight, N)) + + cargobay=cargobay + N*CargoWeight + + if cargobay>=TotalWeight then + --env.info(string.format("FF found enough assets to transport all cargo! N=%d [%d], cargobay=%.1f >= %.1f kg total weight", i, Nassets, cargobay, TotalWeight)) + Nassets=i + break + end + + end + + end + + -- Return payloads of not needed assets. + for i=#Assets,Nassets+1,-1 do + local asset=Assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + if asset.legion:IsAirwing() and not asset.spawned then + asset.legion:T2(asset.legion.lid..string.format("Returning payload from asset %s", asset.spawngroupname)) + asset.legion:ReturnPayloadFromAsset(asset) + end + table.remove(Assets, i) + end + + -- Found enough assets. + return true, Assets, Legions + else + + --- + -- NOT enough assets + --- + + -- Return payloads of assets. + for i=1,#Assets do + local asset=Assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + if asset.legion:IsAirwing() and not asset.spawned then + asset.legion:T2(asset.legion.lid..string.format("Returning payload from asset %s", asset.spawngroupname)) + asset.legion:ReturnPayloadFromAsset(asset) + end + end + + -- Not enough assets found. + return false, {}, {} + end + + return false, {}, {} +end + +--- Unrecruit assets. Set `isReserved` to false, return payload to airwing and (optionally) remove from assigned mission. +-- @param #table Assets List of assets. +-- @param Ops.Auftrag#AUFTRAG Mission (Optional) The mission from which the assets will be deleted. +function LEGION.UnRecruitAssets(Assets, Mission) + + -- Return payloads of assets. + for i=1,#Assets do + local asset=Assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + -- Not reserved any more. + asset.isReserved=false + -- Return payload. + if asset.legion:IsAirwing() and not asset.spawned then + asset.legion:T2(asset.legion.lid..string.format("Returning payload from asset %s", asset.spawngroupname)) + asset.legion:ReturnPayloadFromAsset(asset) + end + -- Remove from mission. + if Mission then + Mission:DelAsset(asset) + end + end + end ---- Checks if a mission type is contained in a table of possible types. --- @param #SQUADRON self --- @param Ops.AirWing#AIRWING.SquadronAsset Asset The asset. --- @return #boolean If true, the requested mission type is part of the possible mission types. -function SQUADRON:IsRepaired(Asset) - if Asset.Treturned then - local Tnow=timer.getAbsTime() - local Trepaired=Asset.Treturned+self.maintenancetime - if Tnow>=Trepaired then - return true +--- Recruit and assign assets performing an escort mission for a given asset list. Note that each asset gets an escort. +-- @param #LEGION self +-- @param #table Cohorts Cohorts for escorting assets. +-- @param #table Assets Table of assets to be escorted. +-- @param #number NescortMin Min number of escort groups required per escorted asset. +-- @param #number NescortMax Max number of escort groups required per escorted asset. +-- @param #string MissionType Mission type. +-- @param #string TargetTypes Types of targets that are engaged. +-- @param #number EngageRange EngageRange in Nautical Miles. +-- @return #boolean If `true`, enough assets could be recruited or no escort was required in the first place. +function LEGION:AssignAssetsForEscort(Cohorts, Assets, NescortMin, NescortMax, MissionType, TargetTypes, EngageRange) + + -- Is an escort requested in the first place? + if NescortMin and NescortMax and (NescortMin>0 or NescortMax>0) then + + -- Debug info. + self:T(self.lid..string.format("Requested escort for %d assets from %d cohorts. Required escort assets=%d-%d", #Assets, #Cohorts, NescortMin, NescortMax)) + + -- Escorts for each asset. + local Escorts={} + + local EscortAvail=true + for _,_asset in pairs(Assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Target vector is the legion of the asset. + local TargetVec2=asset.legion:GetVec2() + + -- We want airplanes for airplanes and helos for everything else. + local Categories={Group.Category.HELICOPTER} + local targetTypes={"Ground Units"} + if asset.category==Group.Category.AIRPLANE then + Categories={Group.Category.AIRPLANE} + targetTypes={"Air"} + end + + TargetTypes=TargetTypes or targetTypes + + -- Recruit escort asset for the mission asset. + local Erecruited, eassets, elegions=LEGION.RecruitCohortAssets(Cohorts, AUFTRAG.Type.ESCORT, MissionType, NescortMin, NescortMax, TargetVec2, nil, nil, nil, nil, nil, Categories) + + if Erecruited then + Escorts[asset.spawngroupname]={EscortLegions=elegions, EscortAssets=eassets, ecategory=asset.category} + else + -- Could not find escort for this asset ==> Escort not possible ==> Break the loop. + EscortAvail=false + break + end + end + + -- ALL escorts could be recruited. + if EscortAvail then + + local N=0 + for groupname,value in pairs(Escorts) do + + local Elegions=value.EscortLegions + local Eassets=value.EscortAssets + local ecategory=value.ecategory + + for _,_legion in pairs(Elegions) do + local legion=_legion --Ops.Legion#LEGION + + local OffsetVector=nil --DCS#Vec3 + if ecategory==Group.Category.GROUND then + -- Overhead at 1000 ft. + OffsetVector={} + OffsetVector.x=0 + OffsetVector.y=UTILS.FeetToMeters(1000) + OffsetVector.z=0 + elseif MissionType==AUFTRAG.Type.SEAD then + -- Overhead slightly higher and right. + OffsetVector={} + OffsetVector.x=-100 + OffsetVector.y= 500 + OffsetVector.z= 500 + end + + -- Create and ESCORT mission for this asset. + local escort=AUFTRAG:NewESCORT(groupname, OffsetVector, EngageRange, TargetTypes) + + -- For a SEAD mission, we also adjust the mission task. + if MissionType==AUFTRAG.Type.SEAD then + escort.missionTask=ENUMS.MissionTask.SEAD + end + + -- Reserve assts and add to mission. + for _,_asset in pairs(Eassets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + escort:AddAsset(asset) + N=N+1 + end + + -- Assign mission to legion. + self:MissionAssign(escort, {legion}) + end + end + + -- Debug info. + self:T(self.lid..string.format("Recruited %d escort assets", N)) + + -- Yup! + return true else - return false + + -- Debug info. + self:T(self.lid..string.format("Could not get at least one escort!")) + + -- Could not get at least one escort. Unrecruit all recruited ones. + for groupname,value in pairs(Escorts) do + local Eassets=value.EscortAssets + LEGION.UnRecruitAssets(Eassets) + end + + -- No,no! + return false + end + + else + -- No escort required. + self:T(self.lid..string.format("No escort required! NescortMin=%s, NescortMax=%s", tostring(NescortMin), tostring(NescortMax))) + return true + end + +end + +--- Recruit and assign assets performing an OPSTRANSPORT for a given asset list. +-- @param #LEGION self +-- @param #table Legions Transport legions. +-- @param #table CargoAssets Weight of the heaviest cargo group to be transported. +-- @param #number NcarriersMin Min number of carrier assets. +-- @param #number NcarriersMax Max number of carrier assets. +-- @param Core.Zone#ZONE DeployZone Deploy zone. +-- @param Core.Zone#ZONE DisembarkZone (Optional) Disembark zone. +-- @return #boolean If `true`, enough assets could be recruited and an OPSTRANSPORT object was created. +-- @return Ops.OpsTransport#OPSTRANSPORT Transport The transport. +function LEGION:AssignAssetsForTransport(Legions, CargoAssets, NcarriersMin, NcarriersMax, DeployZone, DisembarkZone, Categories, Attributes) + + -- Is an escort requested in the first place? + if NcarriersMin and NcarriersMax and (NcarriersMin>0 or NcarriersMax>0) then + + -- Cohorts. + local Cohorts={} + for _,_legion in pairs(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 + + -- Get all legions and heaviest cargo group weight + local CargoLegions={} ; local CargoWeight=nil ; local TotalWeight=0 + for _,_asset in pairs(CargoAssets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + CargoLegions[asset.legion.alias]=asset.legion + if CargoWeight==nil or asset.weight>CargoWeight then + CargoWeight=asset.weight + end + TotalWeight=TotalWeight+asset.weight end - else - return true + -- Target is the deploy zone. + local TargetVec2=DeployZone:GetVec2() + + -- Recruit assets and legions. + local TransportAvail, CarrierAssets, CarrierLegions= + LEGION.RecruitCohortAssets(Cohorts, AUFTRAG.Type.OPSTRANSPORT, nil, NcarriersMin, NcarriersMax, TargetVec2, nil, nil, nil, CargoWeight, TotalWeight, Categories, Attributes) + + if TransportAvail then + + -- Create and OPSTRANSPORT assignment. + local Transport=OPSTRANSPORT:New(nil, nil, DeployZone) + if DisembarkZone then + Transport:SetDisembarkZone(DisembarkZone) + end + + -- Debug info. + self:T(self.lid..string.format("Transport available with %d carrier assets", #CarrierAssets)) + + -- Add cargo assets to transport. + for _,_legion in pairs(CargoLegions) do + local legion=_legion --Ops.Legion#LEGION + + -- Set pickup zone to spawn zone or airbase if the legion has one that is operational. + local pickupzone=legion.spawnzone + + -- Add TZC from legion spawn zone to deploy zone. + local tpz=Transport:AddTransportZoneCombo(nil, pickupzone, Transport:GetDeployZone()) + + -- 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 + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + if asset.legion.alias==legion.alias then + Transport:AddAssetCargo(asset, tpz) + end + end + end + + -- Add carrier assets. + for _,_asset in pairs(CarrierAssets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + Transport:AddAsset(asset) + end + + -- Assign TRANSPORT to legions. This also sends the request for the assets. + self:TransportAssign(Transport, CarrierLegions) + + -- Got transport. + return true, Transport + else + -- Uncrecruit transport assets. + LEGION.UnRecruitAssets(CarrierAssets) + return false, nil + end + + return nil, nil + end + + -- No transport requested in the first place. + return true, nil +end + + +--- Calculate the mission score of an asset. +-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset Asset +-- @param #string MissionType Mission type for which the best assets are desired. +-- @param DCS#Vec2 TargetVec2 Target 2D vector. +-- @param #boolean IncludePayload If `true`, include the payload in the calulation if the asset has one attached. +-- @return #number Mission score. +function LEGION.CalculateAssetMissionScore(asset, MissionType, TargetVec2, IncludePayload) + + -- Mission score. + local score=0 + + -- Prefer highly skilled assets. + if asset.skill==AI.Skill.AVERAGE then + score=score+0 + elseif asset.skill==AI.Skill.GOOD then + score=score+10 + elseif asset.skill==AI.Skill.HIGH then + score=score+20 + elseif asset.skill==AI.Skill.EXCELLENT then + score=score+30 + end + + -- Add mission performance to score. + score=score+asset.cohort:GetMissionPeformance(MissionType) + + -- Add payload performance to score. + local function scorePayload(Payload, MissionType) + for _,Capability in pairs(Payload.capabilities) do + local capability=Capability --Ops.Auftrag#AUFTRAG.Capability + if capability.MissionType==MissionType then + return capability.Performance + end + end + return 0 + end + + if IncludePayload and asset.payload then + score=score+scorePayload(asset.payload, MissionType) + end + + -- Origin: We take the OPSGROUP position or the one of the legion. + local OrigVec2=asset.flightgroup and asset.flightgroup:GetVec2() or asset.legion:GetVec2() + + -- Distance factor. + local distance=0 + if TargetVec2 and OrigVec2 then + -- Distance in NM. + distance=UTILS.MetersToNM(UTILS.VecDist2D(OrigVec2, TargetVec2)) + -- Round: 55 NM ==> 5.5 ==> 6, 63 NM ==> 6.3 ==> 6 + distance=UTILS.Round(distance/10, 0) + end + + -- Reduce score for legions that are futher away. + score=score-distance + + -- Check for spawned assets. + if asset.spawned and asset.flightgroup and asset.flightgroup:IsAlive() then + + -- Get current mission. + local currmission=asset.flightgroup:GetMissionCurrent() + + if currmission then + + if currmission.type==AUFTRAG.Type.ALERT5 and currmission.alert5MissionType==MissionType then + -- 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. 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 + score=score+25 + end + + end + + if MissionType==AUFTRAG.Type.OPSTRANSPORT or MissionType==AUFTRAG.Type.AMMOSUPPLY or MissionType==AUFTRAG.Type.AWACS or MissionType==AUFTRAG.Type.FUELSUPPLY or MissionType==AUFTRAG.Type.TANKER then + -- TODO: need to check for missions that do not require ammo like transport, recon, awacs, tanker etc. + -- We better take a fresh asset. Sometimes spawned assets to something else, which is difficult to check. + score=score-10 + else + -- Combat mission. + if asset.flightgroup:IsOutOfAmmo() then + -- Assets that are out of ammo are not considered. + score=score-1000 + end + end + end + + -- TRANSPORT specific. + if MissionType==AUFTRAG.Type.OPSTRANSPORT then + -- Add 1 score point for each 10 kg of cargo bay. + score=score+UTILS.Round(asset.cargobaymax/10, 0) + end + + -- TODO: This could be vastly improved. Need to gather ideas during testing. + -- Calculate ETA? Assets on orbit missions should arrive faster even if they are further away. + -- Max speed of assets. + -- Fuel amount? + -- Range of assets? + + if asset.legion and asset.legion.verbose>=2 then + asset.legion:I(asset.legion.lid..string.format("Asset %s [spawned=%s] score=%d", asset.spawngroupname, tostring(asset.spawned), score)) + end + + return score +end + +--- Optimize chosen assets for the mission at hand. +-- @param #table assets Table of (unoptimized) assets. +-- @param #string MissionType Mission type. +-- @param DCS#Vec2 TargetVec2 Target position as 2D vector. +-- @param #boolean IncludePayload If `true`, include the payload in the calulation if the asset has one attached. +function LEGION._OptimizeAssetSelection(assets, MissionType, TargetVec2, IncludePayload) + + -- Calculate the mission score of all assets. + for _,_asset in pairs(assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + asset.score=LEGION.CalculateAssetMissionScore(asset, MissionType, TargetVec2, IncludePayload) + end + + --- Sort assets wrt to their mission score. Higher is better. + local function optimize(a, b) + local assetA=a --Functional.Warehouse#WAREHOUSE.Assetitem + local assetB=b --Functional.Warehouse#WAREHOUSE.Assetitem + -- Higher score wins. If equal score ==> closer wins. + return (assetA.score>assetB.score) + end + table.sort(assets, optimize) + + -- Remove distance parameter. + if LEGION.verbose>0 then + local text=string.format("Optimized %d assets for %s mission/transport (payload=%s):", #assets, MissionType, tostring(IncludePayload)) + for i,Asset in pairs(assets) do + local asset=Asset --Functional.Warehouse#WAREHOUSE.Assetitem + text=text..string.format("\n%s %s: score=%d", asset.squadname, asset.spawngroupname, asset.score) + asset.score=nil + end + env.info(text) end end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Checks if a mission type is contained in a table of possible types. --- @param #SQUADRON self --- @param #string MissionType The requested mission type. --- @param #table PossibleTypes A table with possible mission types. --- @return #boolean If true, the requested mission type is part of the possible mission types. -function SQUADRON:CheckMissionType(MissionType, PossibleTypes) +--- Returns the mission for a given mission ID (Autragsnummer). +-- @param #LEGION self +-- @param #number mid Mission ID (Auftragsnummer). +-- @return Ops.Auftrag#AUFTRAG Mission table. +function LEGION:GetMissionByID(mid) + + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission.auftragsnummer==tonumber(mid) then + return mission + end - if type(PossibleTypes)=="string" then - PossibleTypes={PossibleTypes} end - for _,canmission in pairs(PossibleTypes) do - if canmission==MissionType then - return true - end - end - - return false + return nil end ---- Check if a mission type is contained in a list of possible capabilities. --- @param #SQUADRON self --- @param #string MissionType The requested mission type. --- @param #table Capabilities A table with possible capabilities. --- @return #boolean If true, the requested mission type is part of the possible mission types. -function SQUADRON:CheckMissionCapability(MissionType, Capabilities) +--- Returns the mission for a given ID. +-- @param #LEGION self +-- @param #number uid Transport UID. +-- @return Ops.OpsTransport#OPSTRANSPORT Transport assignment. +function LEGION:GetTransportByID(uid) + + for _,_transport in pairs(self.transportqueue) do + local transport=_transport --Ops.OpsTransport#OPSTRANSPORT + + if transport.uid==tonumber(uid) then + return transport + end - for _,cap in pairs(Capabilities) do - local capability=cap --Ops.Auftrag#AUFTRAG.Capability - if capability.MissionType==MissionType then - return true - end end - return false + return nil +end + +--- Returns the mission for a given request ID. +-- @param #LEGION self +-- @param #number RequestID Unique ID of the request. +-- @return Ops.Auftrag#AUFTRAG Mission table or *nil*. +function LEGION:GetMissionFromRequestID(RequestID) + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + local mid=mission.requestID[self.alias] + if mid and mid==RequestID then + return mission + end + end + return nil +end + +--- Returns the mission for a given request. +-- @param #LEGION self +-- @param Functional.Warehouse#WAREHOUSE.Queueitem Request The warehouse request. +-- @return Ops.Auftrag#AUFTRAG Mission table or *nil*. +function LEGION:GetMissionFromRequest(Request) + return self:GetMissionFromRequestID(Request.uid) +end + +--- Fetch a payload from the airwing resources for a given unit and mission type. +-- The payload with the highest priority is preferred. +-- @param #LEGION self +-- @param #string UnitType The type of the unit. +-- @param #string MissionType The mission type. +-- @param #table Payloads Specific payloads only to be considered. +-- @return Ops.Airwing#AIRWING.Payload Payload table or *nil*. +function LEGION:FetchPayloadFromStock(UnitType, MissionType, Payloads) + -- Polymorphic. Will return something when called by airwing. + return nil +end + +--- Return payload from asset back to stock. +-- @param #LEGION self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The squadron asset. +function LEGION:ReturnPayloadFromAsset(asset) + -- Polymorphic. + return nil end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - --- **Ops** - Airwing Warehouse. -- -- **Main Features:** -- -- * Manage squadrons. +-- * Launch A2A and A2G missions (AUFTRAG) +-- +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20Airwing). -- -- === -- -- ### Author: **funkyfranky** +-- +-- === -- @module Ops.Airwing -- @image OPS_AirWing.png @@ -139965,7 +162520,7 @@ end -- @field #table menu Table of menu items. -- @field #table squadrons Table of squadrons. -- @field #table missionqueue Mission queue table. --- @field #table payloads Playloads for specific aircraft and mission types. +-- @field #table payloads Playloads for specific aircraft and mission types. -- @field #number payloadcounter Running index of payloads. -- @field Core.Set#SET_ZONE zonesetCAP Set of CAP zones. -- @field Core.Set#SET_ZONE zonesetTANKER Set of TANKER zones. @@ -139973,83 +162528,79 @@ end -- @field #number nflightsCAP Number of CAP flights constantly in the air. -- @field #number nflightsAWACS Number of AWACS flights constantly in the air. -- @field #number nflightsTANKERboom Number of TANKER flights with BOOM constantly in the air. --- @field #number nflightsTANKERprobe Number of TANKER flights with PROBE constantly in the air. +-- @field #number nflightsTANKERprobe Number of TANKER flights with PROBE constantly in the air. -- @field #number nflightsRescueHelo Number of Rescue helo flights constantly in the air. -- @field #table pointsCAP Table of CAP points. -- @field #table pointsTANKER Table of Tanker points. -- @field #table pointsAWACS Table of AWACS points. -- @field #boolean markpoints Display markers on the F10 map. --- @field Ops.WingCommander#WINGCOMMANDER wingcommander The wing commander responsible for this airwing. --- +-- @field Ops.Airboss#AIRBOSS airboss Airboss attached to this wing. +-- -- @field Ops.RescueHelo#RESCUEHELO rescuehelo The rescue helo. -- @field Ops.RecoveryTanker#RECOVERYTANKER recoverytanker The recoverytanker. -- --- @extends Functional.Warehouse#WAREHOUSE +-- @field #string takeoffType Take of type. +-- @field #boolean despawnAfterLanding Aircraft are despawned after landing. +-- @field #boolean despawnAfterHolding Aircraft are despawned after holding. +-- +-- @extends Ops.Legion#LEGION ---- Be surprised! +--- *I fly because it releases my mind from the tyranny of petty things.* -- Antoine de Saint-Exupery -- -- === -- --- ![Banner Image](..\Presentations\OPS\AirWing\_Main.png) --- -- # The AIRWING Concept --- +-- -- An AIRWING consists of multiple SQUADRONS. These squadrons "live" in a WAREHOUSE, i.e. a physical structure that is connected to an airbase (airdrome, FRAP or ship). -- For an airwing to be operational, it needs airframes, weapons/fuel and an airbase. --- +-- -- # Create an Airwing --- +-- -- ## Constructing the Airwing --- +-- -- airwing=AIRWING:New("Warehouse Batumi", "8th Fighter Wing") -- airwing:Start() --- +-- -- The first parameter specified the warehouse, i.e. the static building housing the airwing (or the name of the aircraft carrier). The second parameter is optional -- and sets an alias. --- +-- -- ## Adding Squadrons --- +-- -- At this point the airwing does not have any assets (aircraft). In order to add these, one needs to first define SQUADRONS. --- +-- -- VFA151=SQUADRON:New("F-14 Group", 8, "VFA-151 (Vigilantes)") -- VFA151:AddMissionCapability({AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT}) --- +-- -- airwing:AddSquadron(VFA151) --- +-- -- This adds eight Tomcat groups beloning to VFA-151 to the airwing. This squadron has the ability to perform combat air patrols and intercepts. --- +-- -- ## Adding Payloads --- +-- -- Adding pure airframes is not enough. The aircraft also need weapons (and fuel) for certain missions. These must be given to the airwing from template groups -- defined in the Mission Editor. --- +-- -- -- F-14 payloads for CAP and INTERCEPT. Phoenix are first, sparrows are second choice. -- airwing:NewPayload(GROUP:FindByName("F-14 Payload AIM-54C"), 2, {AUFTRAG.Type.INTERCEPT, AUFTRAG.Type.GCICAP}, 80) -- airwing:NewPayload(GROUP:FindByName("F-14 Payload AIM-7M"), 20, {AUFTRAG.Type.INTERCEPT, AUFTRAG.Type.GCICAP}) --- +-- -- This will add two AIM-54C and 20 AIM-7M payloads. --- +-- -- If the airwing gets an intercept or patrol mission assigned, it will first use the AIM-54s. Once these are consumed, the AIM-7s are attached to the aircraft. --- +-- -- When an airwing does not have a payload for a certain mission type, the mission cannot be carried out. --- +-- -- You can set the number of payloads to "unlimited" by setting its quantity to -1. --- +-- -- # Adding Missions --- +-- -- Various mission types can be added easily via the AUFTRAG class. --- +-- -- Once you created an AUFTRAG you can add it to the AIRWING with the :AddMission(mission) function. --- --- This mission will be put into the AIRWING queue. Once the mission start time is reached and all resources (airframes and pylons) are available, the mission is started. +-- +-- This mission will be put into the AIRWING queue. Once the mission start time is reached and all resources (airframes and payloads) are available, the mission is started. -- If the mission stop time is over (and the mission is not finished), it will be cancelled and removed from the queue. This applies also to mission that were not even -- started. --- --- # Command an Airwing --- --- An airwing can receive missions from a WINGCOMMANDER. See docs of that class for details. --- --- However, you are still free to add missions at anytime. -- -- -- @field #AIRWING @@ -140058,25 +162609,14 @@ AIRWING = { verbose = 0, lid = nil, menu = nil, - squadrons = {}, - missionqueue = {}, payloads = {}, payloadcounter = 0, pointsCAP = {}, pointsTANKER = {}, pointsAWACS = {}, - wingcommander = nil, markpoints = false, } ---- Squadron asset. --- @type AIRWING.SquadronAsset --- @field #AIRWING.Payload payload The payload of the asset. --- @field Ops.FlightGroup#FLIGHTGROUP flightgroup The flightgroup object. --- @field #string squadname Name of the squadron this asset belongs to. --- @field #number Treturned Time stamp when asset returned to the airwing. --- @extends Functional.Warehouse#WAREHOUSE.Assetitem - --- Payload data. -- @type AIRWING.Payload -- @field #number uid Unique payload ID. @@ -140095,20 +162635,47 @@ AIRWING = { -- @field #number heading Heading in degrees. -- @field #number leg Leg length in NM. -- @field #number speed Speed in knots. +-- @field #number refuelsystem Refueling system type: `0=Unit.RefuelingSystem.BOOM_AND_RECEPTACLE`, `1=Unit.RefuelingSystem.PROBE_AND_DROGUE`. -- @field #number noccupied Number of flights on this patrol point. -- @field Wrapper.Marker#MARKER marker F10 marker. +--- Patrol zone. +-- @type AIRWING.PatrolZone +-- @field Core.Zone#ZONE zone Zone. +-- @field #number altitude Altitude in feet. +-- @field #number heading Heading in degrees. +-- @field #number leg Leg length in NM. +-- @field #number speed Speed in knots. +-- @field Ops.Auftrag#AUFTRAG mission Mission assigned. +-- @field Wrapper.Marker#MARKER marker F10 marker. + +--- AWACS zone. +-- @type AIRWING.AwacsZone +-- @field Core.Zone#ZONE zone Zone. +-- @field #number altitude Altitude in feet. +-- @field #number heading Heading in degrees. +-- @field #number leg Leg length in NM. +-- @field #number speed Speed in knots. +-- @field Ops.Auftrag#AUFTRAG mission Mission assigned. +-- @field Wrapper.Marker#MARKER marker F10 marker. + +--- Tanker zone. +-- @type AIRWING.TankerZone +-- @field #number refuelsystem Refueling system type: `0=Unit.RefuelingSystem.BOOM_AND_RECEPTACLE`, `1=Unit.RefuelingSystem.PROBE_AND_DROGUE`. +-- @extends #AIRWING.PatrolZone + --- AIRWING class version. -- @field #string version -AIRWING.version="0.5.2" +AIRWING.version="0.9.2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Spawn in air or hot ==> Needs WAREHOUSE update. --- TODO: Make special request to transfer squadrons to anther airwing (or warehouse). --- TODO: Check that airbase has enough parking spots if a request is BIG. Alternatively, split requests. +-- TODO: Check that airbase has enough parking spots if a request is BIG. +-- DONE: Spawn in air ==> Needs WAREHOUSE update. +-- DONE: Spawn hot. +-- DONE: Make special request to transfer squadrons to anther airwing (or warehouse). -- DONE: Add squadrons to warehouse. -- DONE: Build mission queue. -- DONE: Find way to start missions. @@ -140128,8 +162695,8 @@ AIRWING.version="0.5.2" -- @return #AIRWING self function AIRWING:New(warehousename, airwingname) - -- Inherit everything from WAREHOUSE class. - local self=BASE:Inherit(self, WAREHOUSE:New(warehousename, airwingname)) -- #AIRWING + -- Inherit everything from LEGION class. + local self=BASE:Inherit(self, LEGION:New(warehousename, airwingname)) -- #AIRWING -- Nil check. if not self then @@ -140139,25 +162706,19 @@ function AIRWING:New(warehousename, airwingname) -- Set some string id for output to DCS.log file. self.lid=string.format("AIRWING %s | ", self.alias) - - -- Add FSM transitions. - -- From State --> Event --> To State - self:AddTransition("*", "MissionRequest", "*") -- Add a (mission) request to the warehouse. - self:AddTransition("*", "MissionCancel", "*") -- Cancel mission. - self:AddTransition("*", "SquadAssetReturned", "*") -- Flight was spawned with a mission. - - self:AddTransition("*", "FlightOnMission", "*") -- Flight was spawned with a mission. - -- Defaults: - --self:SetVerbosity(0) self.nflightsCAP=0 self.nflightsAWACS=0 self.nflightsTANKERboom=0 self.nflightsTANKERprobe=0 self.nflightsRecoveryTanker=0 self.nflightsRescueHelo=0 - self.markpoints=false + self.markpoints=false + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("*", "FlightOnMission", "*") -- A FLIGHTGROUP was send on a Mission (AUFTRAG). ------------------------ --- Pseudo Functions --- @@ -140172,6 +162733,7 @@ function AIRWING:New(warehousename, airwingname) -- @param #AIRWING self -- @param #number delay Delay in seconds. + --- Triggers the FSM event "Stop". Stops the AIRWING and all its event handlers. -- @param #AIRWING self @@ -140179,24 +162741,29 @@ function AIRWING:New(warehousename, airwingname) -- @function [parent=#AIRWING] __Stop -- @param #AIRWING self -- @param #number delay Delay in seconds. - - --- On after "FlightOnMission" event. Triggered when an asset group starts a mission. - -- @function [parent=#AIRWING] OnAfterFlightOnMission - -- @param #AIRWING self - -- @param #string From The From state - -- @param #string Event The Event called - -- @param #string To The To state - -- @param Ops.FlightGroup#FLIGHTGROUP Flightgroup The Flightgroup on mission - -- @param Ops.Auftrag#AUFTRAG Mission The Auftrag of the Flightgroup - - --- On after "AssetReturned" event. Triggered when an asset group returned to its airwing. - -- @function [parent=#AIRWING] OnAfterAssetReturned - -- @param #AIRWING self - -- @param #string From From state. - -- @param #string Event Event. - -- @param #string To To state. - -- @param Ops.Squadron#SQUADRON Squadron The asset squadron. - -- @param #AIRWING.SquadronAsset Asset The asset that returned. + + + --- Triggers the FSM event "FlightOnMission". + -- @function [parent=#AIRWING] FlightOnMission + -- @param #AIRWING self + -- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup The FLIGHTGROUP on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "FlightOnMission" after a delay. + -- @function [parent=#AIRWING] __FlightOnMission + -- @param #AIRWING self + -- @param #number delay Delay in seconds. + -- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup The FLIGHTGROUP on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "FlightOnMission" event. + -- @function [parent=#AIRWING] OnAfterFlightOnMission + -- @param #AIRWING self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup The FLIGHTGROUP on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. return self end @@ -140212,21 +162779,24 @@ end function AIRWING:AddSquadron(Squadron) -- Add squadron to airwing. - table.insert(self.squadrons, Squadron) - + table.insert(self.cohorts, Squadron) + -- Add assets to squadron. self:AddAssetToSquadron(Squadron, Squadron.Ngroups) - + -- Tanker and AWACS get unlimited payloads. if Squadron.attribute==GROUP.Attribute.AIR_AWACS then self:NewPayload(Squadron.templategroup, -1, AUFTRAG.Type.AWACS) elseif Squadron.attribute==GROUP.Attribute.AIR_TANKER then self:NewPayload(Squadron.templategroup, -1, AUFTRAG.Type.TANKER) end + + -- Relocate mission. + self:NewPayload(Squadron.templategroup, -1, AUFTRAG.Type.RELOCATECOHORT, 0) -- Set airwing to squadron. Squadron:SetAirwing(self) - + -- Start squadron. if Squadron:IsStopped() then Squadron:Start() @@ -140261,25 +162831,23 @@ function AIRWING:NewPayload(Unit, Npayloads, MissionTypes, Performance) if Unit:IsInstanceOf("GROUP") then Unit=Unit:GetUnit(1) end - + -- Ensure Missiontypes is a table. if MissionTypes and type(MissionTypes)~="table" then MissionTypes={MissionTypes} end - + -- Create payload. local payload={} --#AIRWING.Payload payload.uid=self.payloadcounter payload.unitname=Unit:GetName() - payload.aircrafttype=Unit:GetTypeName() + payload.aircrafttype=Unit:GetTypeName() payload.pylons=Unit:GetTemplatePayload() - payload.unlimited=Npayloads<0 - if payload.unlimited then - payload.navail=1 - else - payload.navail=Npayloads or 99 - end + -- Set the number of available payloads. + self:SetPayloadAmount(payload, Npayloads) + + -- Payload capabilities. payload.capabilities={} for _,missiontype in pairs(MissionTypes) do local capability={} --Ops.Auftrag#AUFTRAG.Capability @@ -140287,33 +162855,64 @@ function AIRWING:NewPayload(Unit, Npayloads, MissionTypes, Performance) capability.Performance=Performance table.insert(payload.capabilities, capability) end - - -- Add ORBIT for all. - if not self:CheckMissionType(AUFTRAG.Type.ORBIT, MissionTypes) then + + -- Add ORBIT for all. + if not AUFTRAG.CheckMissionType(AUFTRAG.Type.ORBIT, MissionTypes) then local capability={} --Ops.Auftrag#AUFTRAG.Capability capability.MissionType=AUFTRAG.Type.ORBIT capability.Performance=50 table.insert(payload.capabilities, capability) - end + end + -- Add RELOCATION for all. + if not AUFTRAG.CheckMissionType(AUFTRAG.Type.RELOCATECOHORT, MissionTypes) then + local capability={} --Ops.Auftrag#AUFTRAG.Capability + capability.MissionType=AUFTRAG.Type.RELOCATECOHORT + capability.Performance=50 + table.insert(payload.capabilities, capability) + end + -- Info - self:T(self.lid..string.format("Adding new payload from unit %s for aircraft type %s: ID=%d, N=%d (unlimited=%s), performance=%d, missions: %s", + self:T(self.lid..string.format("Adding new payload from unit %s for aircraft type %s: ID=%d, N=%d (unlimited=%s), performance=%d, missions: %s", payload.unitname, payload.aircrafttype, payload.uid, payload.navail, tostring(payload.unlimited), Performance, table.concat(MissionTypes, ", "))) -- Add payload table.insert(self.payloads, payload) - + -- Increase counter self.payloadcounter=self.payloadcounter+1 - + return payload - + end self:E(self.lid.."ERROR: No UNIT found to create PAYLOAD!") return nil end +--- Set the number of payload available. +-- @param #AIRWING self +-- @param #AIRWING.Payload Payload The payload table created by the `:NewPayload` function. +-- @param #number Navailable Number of payloads available to the airwing resources. Default 99 (which should be enough for most scenarios). Set to -1 for unlimited. +-- @return #AIRWING self +function AIRWING:SetPayloadAmount(Payload, Navailable) + + Navailable=Navailable or 99 + + if Payload then + + Payload.unlimited=Navailable<0 + if Payload.unlimited then + Payload.navail=1 + else + Payload.navail=Navailable + end + + end + + return self +end + --- Add a mission capability to an existing payload. -- @param #AIRWING self -- @param #AIRWING.Payload Payload The payload table to which the capability should be added. @@ -140328,15 +162927,15 @@ function AIRWING:AddPayloadCapability(Payload, MissionTypes, Performance) end Payload.capabilities=Payload.capabilities or {} - + for _,missiontype in pairs(MissionTypes) do - + local capability={} --Ops.Auftrag#AUFTRAG.Capability capability.MissionType=missiontype capability.Performance=Performance - + --TODO: check that capability does not already exist! - + table.insert(Payload.capabilities, capability) end @@ -140357,7 +162956,7 @@ function AIRWING:FetchPayloadFromStock(UnitType, MissionType, Payloads) self:T(self.lid.."WARNING: No payloads in stock!") return nil end - + -- Debug. if self.verbose>=4 then self:I(self.lid..string.format("Looking for payload for unit type=%s and mission type=%s", UnitType, MissionType)) @@ -140370,7 +162969,7 @@ function AIRWING:FetchPayloadFromStock(UnitType, MissionType, Payloads) --- Sort payload wrt the following criteria: -- 1) Highest performance is the main selection criterion. - -- 2) If payloads have the same performance, unlimited payloads are preferred over limited ones. + -- 2) If payloads have the same performance, unlimited payloads are preferred over limited ones. -- 3) If payloads have the same performance _and_ are limited, the more abundant one is preferred. local function sortpayloads(a,b) local pA=a --#AIRWING.Payload @@ -140403,7 +163002,7 @@ function AIRWING:FetchPayloadFromStock(UnitType, MissionType, Payloads) return nil end return false - end + end -- Pre-selection: filter out only those payloads that are valid for the airframe and mission type and are available. local payloads={} @@ -140411,31 +163010,31 @@ function AIRWING:FetchPayloadFromStock(UnitType, MissionType, Payloads) local payload=_payload --#AIRWING.Payload local specialpayload=_checkPayloads(payload) - local compatible=self:CheckMissionCapability(MissionType, payload.capabilities) - + local compatible=AUFTRAG.CheckMissionCapability(MissionType, payload.capabilities) + local goforit = specialpayload or (specialpayload==nil and compatible) if payload.aircrafttype==UnitType and payload.navail>0 and goforit then table.insert(payloads, payload) end end - + -- Debug. if self.verbose>=4 then - self:I(self.lid..string.format("Sorted payloads for mission type X and aircraft type=Y:")) + self:I(self.lid..string.format("Sorted payloads for mission type %s and aircraft type=%s:", MissionType, UnitType)) for _,_payload in ipairs(self.payloads) do local payload=_payload --#AIRWING.Payload - if payload.aircrafttype==UnitType and self:CheckMissionCapability(MissionType, payload.capabilities) then + if payload.aircrafttype==UnitType and AUFTRAG.CheckMissionCapability(MissionType, payload.capabilities) then local performace=self:GetPayloadPeformance(payload, MissionType) - self:I(self.lid..string.format("FF %s payload for %s: avail=%d performace=%d", MissionType, payload.aircrafttype, payload.navail, performace)) + self:I(self.lid..string.format("- %s payload for %s: avail=%d performace=%d", MissionType, payload.aircrafttype, payload.navail, performace)) end end end - + -- Cases: if #payloads==0 then -- No payload available. - self:T(self.lid.."Warning could not find a payload for airframe X mission type Y!") + self:T(self.lid..string.format("WARNING: Could not find a payload for airframe %s mission type %s!", UnitType, MissionType)) return nil elseif #payloads==1 then -- Only one payload anyway. @@ -140450,21 +163049,21 @@ function AIRWING:FetchPayloadFromStock(UnitType, MissionType, Payloads) local payload=payloads[1] --#AIRWING.Payload if not payload.unlimited then payload.navail=payload.navail-1 - end + end return payload end - + end --- Return payload from asset back to stock. -- @param #AIRWING self --- @param #AIRWING.SquadronAsset asset The squadron asset. +-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The squadron asset. function AIRWING:ReturnPayloadFromAsset(asset) local payload=asset.payload - + if payload then - + -- Increase count if not unlimited. if not payload.unlimited then payload.navail=payload.navail+1 @@ -140472,11 +163071,11 @@ function AIRWING:ReturnPayloadFromAsset(asset) -- Remove asset payload. asset.payload=nil - + else self:E(self.lid.."ERROR: asset had no payload attached!") end - + end @@ -140488,23 +163087,23 @@ end function AIRWING:AddAssetToSquadron(Squadron, Nassets) if Squadron then - + -- Get the template group of the squadron. local Group=GROUP:FindByName(Squadron.templatename) - + if Group then - + -- Debug text. local text=string.format("Adding asset %s to squadron %s", Group:GetName(), Squadron.name) self:T(self.lid..text) - + -- Add assets to airwing warehouse. self:AddAsset(Group, Nassets, nil, nil, nil, nil, Squadron.skill, Squadron.livery, Squadron.name) - + else self:E(self.lid.."ERROR: Group does not exist!") end - + else self:E(self.lid.."ERROR: Squadron does not exit!") end @@ -140517,31 +163116,13 @@ end -- @param #string SquadronName Name of the squadron, e.g. "VFA-37". -- @return Ops.Squadron#SQUADRON The squadron object. function AIRWING:GetSquadron(SquadronName) - - for _,_squadron in pairs(self.squadrons) do - local squadron=_squadron --Ops.Squadron#SQUADRON - - if squadron.name==SquadronName then - return squadron - end - - end - - return nil -end - ---- Set verbosity level. --- @param #AIRWING self --- @param #number VerbosityLevel Level of output (higher=more). Default 0. --- @return #AIRWING self -function AIRWING:SetVerbosity(VerbosityLevel) - self.verbose=VerbosityLevel or 0 - return self + local squad=self:_GetCohort(SquadronName) + return squad end --- Get squadron of an asset. -- @param #AIRWING self --- @param #AIRWING.SquadronAsset Asset The squadron asset. +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The squadron asset. -- @return Ops.Squadron#SQUADRON The squadron object. function AIRWING:GetSquadronOfAsset(Asset) return self:GetSquadron(Asset.squadname) @@ -140549,7 +163130,7 @@ end --- Remove asset from squadron. -- @param #AIRWING self --- @param #AIRWING.SquadronAsset Asset The squad asset. +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The squad asset. function AIRWING:RemoveAssetFromSquadron(Asset) local squad=self:GetSquadronOfAsset(Asset) if squad then @@ -140557,45 +163138,6 @@ function AIRWING:RemoveAssetFromSquadron(Asset) end end ---- Add mission to queue. --- @param #AIRWING self --- @param Ops.Auftrag#AUFTRAG Mission for this group. --- @return #AIRWING self -function AIRWING:AddMission(Mission) - - -- Set status to QUEUED. This also attaches the airwing to this mission. - Mission:Queued(self) - - -- Add mission to queue. - table.insert(self.missionqueue, Mission) - - -- Info text. - local text=string.format("Added mission %s (type=%s). Starting at %s. Stopping at %s", - tostring(Mission.name), tostring(Mission.type), UTILS.SecondsToClock(Mission.Tstart, true), Mission.Tstop and UTILS.SecondsToClock(Mission.Tstop, true) or "INF") - self:T(self.lid..text) - - return self -end - ---- Remove mission from queue. --- @param #AIRWING self --- @param Ops.Auftrag#AUFTRAG Mission Mission to be removed. --- @return #AIRWING self -function AIRWING:RemoveMission(Mission) - - for i,_mission in pairs(self.missionqueue) do - local mission=_mission --Ops.Auftrag#AUFTRAG - - if mission.auftragsnummer==Mission.auftragsnummer then - table.remove(self.missionqueue, i) - break - end - - end - - return self -end - --- Set number of CAP flights constantly carried out. -- @param #AIRWING self -- @param #number n Number of flights. Default 1. @@ -140654,13 +163196,13 @@ function AIRWING:SetNumberRescuehelo(n) return self end ---- +--- -- @param #AIRWING self -- @param #AIRWING.PatrolData point Patrol point table. -- @return #string Marker text. function AIRWING:_PatrolPointMarkerText(point) - local text=string.format("%s Occupied=%d, \nheading=%03d, leg=%d NM, alt=%d ft, speed=%d kts", + local text=string.format("%s Occupied=%d, \nheading=%03d, leg=%d NM, alt=%d ft, speed=%d kts", point.type, point.noccupied, point.heading, point.leg, point.altitude, point.speed) return text @@ -140670,9 +163212,9 @@ end -- @param #AIRWING.PatrolData point Patrol point table. function AIRWING:UpdatePatrolPointMarker(point) if self.markpoints then -- sometimes there's a direct call from #OPSGROUP - local text=string.format("%s Occupied=%d\nheading=%03d, leg=%d NM, alt=%d ft, speed=%d kts", + local text=string.format("%s Occupied=%d\nheading=%03d, leg=%d NM, alt=%d ft, speed=%d kts", point.type, point.noccupied, point.heading, point.leg, point.altitude, point.speed) - + point.marker:UpdateText(text, 1) end end @@ -140686,8 +163228,14 @@ end -- @param #number Heading Heading in degrees. Default random (0, 360] degrees. -- @param #number LegLength Length of race-track orbit in NM. Default 15 NM. -- @param #number Speed Orbit speed in knots. Default 350 knots. +-- @param #number RefuelSystem Refueling system: 0=Boom, 1=Probe. Default nil=any. -- @return #AIRWING.PatrolData Patrol point table. -function AIRWING:NewPatrolPoint(Type, Coordinate, Altitude, Speed, Heading, LegLength) +function AIRWING:NewPatrolPoint(Type, Coordinate, Altitude, Speed, Heading, LegLength, RefuelSystem) + + -- Check if a zone was passed instead of a coordinate. + if Coordinate and Coordinate:IsInstanceOf("ZONE_BASE") then + Coordinate=Coordinate:GetCoordinate() + end local patrolpoint={} --#AIRWING.PatrolData patrolpoint.type=Type or "Unknown" @@ -140697,12 +163245,13 @@ function AIRWING:NewPatrolPoint(Type, Coordinate, Altitude, Speed, Heading, LegL patrolpoint.altitude=Altitude or math.random(10,20)*1000 patrolpoint.speed=Speed or 350 patrolpoint.noccupied=0 - + patrolpoint.refuelsystem=RefuelSystem + if self.markpoints then patrolpoint.marker=MARKER:New(Coordinate, "New Patrol Point"):ToAll() AIRWING.UpdatePatrolPointMarker(patrolpoint) end - + return patrolpoint end @@ -140715,7 +163264,7 @@ end -- @param #number LegLength Length of race-track orbit in NM. -- @return #AIRWING self function AIRWING:AddPatrolPointCAP(Coordinate, Altitude, Speed, Heading, LegLength) - + local patrolpoint=self:NewPatrolPoint("CAP", Coordinate, Altitude, Speed, Heading, LegLength) table.insert(self.pointsCAP, patrolpoint) @@ -140730,10 +163279,11 @@ end -- @param #number Speed Orbit speed in knots. -- @param #number Heading Heading in degrees. -- @param #number LegLength Length of race-track orbit in NM. +-- @param #number RefuelSystem Set refueling system of tanker: 0=boom, 1=probe. Default any (=nil). -- @return #AIRWING self -function AIRWING:AddPatrolPointTANKER(Coordinate, Altitude, Speed, Heading, LegLength) - - local patrolpoint=self:NewPatrolPoint("Tanker", Coordinate, Altitude, Speed, Heading, LegLength) +function AIRWING:AddPatrolPointTANKER(Coordinate, Altitude, Speed, Heading, LegLength, RefuelSystem) + + local patrolpoint=self:NewPatrolPoint("Tanker", Coordinate, Altitude, Speed, Heading, LegLength, RefuelSystem) table.insert(self.pointsTANKER, patrolpoint) @@ -140749,7 +163299,7 @@ end -- @param #number LegLength Length of race-track orbit in NM. -- @return #AIRWING self function AIRWING:AddPatrolPointAWACS(Coordinate, Altitude, Speed, Heading, LegLength) - + local patrolpoint=self:NewPatrolPoint("AWACS", Coordinate, Altitude, Speed, Heading, LegLength) table.insert(self.pointsAWACS, patrolpoint) @@ -140757,6 +163307,86 @@ function AIRWING:AddPatrolPointAWACS(Coordinate, Altitude, Speed, Heading, LegLe return self end +--- Set airboss of this wing. He/she will take care that no missions are launched if the carrier is recovering. +-- @param #AIRWING self +-- @param Ops.Airboss#AIRBOSS airboss The AIRBOSS object. +-- @return #AIRWING self +function AIRWING:SetAirboss(airboss) + self.airboss=airboss + return self +end + +--- Set takeoff type. All assets of this squadron will be spawned with cold (default) or hot engines. +-- Spawning on runways is not supported. +-- @param #AIRWING self +-- @param #string TakeoffType Take off type: "Cold" (default) or "Hot" with engines on or "Air" for spawning in air. +-- @return #AIRWING self +function AIRWING:SetTakeoffType(TakeoffType) + TakeoffType=TakeoffType or "Cold" + if TakeoffType:lower()=="hot" then + self.takeoffType=COORDINATE.WaypointType.TakeOffParkingHot + elseif TakeoffType:lower()=="cold" then + self.takeoffType=COORDINATE.WaypointType.TakeOffParking + elseif TakeoffType:lower()=="air" then + self.takeoffType=COORDINATE.WaypointType.TurningPoint + else + self.takeoffType=COORDINATE.WaypointType.TakeOffParking + end + return self +end + +--- Set takeoff type cold (default). All assets of this squadron will be spawned with engines off (cold). +-- @param #AIRWING self +-- @return #AIRWING self +function AIRWING:SetTakeoffCold() + self:SetTakeoffType("Cold") + return self +end + +--- Set takeoff type hot. All assets of this squadron will be spawned with engines on (hot). +-- @param #AIRWING self +-- @return #AIRWING self +function AIRWING:SetTakeoffHot() + self:SetTakeoffType("Hot") + return self +end + +--- Set takeoff type air. All assets of this squadron will be spawned in air above the airbase. +-- @param #AIRWING self +-- @return #AIRWING self +function AIRWING:SetTakeoffAir() + self:SetTakeoffType("Air") + return self +end + +--- Set despawn after landing. Aircraft will be despawned after the landing event. +-- Can help to avoid DCS AI taxiing issues. +-- @param #AIRWING self +-- @param #boolean Switch If `true` (default), activate despawn after landing. +-- @return #AIRWING self +function AIRWING:SetDespawnAfterLanding(Switch) + if Switch then + self.despawnAfterLanding=Switch + else + self.despawnAfterLanding=true + end + return self +end + +--- Set despawn after holding. Aircraft will be despawned when they arrive at their holding position at the airbase. +-- Can help to avoid DCS AI taxiing issues. +-- @param #AIRWING self +-- @param #boolean Switch If `true` (default), activate despawn after landing. +-- @return #AIRWING self +function AIRWING:SetDespawnAfterHolding(Switch) + if Switch then + self.despawnAfterHolding=Switch + else + self.despawnAfterHolding=true + end + return self +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Start & Status ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -140766,7 +163396,7 @@ end function AIRWING:onafterStart(From, Event, To) -- Start parent Warehouse. - self:GetParent(self).onafterStart(self, From, Event, To) + self:GetParent(self, AIRWING).onafterStart(self, From, Event, To) -- Info. self:I(self.lid..string.format("Starting AIRWING v%s", AIRWING.version)) @@ -140781,39 +163411,53 @@ function AIRWING:onafterStatus(From, Event, To) self:GetParent(self).onafterStatus(self, From, Event, To) local fsmstate=self:GetState() - + -- Check CAP missions. self:CheckCAP() - + -- Check TANKER missions. self:CheckTANKER() - + -- Check AWACS missions. self:CheckAWACS() - + -- Check Rescue Helo missions. self:CheckRescuhelo() - - + + ---------------- + -- Transport --- + ---------------- + + -- Check transport queue. + self:CheckTransportQueue() + + -------------- + -- Mission --- + -------------- + + -- Check mission queue. + self:CheckMissionQueue() + + -- General info: if self.verbose>=1 then -- Count missions not over yet. local Nmissions=self:CountMissionsInQueue() - + -- Count ALL payloads in stock. If any payload is unlimited, this gives 999. local Npayloads=self:CountPayloadsInStock(AUFTRAG.Type) - + -- Assets tot local Npq, Np, Nq=self:CountAssetsOnMission() - + local assets=string.format("%d (OnMission: Total=%d, Active=%d, Queued=%d)", self:CountAssets(), Npq, Np, Nq) -- Output. - local text=string.format("%s: Missions=%d, Payloads=%d (%d), Squads=%d, Assets=%s", fsmstate, Nmissions, Npayloads, #self.payloads, #self.squadrons, assets) + local text=string.format("%s: Missions=%d, Payloads=%d (%d), Squads=%d, Assets=%s", fsmstate, Nmissions, Npayloads, #self.payloads, #self.cohorts, assets) self:I(self.lid..text) end - + ------------------ -- Mission Info -- ------------------ @@ -140821,56 +163465,43 @@ function AIRWING:onafterStatus(From, Event, To) local text=string.format("Missions Total=%d:", #self.missionqueue) for i,_mission in pairs(self.missionqueue) do local mission=_mission --Ops.Auftrag#AUFTRAG - + local prio=string.format("%d/%s", mission.prio, tostring(mission.importance)) ; if mission.urgent then prio=prio.." (!)" end - local assets=string.format("%d/%d", mission:CountOpsGroups(), mission.nassets) + local assets=string.format("%d/%d", mission:CountOpsGroups(), mission:GetNumberOfRequiredAssets()) local target=string.format("%d/%d Damage=%.1f", mission:CountMissionTargets(), mission:GetTargetInitialNumber(), mission:GetTargetDamage()) - - text=text..string.format("\n[%d] %s %s: Status=%s, Prio=%s, Assets=%s, Targets=%s", i, mission.name, mission.type, mission.status, prio, assets, target) + local mystatus=mission:GetLegionStatus(self) + + text=text..string.format("\n[%d] %s %s: Status=%s [%s], Prio=%s, Assets=%s, Targets=%s", i, mission.name, mission.type, mystatus, mission.status, prio, assets, target) end self:I(self.lid..text) end - + ------------------- -- Squadron Info -- ------------------- if self.verbose>=3 then local text="Squadrons:" - for i,_squadron in pairs(self.squadrons) do + for i,_squadron in pairs(self.cohorts) do local squadron=_squadron --Ops.Squadron#SQUADRON - + local callsign=squadron.callsignName and UTILS.GetCallsignName(squadron.callsignName) or "N/A" local modex=squadron.modex and squadron.modex or -1 local skill=squadron.skill and tostring(squadron.skill) or "N/A" - + -- Squadron text - text=text..string.format("\n* %s %s: %s*%d/%d, Callsign=%s, Modex=%d, Skill=%s", squadron.name, squadron:GetState(), squadron.aircrafttype, squadron:CountAssetsInStock(), #squadron.assets, callsign, modex, skill) + text=text..string.format("\n* %s %s: %s*%d/%d, Callsign=%s, Modex=%d, Skill=%s", squadron.name, squadron:GetState(), squadron.aircrafttype, squadron:CountAssets(true), #squadron.assets, callsign, modex, skill) end self:I(self.lid..text) end - - -------------- - -- Mission --- - -------------- - - -- Check if any missions should be cancelled. - self:_CheckMissions() - - -- Get next mission. - local mission=self:_GetNextMission() - - -- Request mission execution. - if mission then - self:MissionRequest(mission) - end end ---- Get patrol data +--- Get patrol data. -- @param #AIRWING self -- @param #table PatrolPoints Patrol data points. --- @return #AIRWING.PatrolData -function AIRWING:_GetPatrolData(PatrolPoints) +-- @param #number RefuelSystem If provided, only return points with the specific refueling system. +-- @return #AIRWING.PatrolData Patrol point data table. +function AIRWING:_GetPatrolData(PatrolPoints, RefuelSystem) -- Sort wrt lowest number of flights on this point. local function sort(a,b) @@ -140878,17 +163509,21 @@ function AIRWING:_GetPatrolData(PatrolPoints) end if PatrolPoints and #PatrolPoints>0 then - + -- Sort data wrt number of flights at that point. table.sort(PatrolPoints, sort) - return PatrolPoints[1] - else - - return self:NewPatrolPoint() - + for _,_patrolpoint in pairs(PatrolPoints) do + local patrolpoint=_patrolpoint --#AIRWING.PatrolData + if (RefuelSystem and patrolpoint.refuelsystem and RefuelSystem==patrolpoint.refuelsystem) or RefuelSystem==nil or patrolpoint.refuelsystem==nil then + return patrolpoint + end + end + end - + + -- Return a new point. + return self:NewPatrolPoint() end --- Check how many CAP missions are assigned and add number of missing missions. @@ -140896,26 +163531,36 @@ end -- @return #AIRWING self function AIRWING:CheckCAP() - local Ncap=self:CountMissionsInQueue({AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT}) - - for i=1,self.nflightsCAP-Ncap do - - local patrol=self:_GetPatrolData(self.pointsCAP) - - local altitude=patrol.altitude+1000*patrol.noccupied - - local missionCAP=AUFTRAG:NewGCICAP(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg) - - missionCAP.patroldata=patrol - - patrol.noccupied=patrol.noccupied+1 - - if self.markpoints then AIRWING.UpdatePatrolPointMarker(patrol) end - - self:AddMission(missionCAP) - + local Ncap=0 --self:CountMissionsInQueue({AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT}) + + -- Count CAP missions. + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission:IsNotOver() and mission.type==AUFTRAG.Type.GCICAP and mission.patroldata then + Ncap=Ncap+1 + end + end - + + for i=1,self.nflightsCAP-Ncap do + + local patrol=self:_GetPatrolData(self.pointsCAP) + + local altitude=patrol.altitude+1000*patrol.noccupied + + local missionCAP=AUFTRAG:NewGCICAP(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg) + + missionCAP.patroldata=patrol + + patrol.noccupied=patrol.noccupied+1 + + if self.markpoints then AIRWING.UpdatePatrolPointMarker(patrol) end + + self:AddMission(missionCAP) + + end + return self end @@ -140926,58 +163571,60 @@ function AIRWING:CheckTANKER() local Nboom=0 local Nprob=0 - - -- Count tanker mission. + + -- Count tanker missions. for _,_mission in pairs(self.missionqueue) do local mission=_mission --Ops.Auftrag#AUFTRAG - - if mission:IsNotOver() and mission.type==AUFTRAG.Type.TANKER then - if mission.refuelSystem==0 then + + if mission:IsNotOver() and mission.type==AUFTRAG.Type.TANKER and mission.patroldata then + if mission.refuelSystem==Unit.RefuelingSystem.BOOM_AND_RECEPTACLE then Nboom=Nboom+1 - elseif mission.refuelSystem==1 then + elseif mission.refuelSystem==Unit.RefuelingSystem.PROBE_AND_DROGUE then Nprob=Nprob+1 end - + end - + end - + + -- Check missing boom tankers. for i=1,self.nflightsTANKERboom-Nboom do - + local patrol=self:_GetPatrolData(self.pointsTANKER) - + local altitude=patrol.altitude+1000*patrol.noccupied - - local mission=AUFTRAG:NewTANKER(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg, 1) - + + local mission=AUFTRAG:NewTANKER(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg, Unit.RefuelingSystem.BOOM_AND_RECEPTACLE) + mission.patroldata=patrol - + patrol.noccupied=patrol.noccupied+1 - + if self.markpoints then AIRWING.UpdatePatrolPointMarker(patrol) end - + self:AddMission(mission) - + end - + + -- Check missing probe tankers. for i=1,self.nflightsTANKERprobe-Nprob do - + local patrol=self:_GetPatrolData(self.pointsTANKER) - + local altitude=patrol.altitude+1000*patrol.noccupied - - local mission=AUFTRAG:NewTANKER(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg, 0) - + + local mission=AUFTRAG:NewTANKER(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg, Unit.RefuelingSystem.PROBE_AND_DROGUE) + mission.patroldata=patrol - + patrol.noccupied=patrol.noccupied+1 - + if self.markpoints then AIRWING.UpdatePatrolPointMarker(patrol) end - + self:AddMission(mission) - - end - + + end + return self end @@ -140986,26 +163633,37 @@ end -- @return #AIRWING self function AIRWING:CheckAWACS() - local N=self:CountMissionsInQueue({AUFTRAG.Type.AWACS}) - - for i=1,self.nflightsAWACS-N do - - local patrol=self:_GetPatrolData(self.pointsAWACS) - - local altitude=patrol.altitude+1000*patrol.noccupied - - local mission=AUFTRAG:NewAWACS(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg) - - mission.patroldata=patrol - - patrol.noccupied=patrol.noccupied+1 - - if self.markpoints then AIRWING.UpdatePatrolPointMarker(patrol) end - - self:AddMission(mission) - + local N=0 --self:CountMissionsInQueue({AUFTRAG.Type.AWACS}) + + -- Count AWACS missions. + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission:IsNotOver() and mission.type==AUFTRAG.Type.AWACS and mission.patroldata then + N=N+1 + end + end - + + + for i=1,self.nflightsAWACS-N do + + local patrol=self:_GetPatrolData(self.pointsAWACS) + + local altitude=patrol.altitude+1000*patrol.noccupied + + local mission=AUFTRAG:NewAWACS(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg) + + mission.patroldata=patrol + + patrol.noccupied=patrol.noccupied+1 + + if self.markpoints then AIRWING.UpdatePatrolPointMarker(patrol) end + + self:AddMission(mission) + + end + return self end @@ -141015,55 +163673,55 @@ end function AIRWING:CheckRescuhelo() local N=self:CountMissionsInQueue({AUFTRAG.Type.RESCUEHELO}) - + local name=self.airbase:GetName() - + local carrier=UNIT:FindByName(name) - + for i=1,self.nflightsRescueHelo-N do - + local mission=AUFTRAG:NewRESCUEHELO(carrier) - + self:AddMission(mission) - + end - + return self end --- Check how many AWACS missions are assigned and add number of missing missions. -- @param #AIRWING self -- @param Ops.FlightGroup#FLIGHTGROUP flightgroup The flightgroup. --- @return #AIRWING.SquadronAsset The tanker asset. +-- @return Functional.Warehouse#WAREHOUSE.Assetitem The tanker asset. function AIRWING:GetTankerForFlight(flightgroup) local tankers=self:GetAssetsOnMission(AUFTRAG.Type.TANKER) - + if #tankers>0 then - + local tankeropt={} for _,_tanker in pairs(tankers) do - local tanker=_tanker --#AIRWING.SquadronAsset - + local tanker=_tanker --Functional.Warehouse#WAREHOUSE.Assetitem + -- Check that donor and acceptor use the same refuelling system. if flightgroup.refueltype and flightgroup.refueltype==tanker.flightgroup.tankertype then - + local tankercoord=tanker.flightgroup.group:GetCoordinate() local assetcoord=flightgroup.group:GetCoordinate() - + local dist=assetcoord:Get2DDistance(tankercoord) - + -- Ensure that the flight does not find itself. Asset could be a tanker! if dist>5 then table.insert(tankeropt, {tanker=tanker, dist=dist}) end - + end end - + -- Sort tankers wrt to distance. table.sort(tankeropt, function(a,b) return a.dist0 then return tankeropt[1].tanker @@ -141075,782 +163733,49 @@ function AIRWING:GetTankerForFlight(flightgroup) return nil end - ---- Check if mission is not over and ready to cancel. +--- Add the ability to call back an Ops.Awacs#AWACS object with an FSM call "FlightOnMission(FlightGroup, Mission)". -- @param #AIRWING self -function AIRWING:_CheckMissions() - - -- Loop over missions in queue. - for _,_mission in pairs(self.missionqueue) do - local mission=_mission --Ops.Auftrag#AUFTRAG - - if mission:IsNotOver() and mission:IsReadyToCancel() then - mission:Cancel() - end - end - -end ---- Get next mission. --- @param #AIRWING self --- @return Ops.Auftrag#AUFTRAG Next mission or *nil*. -function AIRWING:_GetNextMission() - - -- Number of missions. - local Nmissions=#self.missionqueue - - -- Treat special cases. - if Nmissions==0 then - return nil - end - - -- Sort results table wrt prio and start time. - local function _sort(a, b) - local taskA=a --Ops.Auftrag#AUFTRAG - local taskB=b --Ops.Auftrag#AUFTRAG - return (taskA.prio0 then - self:E(self.lid..string.format("ERROR: mission %s of type %s has already assets attached!", mission.name, mission.type)) - end - mission.assets={} - - -- Assign assets to mission. - for i=1,mission.nassets do - local asset=assets[i] --#AIRWING.SquadronAsset - - -- Should not happen as we just checked! - if not asset.payload then - self:E(self.lid.."ERROR: No payload for asset! This should not happen!") - end - - -- Add asset to mission. - mission:AddAsset(asset) - end - - -- Now return the remaining payloads. - for i=mission.nassets+1,#assets do - local asset=assets[i] --#AIRWING.SquadronAsset - for _,uid in pairs(gotpayload) do - if uid==asset.uid then - self:ReturnPayloadFromAsset(asset) - break - end - end - end - - return mission - end - - end -- mission due? - end -- mission loop - - return nil +-- @param Ops.Awacs#AWACS ConnectecdAwacs +-- @return #AIRWING self +function AIRWING:SetUsingOpsAwacs(ConnectecdAwacs) + self:I(self.lid .. "Added AWACS Object: "..ConnectecdAwacs:GetName() or "unknown") + self.UseConnectedOpsAwacs = true + self.ConnectedOpsAwacs = ConnectecdAwacs + return self end ---- Calculate the mission score of an asset. +--- Remove the ability to call back an Ops.Awacs#AWACS object with an FSM call "FlightOnMission(FlightGroup, Mission)". -- @param #AIRWING self --- @param #AIRWING.SquadronAsset asset Asset --- @param Ops.Auftrag#AUFTRAG Mission Mission for which the best assets are desired. --- @param #boolean includePayload If true, include the payload in the calulation if the asset has one attached. --- @return #number Mission score. -function AIRWING:CalculateAssetMissionScore(asset, Mission, includePayload) - - local score=0 - - -- Prefer highly skilled assets. - if asset.skill==AI.Skill.AVERAGE then - score=score+0 - elseif asset.skill==AI.Skill.GOOD then - score=score+10 - elseif asset.skill==AI.Skill.HIGH then - score=score+20 - elseif asset.skill==AI.Skill.EXCELLENT then - score=score+30 - end - - -- Add mission performance to score. - local squad=self:GetSquadronOfAsset(asset) - local missionperformance=squad:GetMissionPeformance(Mission.type) - score=score+missionperformance - - -- Add payload performance to score. - if includePayload and asset.payload then - score=score+self:GetPayloadPeformance(asset.payload, Mission.type) - end - - -- Intercepts need to be carried out quickly. We prefer spawned assets. - if Mission.type==AUFTRAG.Type.INTERCEPT then - if asset.spawned then - self:T(self.lid.."Adding 25 to asset because it is spawned") - score=score+25 - end - end - - -- TODO: This could be vastly improved. Need to gather ideas during testing. - -- Calculate ETA? Assets on orbit missions should arrive faster even if they are further away. - -- Max speed of assets. - -- Fuel amount? - -- Range of assets? - - return score -end - ---- Optimize chosen assets for the mission at hand. --- @param #AIRWING self --- @param #table assets Table of (unoptimized) assets. --- @param Ops.Auftrag#AUFTRAG Mission Mission for which the best assets are desired. --- @param #boolean includePayload If true, include the payload in the calulation if the asset has one attached. -function AIRWING:_OptimizeAssetSelection(assets, Mission, includePayload) - - local TargetVec2=Mission:GetTargetVec2() - - --local dStock=self:GetCoordinate():Get2DDistance(TargetCoordinate) - - local dStock=UTILS.VecDist2D(TargetVec2, self:GetVec2()) - - -- Calculate distance to mission target. - local distmin=math.huge - local distmax=0 - for _,_asset in pairs(assets) do - local asset=_asset --#AIRWING.SquadronAsset - - if asset.spawned then - local group=GROUP:FindByName(asset.spawngroupname) - --asset.dist=group:GetCoordinate():Get2DDistance(TargetCoordinate) - asset.dist=UTILS.VecDist2D(group:GetVec2(), TargetVec2) - else - asset.dist=dStock - end - - if asset.distdistmax then - distmax=asset.dist - end - - end - - -- Calculate the mission score of all assets. - for _,_asset in pairs(assets) do - local asset=_asset --#AIRWING.SquadronAsset - --self:I(string.format("FF asset %s has payload %s", asset.spawngroupname, asset.payload and "yes" or "no!")) - asset.score=self:CalculateAssetMissionScore(asset, Mission, includePayload) - end - - --- Sort assets wrt to their mission score. Higher is better. - local function optimize(a, b) - local assetA=a --#AIRWING.SquadronAsset - local assetB=b --#AIRWING.SquadronAsset - - -- Higher score wins. If equal score ==> closer wins. - -- TODO: Need to include the distance in a smarter way! - return (assetA.score>assetB.score) or (assetA.score==assetB.score and assetA.dist0 then - - --local text=string.format("Requesting assets for mission %s:", Mission.name) - for i,_asset in pairs(Assetlist) do - local asset=_asset --#AIRWING.SquadronAsset - - -- Set asset to requested! Important so that new requests do not use this asset! - asset.requested=true - - if Mission.missionTask then - asset.missionTask=Mission.missionTask - end - - end - - -- Add request to airwing warehouse. - -- TODO: better Assignment string. - self:AddRequest(self, WAREHOUSE.Descriptor.ASSETLIST, Assetlist, #Assetlist, nil, nil, Mission.prio, tostring(Mission.auftragsnummer)) - - -- The queueid has been increased in the onafterAddRequest function. So we can simply use it here. - Mission.requestID=self.queueid - end - -end - ---- On after "MissionCancel" event. Cancels the missions of all flightgroups. Deletes request from warehouse queue. --- @param #AIRWING self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. --- @param Ops.Auftrag#AUFTRAG Mission The mission to be cancelled. -function AIRWING:onafterMissionCancel(From, Event, To, Mission) - - -- Info message. - self:I(self.lid..string.format("Cancel mission %s", Mission.name)) - - local Ngroups = Mission:CountOpsGroups() - - if Mission:IsPlanned() or Mission:IsQueued() or Mission:IsRequested() or Ngroups == 0 then - - Mission:Done() - - else - - for _,_asset in pairs(Mission.assets) do - local asset=_asset --#AIRWING.SquadronAsset - - local flightgroup=asset.flightgroup - - if flightgroup then - flightgroup:MissionCancel(Mission) - end - - -- Not requested any more (if it was). - asset.requested=nil - end - - end - - -- Remove queued request (if any). - if Mission.requestID then - self:_DeleteQueueItemByID(Mission.requestID, self.queue) - end - -end - ---- On after "NewAsset" event. Asset is added to the given squadron (asset assignment). --- @param #AIRWING self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. --- @param #AIRWING.SquadronAsset asset The asset that has just been added. --- @param #string assignment The (optional) assignment for the asset. -function AIRWING:onafterNewAsset(From, Event, To, asset, assignment) - - -- Call parent warehouse function first. - self:GetParent(self).onafterNewAsset(self, From, Event, To, asset, assignment) - - -- Debug text. - local text=string.format("New asset %s with assignment %s and request assignment %s", asset.spawngroupname, tostring(asset.assignment), tostring(assignment)) - self:T3(self.lid..text) - - -- Get squadron. - local squad=self:GetSquadron(asset.assignment) - - -- Check if asset is already part of the squadron. If an asset returns, it will be added again! We check that asset.assignment is also assignment. - if squad then - - if asset.assignment==assignment then - - local nunits=#asset.template.units - - -- Debug text. - local text=string.format("Adding asset to squadron %s: assignment=%s, type=%s, attribute=%s, nunits=%d %s", squad.name, assignment, asset.unittype, asset.attribute, nunits, tostring(squad.ngrouping)) - self:T(self.lid..text) - - -- Adjust number of elements in the group. - if squad.ngrouping then - local template=asset.template - - local N=math.max(#template.units, squad.ngrouping) - - -- Handle units. - for i=1,N do - - -- Unit template. - local unit = template.units[i] - - -- If grouping is larger than units present, copy first unit. - if i>nunits then - table.insert(template.units, UTILS.DeepCopy(template.units[1])) - end - - -- Remove units if original template contains more than in grouping. - if squad.ngroupingnunits then - unit=nil - end - end - - asset.nunits=squad.ngrouping - end - - -- Set takeoff type. - asset.takeoffType=squad.takeoffType - - -- Create callsign and modex (needs to be after grouping). - squad:GetCallsign(asset) - squad:GetModex(asset) - - -- Set spawn group name. This has to include "AID-" for warehouse. - asset.spawngroupname=string.format("%s_AID-%d", squad.name, asset.uid) - - -- Add asset to squadron. - squad:AddAsset(asset) - - -- TODO - --asset.terminalType=AIRBASE.TerminalType.OpenBig - else - - --env.info("FF squad asset returned") - self:SquadAssetReturned(squad, asset) - - end - - end -end - ---- On after "AssetReturned" event. Triggered when an asset group returned to its airwing. --- @param #AIRWING self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. --- @param Ops.Squadron#SQUADRON Squadron The asset squadron. --- @param #AIRWING.SquadronAsset Asset The asset that returned. -function AIRWING:onafterSquadAssetReturned(From, Event, To, Squadron, Asset) - -- Debug message. - self:T(self.lid..string.format("Asset %s from squadron %s returned! asset.assignment=\"%s\"", Asset.spawngroupname, Squadron.name, tostring(Asset.assignment))) - - -- Stop flightgroup. - if Asset.flightgroup and not Asset.flightgroup:IsStopped() then - Asset.flightgroup:Stop() - end - - -- Return payload. - self:ReturnPayloadFromAsset(Asset) - - -- Return tacan channel. - if Asset.tacan then - Squadron:ReturnTacan(Asset.tacan) - end - - -- Set timestamp. - Asset.Treturned=timer.getAbsTime() -end - - ---- On after "AssetSpawned" event triggered when an asset group is spawned into the cruel world. --- Creates a new flightgroup element and adds the mission to the flightgroup queue. --- @param #AIRWING self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. --- @param Wrapper.Group#GROUP group The group spawned. --- @param #AIRWING.SquadronAsset asset The asset that was spawned. --- @param Functional.Warehouse#WAREHOUSE.Pendingitem request The request of the dead asset. -function AIRWING:onafterAssetSpawned(From, Event, To, group, asset, request) - - -- Call parent warehouse function first. - self:GetParent(self).onafterAssetSpawned(self, From, Event, To, group, asset, request) - - -- Get the SQUADRON of the asset. - local squadron=self:GetSquadronOfAsset(asset) - - -- Check if we have a squadron or if this was some other request. - if squadron then - - -- Create a flight group. - local flightgroup=self:_CreateFlightGroup(asset) - - --- - -- Asset - --- - - -- Set asset flightgroup. - asset.flightgroup=flightgroup - - -- Not requested any more. - asset.requested=nil - - -- Did not return yet. - asset.Treturned=nil - - --- - -- Squadron - --- - - -- Get TACAN channel. - local Tacan=squadron:FetchTacan() - if Tacan then - asset.tacan=Tacan - end - - -- Set radio frequency and modulation - local radioFreq, radioModu=squadron:GetRadio() - if radioFreq then - flightgroup:SwitchRadio(radioFreq, radioModu) - end - - if squadron.fuellow then - flightgroup:SetFuelLowThreshold(squadron.fuellow) - end - - if squadron.fuellowRefuel then - flightgroup:SetFuelLowRefuel(squadron.fuellowRefuel) - end - - --- - -- Mission - --- - - -- Get Mission (if any). - local mission=self:GetMissionByID(request.assignment) - - -- Add mission to flightgroup queue. - if mission then - - if Tacan then - mission:SetTACAN(Tacan, Morse, UnitName, Band) - end - - -- Add mission to flightgroup queue. - asset.flightgroup:AddMission(mission) - - -- Trigger event. - self:FlightOnMission(flightgroup, mission) - - else - - if Tacan then - flightgroup:SwitchTACAN(Tacan, Morse, UnitName, Band) - end - - end - - -- Add group to the detection set of the WINGCOMMANDER. - if self.wingcommander and self.wingcommander.chief then - self.wingcommander.chief.detectionset:AddGroup(asset.flightgroup.group) - end - - end - -end - ---- On after "AssetDead" event triggered when an asset group died. --- @param #AIRWING self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. --- @param #AIRWING.SquadronAsset asset The asset that is dead. --- @param Functional.Warehouse#WAREHOUSE.Pendingitem request The request of the dead asset. -function AIRWING:onafterAssetDead(From, Event, To, asset, request) - - -- Call parent warehouse function first. - self:GetParent(self).onafterAssetDead(self, From, Event, To, asset, request) - - -- Add group to the detection set of the WINGCOMMANDER. - if self.wingcommander and self.wingcommander.chief then - self.wingcommander.chief.detectionset:RemoveGroupsByName({asset.spawngroupname}) - end - - -- Remove asset from mission is done via Mission:AssetDead() call from flightgroup onafterFlightDead function - -- Remove asset from squadron same -end - ---- On after "Destroyed" event. Remove assets from squadrons. Stop squadrons. Remove airwing from wingcommander. --- @param #AIRWING self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. -function AIRWING:onafterDestroyed(From, Event, To) - - self:I(self.lid.."Airwing warehouse destroyed!") - - -- Cancel all missions. - for _,_mission in pairs(self.missionqueue) do - local mission=_mission --Ops.Auftrag#AUFTRAG - mission:Cancel() - end - - -- Remove all squadron assets. - for _,_squadron in pairs(self.squadrons) do - local squadron=_squadron --Ops.Squadron#SQUADRON - -- Stop Squadron. This also removes all assets. - squadron:Stop() - end - - -- Call parent warehouse function first. - self:GetParent(self).onafterDestroyed(self, From, Event, To) - -end - - ---- On after "Request" event. --- @param #AIRWING self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. --- @param Functional.Warehouse#WAREHOUSE.Queueitem Request Information table of the request. -function AIRWING:onafterRequest(From, Event, To, Request) - - -- Assets - local assets=Request.cargoassets - - -- Get Mission - local Mission=self:GetMissionByID(Request.assignment) - - if Mission and assets then - - for _,_asset in pairs(assets) do - local asset=_asset --#AIRWING.SquadronAsset - -- This would be the place to modify the asset table before the asset is spawned. - end - - end - - -- Call parent warehouse function after assets have been adjusted. - self:GetParent(self).onafterRequest(self, From, Event, To, Request) - -end - ---- On after "SelfRequest" event. --- @param #AIRWING self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. --- @param Core.Set#SET_GROUP groupset The set of asset groups that was delivered to the warehouse itself. --- @param Functional.Warehouse#WAREHOUSE.Pendingitem request Pending self request. -function AIRWING:onafterSelfRequest(From, Event, To, groupset, request) - - -- Call parent warehouse function first. - self:GetParent(self).onafterSelfRequest(self, From, Event, To, groupset, request) - - -- Get Mission - local mission=self:GetMissionByID(request.assignment) - - for _,_asset in pairs(request.assets) do - local asset=_asset --#AIRWING.SquadronAsset - end - - for _,_group in pairs(groupset:GetSet()) do - local group=_group --Wrapper.Group#GROUP - end - +function AIRWING:onafterFlightOnMission(From, Event, To, FlightGroup, Mission) + -- Debug info. + self:T(self.lid..string.format("Group %s on %s mission %s", FlightGroup:GetName(), Mission:GetType(), Mission:GetName())) + if self.UseConnectedOpsAwacs and self.ConnectedOpsAwacs then + self.ConnectedOpsAwacs:__FlightOnMission(2,FlightGroup,Mission) + end end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Misc Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Create a new flight group after an asset was spawned. --- @param #AIRWING self --- @param #AIRWING.SquadronAsset asset The asset. --- @return Ops.FlightGroup#FLIGHTGROUP The created flightgroup object. -function AIRWING:_CreateFlightGroup(asset) - - -- Create flightgroup. - local flightgroup=FLIGHTGROUP:New(asset.spawngroupname) - - -- Set airwing. - flightgroup:SetAirwing(self) - - -- Set squadron. - flightgroup.squadron=self:GetSquadronOfAsset(asset) - - -- Set home base. - flightgroup.homebase=self.airbase - - return flightgroup -end - - ---- Check if an asset is currently on a mission (STARTED or EXECUTING). --- @param #AIRWING self --- @param #AIRWING.SquadronAsset asset The asset. --- @param #table MissionTypes Types on mission to be checked. Default all. --- @return #boolean If true, asset has at least one mission of that type in the queue. -function AIRWING:IsAssetOnMission(asset, MissionTypes) - - if MissionTypes then - if type(MissionTypes)~="table" then - MissionTypes={MissionTypes} - end - else - -- Check all possible types. - MissionTypes=AUFTRAG.Type - end - - if asset.flightgroup and asset.flightgroup:IsAlive() then - - -- Loop over mission queue. - for _,_mission in pairs(asset.flightgroup.missionqueue or {}) do - local mission=_mission --Ops.Auftrag#AUFTRAG - - if mission:IsNotOver() then - - -- Get flight status. - local status=mission:GetGroupStatus(asset.flightgroup) - - -- Only if mission is started or executing. - if (status==AUFTRAG.GroupStatus.STARTED or status==AUFTRAG.GroupStatus.EXECUTING) and self:CheckMissionType(mission.type, MissionTypes) then - return true - end - - end - - end - - end - - -- Alternative: run over all missions and compare to mission assets. - --[[ - for _,_mission in pairs(self.missionqueue) do - local mission=_mission --Ops.Auftrag#AUFTRAG - - if mission:IsNotOver() then - for _,_asset in pairs(mission.assets) do - local sqasset=_asset --#AIRWING.SquadronAsset - - if sqasset.uid==asset.uid then - return true - end - - end - end - - end - ]] - - return false -end - ---- Get the current mission of the asset. --- @param #AIRWING self --- @param #AIRWING.SquadronAsset asset The asset. --- @return Ops.Auftrag#AUFTRAG Current mission or *nil*. -function AIRWING:GetAssetCurrentMission(asset) - - if asset.flightgroup then - return asset.flightgroup:GetMissionCurrent() - end - - return nil -end - --- Count payloads in stock. -- @param #AIRWING self -- @param #table MissionTypes Types on mission to be checked. Default *all* possible types `AUFTRAG.Type`. @@ -141870,7 +163795,7 @@ function AIRWING:CountPayloadsInStock(MissionTypes, UnitTypes, Payloads) UnitTypes={UnitTypes} end end - + local function _checkUnitTypes(payload) if UnitTypes then for _,unittype in pairs(UnitTypes) do @@ -141884,7 +163809,7 @@ function AIRWING:CountPayloadsInStock(MissionTypes, UnitTypes, Payloads) end return false end - + local function _checkPayloads(payload) if Payloads then for _,Payload in pairs(Payloads) do @@ -141897,280 +163822,36 @@ function AIRWING:CountPayloadsInStock(MissionTypes, UnitTypes, Payloads) return nil end return false - end + end local n=0 for _,_payload in pairs(self.payloads) do local payload=_payload --#AIRWING.Payload - + for _,MissionType in pairs(MissionTypes) do - + local specialpayload=_checkPayloads(payload) - local compatible=self:CheckMissionCapability(MissionType, payload.capabilities) - + local compatible=AUFTRAG.CheckMissionCapability(MissionType, payload.capabilities) + local goforit = specialpayload or (specialpayload==nil and compatible) - + if goforit and _checkUnitTypes(payload) then - + if payload.unlimited then -- Payload is unlimited. Return a BIG number. return 999 else n=n+payload.navail end - + end - + end end return n end ---- Count missions in mission queue. --- @param #AIRWING self --- @param #table MissionTypes Types on mission to be checked. Default *all* possible types `AUFTRAG.Type`. --- @return #number Number of missions that are not over yet. -function AIRWING:CountMissionsInQueue(MissionTypes) - - MissionTypes=MissionTypes or AUFTRAG.Type - - local N=0 - for _,_mission in pairs(self.missionqueue) do - local mission=_mission --Ops.Auftrag#AUFTRAG - - -- Check if this mission type is requested. - if mission:IsNotOver() and self:CheckMissionType(mission.type, MissionTypes) then - N=N+1 - end - - end - - return N -end - ---- Count total number of assets. This is the sum of all squadron assets. --- @param #AIRWING self --- @return #number Amount of asset groups. -function AIRWING:CountAssets() - - local N=0 - - for _,_squad in pairs(self.squadrons) do - local squad=_squad --Ops.Squadron#SQUADRON - N=N+#squad.assets - end - - return N -end - ---- Count assets on mission. --- @param #AIRWING self --- @param #table MissionTypes Types on mission to be checked. Default all. --- @param Ops.Squadron#SQUADRON Squadron Only count assets of this squadron. Default count assets of all squadrons. --- @return #number Number of pending and queued assets. --- @return #number Number of pending assets. --- @return #number Number of queued assets. -function AIRWING:CountAssetsOnMission(MissionTypes, Squadron) - - local Nq=0 - local Np=0 - - for _,_mission in pairs(self.missionqueue) do - local mission=_mission --Ops.Auftrag#AUFTRAG - - -- Check if this mission type is requested. - if self:CheckMissionType(mission.type, MissionTypes or AUFTRAG.Type) then - - for _,_asset in pairs(mission.assets or {}) do - local asset=_asset --#AIRWING.SquadronAsset - - if Squadron==nil or Squadron.name==asset.squadname then - - local request, isqueued=self:GetRequestByID(mission.requestID) - - if isqueued then - Nq=Nq+1 - else - Np=Np+1 - end - - end - - end - end - end - - --env.info(string.format("FF N=%d Np=%d, Nq=%d", Np+Nq, Np, Nq)) - return Np+Nq, Np, Nq -end - ---- Count assets on mission. --- @param #AIRWING self --- @param #table MissionTypes Types on mission to be checked. Default all. --- @return #table Assets on pending requests. -function AIRWING:GetAssetsOnMission(MissionTypes) - - local assets={} - local Np=0 - - for _,_mission in pairs(self.missionqueue) do - local mission=_mission --Ops.Auftrag#AUFTRAG - - -- Check if this mission type is requested. - if self:CheckMissionType(mission.type, MissionTypes) then - - for _,_asset in pairs(mission.assets or {}) do - local asset=_asset --#AIRWING.SquadronAsset - - table.insert(assets, asset) - - end - end - end - - return assets -end - ---- Get the aircraft types of this airwing. --- @param #AIRWING self --- @param #boolean onlyactive Count only the active ones. --- @param #table squadrons Table of squadrons. Default all. --- @return #table Table of unit types. -function AIRWING:GetAircraftTypes(onlyactive, squadrons) - - -- Get all unit types that can do the job. - local unittypes={} - - -- Loop over all squadrons. - for _,_squadron in pairs(squadrons or self.squadrons) do - local squadron=_squadron --Ops.Squadron#SQUADRON - - if (not onlyactive) or squadron:IsOnDuty() then - - local gotit=false - for _,unittype in pairs(unittypes) do - if squadron.aircrafttype==unittype then - gotit=true - break - end - end - if not gotit then - table.insert(unittypes, squadron.aircrafttype) - end - - end - end - - return unittypes -end - ---- Check if assets for a given mission type are available. --- @param #AIRWING self --- @param Ops.Auftrag#AUFTRAG Mission The mission. --- @return #boolean If true, enough assets are available. --- @return #table Assets that can do the required mission. -function AIRWING:CanMission(Mission) - - -- Assume we CAN and NO assets are available. - local Can=true - local Assets={} - - -- Squadrons for the job. If user assigned to mission or simply all. - local squadrons=Mission.squadrons or self.squadrons - - -- Get aircraft unit types for the job. - local unittypes=self:GetAircraftTypes(true, squadrons) - - -- Count all payloads in stock. - local Npayloads=self:CountPayloadsInStock(Mission.type, unittypes, Mission.payloads) - - if Npayloads #Assets then - self:T(self.lid..string.format("INFO: Not enough assets available! Got %d but need at least %d", #Assets, Mission.nassets)) - Can=false - end - - return Can, Assets -end - ---- Check if assets for a given mission type are available. --- @param #AIRWING self --- @param Ops.Auftrag#AUFTRAG Mission The mission. --- @return #table Assets that can do the required mission. -function AIRWING:RecruitAssets(Mission) - -end - - ---- Check if a mission type is contained in a list of possible types. --- @param #AIRWING self --- @param #string MissionType The requested mission type. --- @param #table PossibleTypes A table with possible mission types. --- @return #boolean If true, the requested mission type is part of the possible mission types. -function AIRWING:CheckMissionType(MissionType, PossibleTypes) - - if type(PossibleTypes)=="string" then - PossibleTypes={PossibleTypes} - end - - for _,canmission in pairs(PossibleTypes) do - if canmission==MissionType then - return true - end - end - - return false -end - ---- Check if a mission type is contained in a list of possible capabilities. --- @param #AIRWING self --- @param #string MissionType The requested mission type. --- @param #table Capabilities A table with possible capabilities. --- @return #boolean If true, the requested mission type is part of the possible mission types. -function AIRWING:CheckMissionCapability(MissionType, Capabilities) - - for _,cap in pairs(Capabilities) do - local capability=cap --Ops.Auftrag#AUFTRAG.Capability - if capability.MissionType==MissionType then - return true - end - end - - return false -end - --- Get payload performance for a given type of misson type. -- @param #AIRWING self -- @param #AIRWING.Payload Payload The payload table. @@ -142179,7 +163860,7 @@ end function AIRWING:GetPayloadPeformance(Payload, MissionType) if Payload then - + for _,Capability in pairs(Payload.capabilities) do local capability=Capability --Ops.Auftrag#AUFTRAG.Capability if capability.MissionType==MissionType then @@ -142201,7 +163882,7 @@ end function AIRWING:GetPayloadMissionTypes(Payload) local missiontypes={} - + for _,Capability in pairs(Payload.capabilities) do local capability=Capability --Ops.Auftrag#AUFTRAG.Capability table.insert(missiontypes, capability.MissionType) @@ -142210,44 +163891,612 @@ function AIRWING:GetPayloadMissionTypes(Payload) return missiontypes end ---- Returns the mission for a given mission ID (Autragsnummer). --- @param #AIRWING self --- @param #number mid Mission ID (Auftragsnummer). --- @return Ops.Auftrag#AUFTRAG Mission table. -function AIRWING:GetMissionByID(mid) +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- **Ops** - Brigade Warehouse. +-- +-- **Main Features:** +-- +-- * Manage platoons +-- * Carry out ARTY and PATROLZONE missions (AUFTRAG) +-- * Define rearming zones +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20Brigade). +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module Ops.Brigade +-- @image OPS_Brigade_.png - for _,_mission in pairs(self.missionqueue) do - local mission=_mission --Ops.Auftrag#AUFTRAG - - if mission.auftragsnummer==tonumber(mid) then - return mission - end - + +--- BRIGADE class. +-- @type BRIGADE +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity of output. +-- @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 Core.Set#SET_ZONE retreatZones Retreat zone set. +-- @extends Ops.Legion#LEGION + +--- *I am not afraid of an Army of lions lead by a sheep; I am afraid of sheep lead by a lion* -- Alexander the Great +-- +-- === +-- +-- # The BRIGADE Concept +-- +-- A BRIGADE consists of one or multiple PLATOONs. These platoons "live" in a WAREHOUSE that has a phyiscal struction (STATIC or UNIT) and can be captured or destroyed. +-- +-- +-- @field #BRIGADE +BRIGADE = { + ClassName = "BRIGADE", + verbose = 0, + rearmingZones = {}, + refuellingZones = {}, +} + +--- Supply Zone. +-- @type BRIGADE.SupplyZone +-- @field Core.Zone#ZONE zone The zone. +-- @field Ops.Auftrag#AUFTRAG mission Mission assigned to supply ammo or fuel. +-- @field #boolean markerOn If `true`, marker is on. +-- @field Wrapper.Marker#MARKER marker F10 marker. + +--- BRIGADE class version. +-- @field #string version +BRIGADE.version="0.1.1" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Spawn when hosting warehouse is a ship or oil rig or gas platform. +-- TODO: Rearming zones. +-- TODO: Retreat zones. +-- DONE: Add weapon range. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new BRIGADE class object. +-- @param #BRIGADE self +-- @param #string WarehouseName Name of the warehouse STATIC or UNIT object representing the warehouse. +-- @param #string BrigadeName Name of the brigade. +-- @return #BRIGADE self +function BRIGADE:New(WarehouseName, BrigadeName) + + -- Inherit everything from LEGION class. + local self=BASE:Inherit(self, LEGION:New(WarehouseName, BrigadeName)) -- #BRIGADE + + -- Nil check. + if not self then + BASE:E(string.format("ERROR: Could not find warehouse %s!", WarehouseName)) + return nil + end + + -- Set some string id for output to DCS.log file. + self.lid=string.format("BRIGADE %s | ", self.alias) + + -- 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 - return nil + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("*", "ArmyOnMission", "*") -- An ARMYGROUP was send on a Mission (AUFTRAG). + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the BRIGADE. Initializes parameters and starts event handlers. + -- @function [parent=#BRIGADE] Start + -- @param #BRIGADE self + + --- Triggers the FSM event "Start" after a delay. Starts the BRIGADE. Initializes parameters and starts event handlers. + -- @function [parent=#BRIGADE] __Start + -- @param #BRIGADE self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Stop". Stops the BRIGADE and all its event handlers. + -- @param #BRIGADE self + + --- Triggers the FSM event "Stop" after a delay. Stops the BRIGADE and all its event handlers. + -- @function [parent=#BRIGADE] __Stop + -- @param #BRIGADE self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "ArmyOnMission". + -- @function [parent=#BRIGADE] ArmyOnMission + -- @param #BRIGADE self + -- @param Ops.ArmyGroup#ARMYGROUP ArmyGroup The ARMYGROUP on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "ArmyOnMission" after a delay. + -- @function [parent=#BRIGADE] __ArmyOnMission + -- @param #BRIGADE self + -- @param #number delay Delay in seconds. + -- @param Ops.ArmyGroup#ARMYGROUP ArmyGroup The ARMYGROUP on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "ArmyOnMission" event. + -- @function [parent=#BRIGADE] OnAfterArmyOnMission + -- @param #BRIGADE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.ArmyGroup#ARMYGROUP ArmyGroup The ARMYGROUP on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + return self end ---- Returns the mission for a given request ID. --- @param #AIRWING self --- @param #number RequestID Unique ID of the request. --- @return Ops.Auftrag#AUFTRAG Mission table or *nil*. -function AIRWING:GetMissionFromRequestID(RequestID) - for _,_mission in pairs(self.missionqueue) do - local mission=_mission --Ops.Auftrag#AUFTRAG - if mission.requestID and mission.requestID==RequestID then - return mission +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add a platoon to the brigade. +-- @param #BRIGADE self +-- @param Ops.Platoon#PLATOON Platoon The platoon object. +-- @return #BRIGADE self +function BRIGADE:AddPlatoon(Platoon) + + -- Add platoon to brigade. + table.insert(self.cohorts, Platoon) + + -- Add assets to platoon. + self:AddAssetToPlatoon(Platoon, Platoon.Ngroups) + + -- Set brigade of platoon. + Platoon:SetBrigade(self) + + -- Start platoon. + if Platoon:IsStopped() then + Platoon:Start() + end + + return self +end + +--- Add asset group(s) to platoon. +-- @param #BRIGADE self +-- @param Ops.Platoon#PLATOON Platoon The platoon object. +-- @param #number Nassets Number of asset groups to add. +-- @return #BRIGADE self +function BRIGADE:AddAssetToPlatoon(Platoon, Nassets) + + if Platoon then + + -- Get the template group of the platoon. + local Group=GROUP:FindByName(Platoon.templatename) + + if Group then + + -- Debug text. + local text=string.format("Adding asset %s to platoon %s", Group:GetName(), Platoon.name) + self:T(self.lid..text) + + -- Add assets to airwing warehouse. + self:AddAsset(Group, Nassets, nil, nil, nil, nil, Platoon.skill, Platoon.livery, Platoon.name) + + else + self:E(self.lid.."ERROR: Group does not exist!") + end + + else + self:E(self.lid.."ERROR: Platoon does not exit!") + end + + return self +end + +--- Define a set of retreat zones. +-- @param #BRIGADE self +-- @param Core.Set#SET_ZONE RetreatZoneSet Set of retreat zones. +-- @return #BRIGADE self +function BRIGADE:SetRetreatZones(RetreatZoneSet) + self.retreatZones=RetreatZoneSet or SET_ZONE:New() + return self +end + +--- Add a retreat zone. +-- @param #BRIGADE self +-- @param Core.Zone#ZONE RetreatZone Retreat zone. +-- @return #BRIGADE self +function BRIGADE:AddRetreatZone(RetreatZone) + self.retreatZones:AddZone(RetreatZone) + return self +end + +--- Get retreat zones. +-- @param #BRIGADE self +-- @return Core.Set#SET_ZONE Set of retreat zones. +function BRIGADE:GetRetreatZones() + return self.retreatZones +end + +--- Add a rearming zone. +-- @param #BRIGADE self +-- @param Core.Zone#ZONE RearmingZone Rearming zone. +-- @return #BRIGADE.SupplyZone The rearming zone data. +function BRIGADE:AddRearmingZone(RearmingZone) + + local rearmingzone={} --#BRIGADE.SupplyZone + + rearmingzone.zone=RearmingZone + rearmingzone.mission=nil + rearmingzone.marker=MARKER:New(rearmingzone.zone:GetCoordinate(), "Rearming Zone"):ToCoalition(self:GetCoalition()) + + table.insert(self.rearmingZones, rearmingzone) + + return rearmingzone +end + + +--- Add a refuelling zone. +-- @param #BRIGADE self +-- @param Core.Zone#ZONE RefuellingZone Refuelling zone. +-- @return #BRIGADE.SupplyZone The refuelling zone data. +function BRIGADE:AddRefuellingZone(RefuellingZone) + + local supplyzone={} --#BRIGADE.SupplyZone + + supplyzone.zone=RefuellingZone + supplyzone.mission=nil + supplyzone.marker=MARKER:New(supplyzone.zone:GetCoordinate(), "Refuelling Zone"):ToCoalition(self:GetCoalition()) + + table.insert(self.refuellingZones, supplyzone) + + return supplyzone +end + + +--- Get platoon by name. +-- @param #BRIGADE self +-- @param #string PlatoonName Name of the platoon. +-- @return Ops.Platoon#PLATOON The Platoon object. +function BRIGADE:GetPlatoon(PlatoonName) + local platoon=self:_GetCohort(PlatoonName) + return platoon +end + +--- Get platoon of an asset. +-- @param #BRIGADE self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The platoon asset. +-- @return Ops.Platoon#PLATOON The platoon object. +function BRIGADE:GetPlatoonOfAsset(Asset) + local platoon=self:GetPlatoon(Asset.squadname) + return platoon +end + +--- Remove asset from platoon. +-- @param #BRIGADE self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The platoon asset. +function BRIGADE:RemoveAssetFromPlatoon(Asset) + local platoon=self:GetPlatoonOfAsset(Asset) + if platoon then + platoon:DelAsset(Asset) + end +end + + +--- [ GROUND ] Function to load back an asset in the field that has been filed before. +-- @param #BRIGADE self +-- @param #string Templatename e.g."1 PzDv LogRg I\_AID-976" - that's the alias (name) of an platoon spawned as `"platoon - alias"_AID-"asset-ID"` +-- @param Core.Point#COORDINATE Position where to spawn the platoon +-- @return #BRIGADE self +-- @usage +-- Prerequisites: +-- Save the assets spawned by BRIGADE/CHIEF regularly (~every 5 mins) into a file, e.g. like this: +-- +-- local Path = FilePath or "C:\\Users\\\\Saved Games\\DCS\\Missions\\" -- example path +-- local BlueOpsFilename = BlueFileName or "ExamplePlatoonSave.csv" -- example filename +-- local BlueSaveOps = SET_GROUP:New():FilterCoalitions("blue"):FilterPrefixes("AID"):FilterCategoryGround():FilterOnce() +-- UTILS.SaveSetOfGroups(BlueSaveOps,Path,BlueOpsFilename) +-- +-- where Path and Filename are strings, as chosen by you. +-- You can then load back the assets at the start of your next mission run. Be aware that it takes a couple of seconds for the +-- platoon data to arrive in brigade, so make this an action after ~20 seconds, e.g. like so: +-- +-- function LoadBackAssets() +-- local Path = FilePath or "C:\\Users\\\\Saved Games\\DCS\\Missions\\" -- example path +-- local BlueOpsFilename = BlueFileName or "ExamplePlatoonSave.csv" -- example filename +-- if UTILS.CheckFileExists(Path,BlueOpsFilename) then +-- local loadback = UTILS.LoadSetOfGroups(Path,BlueOpsFilename,false) +-- for _,_platoondata in pairs (loadback) do +-- local groupname = _platoondata.groupname -- #string +-- local coordinate = _platoondata.coordinate -- Core.Point#COORDINATE +-- Your_Brigade:LoadBackAssetInPosition(groupname,coordinate) +-- end +-- end +-- end +-- +-- local AssetLoader = TIMER:New(LoadBackAssets) +-- AssetLoader:Start(20) +-- +-- The assets loaded back into the mission will be considered for AUFTRAG type missions from CHIEF and BRIGADE. +function BRIGADE:LoadBackAssetInPosition(Templatename,Position) + self:T(self.lid .. "LoadBackAssetInPosition: " .. tostring(Templatename)) + + -- get Platoon alias from Templatename + local nametbl = UTILS.Split(Templatename,"_") + + local name = nametbl[1] + + self:T(string.format("*** Target Platoon = %s ***",name)) + + -- find a matching asset table from BRIGADE + local cohorts = self.cohorts or {} + local thisasset = nil --Functional.Warehouse#WAREHOUSE.Assetitem + local found = false + + for _,_cohort in pairs(cohorts) do + local asset = _cohort:GetName() + self:T(string.format("*** Looking at Platoon = %s ***",asset)) + if asset == name then + self:T("**** Found Platoon ****") + local cohassets = _cohort.assets or {} + for _,_zug in pairs (cohassets) do + local zug = _zug -- Functional.Warehouse#WAREHOUSE.Assetitem + if zug.assignment == name and zug.requested == false then + self:T("**** Found Asset ****") + found = true + thisasset = zug --Functional.Warehouse#WAREHOUSE.Assetitem + break + end + end end end - return nil + + if found then + + -- prep asset + thisasset.rid = thisasset.uid + thisasset.requested = false + thisasset.score=100 + thisasset.missionTask="CAS" + thisasset.spawned = true + local template = thisasset.templatename + local alias = thisasset.spawngroupname + + -- Spawn group + local spawnasset = SPAWN:NewWithAlias(template,alias) + :InitDelayOff() + :SpawnFromCoordinate(Position) + + -- build a new self request + local request = {} --Functional.Warehouse#WAREHOUSE.Pendingitem + request.assignment = name + request.warehouse = self + request.assets = {thisasset} + request.ntransporthome = 0 + request.ndelivered = 0 + request.ntransport = 0 + request.cargoattribute = thisasset.attribute + request.category = thisasset.category + request.cargoassets = {thisasset} + request.assetdesc = WAREHOUSE.Descriptor.ASSETLIST + request.cargocategory = thisasset.category + request.toself = true + request.transporttype = WAREHOUSE.TransportType.SELFPROPELLED + request.assetproblem = {} + request.born = true + request.prio = 50 + request.uid = thisasset.uid + request.airbase = nil + request.timestamp = timer.getAbsTime() + request.assetdescval = {thisasset} + request.nasset = 1 + request.cargogroupset = SET_GROUP:New() + request.cargogroupset:AddGroup(spawnasset) + request.iscargo = true + + -- Call Brigade self + self:__AssetSpawned(2, spawnasset, thisasset, request) + + end + return self end ---- Returns the mission for a given request. --- @param #AIRWING self --- @param Functional.Warehouse#WAREHOUSE.Queueitem Request The warehouse request. --- @return Ops.Auftrag#AUFTRAG Mission table or *nil*. -function AIRWING:GetMissionFromRequest(Request) - return self:GetMissionFromRequestID(Request.uid) + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Start BRIGADE FSM. +-- @param #BRIGADE self +function BRIGADE:onafterStart(From, Event, To) + + -- Start parent Warehouse. + self:GetParent(self, BRIGADE).onafterStart(self, From, Event, To) + + -- Info. + self:I(self.lid..string.format("Starting BRIGADE v%s", BRIGADE.version)) + +end + +--- Update status. +-- @param #BRIGADE self +function BRIGADE:onafterStatus(From, Event, To) + + -- Status of parent Warehouse. + self:GetParent(self).onafterStatus(self, From, Event, To) + + -- FSM state. + local fsmstate=self:GetState() + + ---------------- + -- Transport --- + ---------------- + + self:CheckTransportQueue() + + -------------- + -- Mission --- + -------------- + + -- Check if any missions should be cancelled. + self:CheckMissionQueue() + + --------------------- + -- Rearming Zones --- + --------------------- + + for _,_rearmingzone in pairs(self.rearmingZones) do + local rearmingzone=_rearmingzone --#BRIGADE.SupplyZone + if (not rearmingzone.mission) or rearmingzone.mission:IsOver() then + rearmingzone.mission=AUFTRAG:NewAMMOSUPPLY(rearmingzone.zone) + self:AddMission(rearmingzone.mission) + end + end + + ----------------------- + -- Refuelling Zones --- + ----------------------- + + -- Check refuelling zones. + for _,_supplyzone in pairs(self.refuellingZones) do + local supplyzone=_supplyzone --#BRIGADE.SupplyZone + -- Check if mission is nil or over. + if (not supplyzone.mission) or supplyzone.mission:IsOver() then + supplyzone.mission=AUFTRAG:NewFUELSUPPLY(supplyzone.zone) + self:AddMission(supplyzone.mission) + end + end + + + ----------- + -- Info --- + ----------- + + -- General info: + if self.verbose>=1 then + + -- Count missions not over yet. + local Nmissions=self:CountMissionsInQueue() + + -- Asset count. + local Npq, Np, Nq=self:CountAssetsOnMission() + + -- Asset string. + local assets=string.format("%d [OnMission: Total=%d, Active=%d, Queued=%d]", self:CountAssets(), Npq, Np, Nq) + + -- Output. + local text=string.format("%s: Missions=%d, Platoons=%d, Assets=%s", fsmstate, Nmissions, #self.cohorts, assets) + self:I(self.lid..text) + end + + ------------------ + -- Mission Info -- + ------------------ + if self.verbose>=2 then + local text=string.format("Missions Total=%d:", #self.missionqueue) + for i,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + local prio=string.format("%d/%s", mission.prio, tostring(mission.importance)) ; if mission.urgent then prio=prio.." (!)" end + local assets=string.format("%d/%d", mission:CountOpsGroups(), mission.Nassets or 0) + local target=string.format("%d/%d Damage=%.1f", mission:CountMissionTargets(), mission:GetTargetInitialNumber(), mission:GetTargetDamage()) + + text=text..string.format("\n[%d] %s %s: Status=%s, Prio=%s, Assets=%s, Targets=%s", i, mission.name, mission.type, mission.status, prio, assets, target) + end + self:I(self.lid..text) + end + + -------------------- + -- Transport Info -- + -------------------- + if self.verbose>=2 then + local text=string.format("Transports Total=%d:", #self.transportqueue) + for i,_transport in pairs(self.transportqueue) do + local transport=_transport --Ops.OpsTransport#OPSTRANSPORT + + local prio=string.format("%d/%s", transport.prio, tostring(transport.importance)) ; if transport.urgent then prio=prio.." (!)" end + local carriers=string.format("Ncargo=%d/%d, Ncarriers=%d", transport.Ncargo, transport.Ndelivered, transport.Ncarrier) + + text=text..string.format("\n[%d] UID=%d: Status=%s, Prio=%s, Cargo: %s", i, transport.uid, transport:GetState(), prio, carriers) + end + self:I(self.lid..text) + end + + ------------------- + -- Platoon Info -- + ------------------- + if self.verbose>=3 then + local text="Platoons:" + for i,_platoon in pairs(self.cohorts) do + local platoon=_platoon --Ops.Platoon#PLATOON + + local callsign=platoon.callsignName and UTILS.GetCallsignName(platoon.callsignName) or "N/A" + local modex=platoon.modex and platoon.modex or -1 + local skill=platoon.skill and tostring(platoon.skill) or "N/A" + + -- Platoon text. + text=text..string.format("\n* %s %s: %s*%d/%d, Callsign=%s, Modex=%d, Skill=%s", platoon.name, platoon:GetState(), platoon.aircrafttype, platoon:CountAssets(true), #platoon.assets, callsign, modex, skill) + end + self:I(self.lid..text) + end + + ------------------- + -- Rearming Info -- + ------------------- + if self.verbose>=4 then + local text="Rearming Zones:" + for i,_rearmingzone in pairs(self.rearmingZones) do + local rearmingzone=_rearmingzone --#BRIGADE.SupplyZone + -- Info text. + text=text..string.format("\n* %s: Mission status=%s, suppliers=%d", rearmingzone.zone:GetName(), rearmingzone.mission:GetState(), rearmingzone.mission:CountOpsGroups()) + end + self:I(self.lid..text) + end + + ------------------- + -- Refuelling Info -- + ------------------- + if self.verbose>=4 then + local text="Refuelling Zones:" + for i,_refuellingzone in pairs(self.refuellingZones) do + local refuellingzone=_refuellingzone --#BRIGADE.SupplyZone + -- Info text. + text=text..string.format("\n* %s: Mission status=%s, suppliers=%d", refuellingzone.zone:GetName(), refuellingzone.mission:GetState(), refuellingzone.mission:CountOpsGroups()) + end + self:I(self.lid..text) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "ArmyOnMission". +-- @param #BRIGADE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.ArmyGroup#ARMYGROUP ArmyGroup Ops army group on mission. +-- @param Ops.Auftrag#AUFTRAG Mission The requested mission. +function BRIGADE:onafterArmyOnMission(From, Event, To, ArmyGroup, Mission) + -- Debug info. + self:T(self.lid..string.format("Group %s on %s mission %s", ArmyGroup:GetName(), Mission:GetType(), Mission:GetName())) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -142260,7 +164509,7 @@ end -- * Detect and track contacts consistently -- * Detect and track clusters of contacts consistently -- * Use FSM events to link functionality into your scripts --- * Easy setup +-- * Easy setup -- -- === -- @@ -142286,56 +164535,56 @@ end -- @field #table ContactsUnknown Table of new detected items. -- @field #table Clusters Clusters of detected groups. -- @field #boolean clusteranalysis If true, create clusters of detected targets. --- @field #boolean clustermarkers If true, create cluster markers on F10 map. +-- @field #boolean clustermarkers If true, create cluster markers on F10 map. -- @field #number clustercounter Running number of clusters. -- @field #number dTforget Time interval in seconds before a known contact which is not detected any more is forgotten. --- @field #number clusterradius Radius im kilometers in which groups/units are considered to belong to a cluster +-- @field #number clusterradius Radius in meters in which groups/units are considered to belong to a cluster. -- @field #number prediction Seconds default to be used with CalcClusterFuturePosition. +-- @field #boolean detectStatics If `true`, detect STATIC objects. Default `false`. +-- @field #number statusupdate Time interval in seconds after which the status is refreshed. Default 60 sec. Should be negative. -- @extends Core.Fsm#FSM --- Top Secret! -- -- === -- --- ![Banner Image](..\Presentations\CarrierAirWing\INTEL_Main.jpg) --- -- # The INTEL Concept --- +-- -- * Lightweight replacement for @{Functional.Detection#DETECTION} -- * Detect and track contacts consistently -- * Detect and track clusters of contacts consistently -- * Once detected and still alive, planes will be tracked 10 minutes, helicopters 20 minutes, ships and trains 1 hour, ground units 2 hours -- * Use FSM events to link functionality into your scripts --- --- # Basic Usage -- --- ## set up a detection SET_GROUP --- --- `Red_DetectionSetGroup = SET_GROUP:New()` --- `Red_DetectionSetGroup:FilterPrefixes( { "Red EWR" } )` --- `Red_DetectionSetGroup:FilterOnce()` --- --- ## New Intel type detection for the red side, logname "KGB" --- --- `RedIntel = INTEL:New(Red_DetectionSetGroup,"red","KGB")` --- `RedIntel:SetClusterAnalysis(true,true)` --- `RedIntel:SetVerbosity(2)` --- `RedIntel:__Start(2)` --- --- ## Hook into new contacts found --- --- `function RedIntel:OnAfterNewContact(From, Event, To, Contact)` --- `local text = string.format("NEW contact %s detected by %s", Contact.groupname, Contact.recce or "unknown")` --- `local m = MESSAGE:New(text,15,"KGB"):ToAll()` --- `end` --- --- ## And/or new clusters found --- --- `function RedIntel:OnAfterNewCluster(From, Event, To, Contact, Cluster)` --- `local text = string.format("NEW cluster %d size %d with contact %s", Cluster.index, Cluster.size, Contact.groupname)` --- `local m = MESSAGE:New(text,15,"KGB"):ToAll()` --- `end` --- +-- # Basic Usage +-- +-- ## Set up a detection SET_GROUP +-- +-- Red_DetectionSetGroup = SET_GROUP:New() +-- Red_DetectionSetGroup:FilterPrefixes( { "Red EWR" } ) +-- Red_DetectionSetGroup:FilterOnce() +-- +-- ## New Intel type detection for the red side, logname "KGB" +-- +-- RedIntel = INTEL:New(Red_DetectionSetGroup, "red", "KGB") +-- RedIntel:SetClusterAnalysis(true, true) +-- RedIntel:SetVerbosity(2) +-- RedIntel:__Start(2) +-- +-- ## Hook into new contacts found +-- +-- function RedIntel:OnAfterNewContact(From, Event, To, Contact) +-- local text = string.format("NEW contact %s detected by %s", Contact.groupname, Contact.recce or "unknown") +-- MESSAGE:New(text, 15, "KGB"):ToAll() +-- end +-- +-- ## And/or new clusters found +-- +-- function RedIntel:OnAfterNewCluster(From, Event, To, Cluster) +-- local text = string.format("NEW cluster #%d of size %d", Cluster.index, Cluster.size) +-- MESSAGE:New(text,15,"KGB"):ToAll() +-- end +-- -- -- @field #INTEL INTEL = { @@ -142350,10 +164599,12 @@ INTEL = { ContactsUnknown = {}, Clusters = {}, clustercounter = 1, - clusterradius = 15, - clusteranalysis = true, - clustermarkers = false, + clusterradius = 15000, + clusteranalysis = true, + clustermarkers = false, + clusterarrows = false, prediction = 300, + detectStatics = false, } --- Detected item info. @@ -142369,11 +164620,18 @@ INTEL = { -- @field Core.Point#COORDINATE position Last known position of the item. -- @field DCS#Vec3 velocity 3D velocity vector. Components x,y and z in m/s. -- @field #number speed Last known speed in m/s. --- @field #boolean isship --- @field #boolean ishelo --- @field #boolean isground --- @field Ops.Auftrag#AUFTRAG mission The current Auftrag attached to this contact --- @field #string recce The name of the recce unit that detected this contact +-- @field #boolean isship If `true`, contact is a naval group. +-- @field #boolean ishelo If `true`, contact is a helo group. +-- @field #boolean isground If `true`, contact is a ground group. +-- @field #boolean isStatic If `true`, contact is a STATIC object. +-- @field Ops.Auftrag#AUFTRAG mission The current Auftrag attached to this contact. +-- @field Ops.Target#TARGET target The Target attached to this contact. +-- @field #string recce The name of the recce unit that detected this contact. +-- @field #string ctype Contact type of #INTEL.Ctype. +-- @field #string platform [AIR] Contact platform name, e.g. Foxbat, Flanker_E, defaults to Bogey if unknown +-- @field #number heading [AIR] Heading of the contact, if available. +-- @field #boolean maneuvering [AIR] Contact has changed direction by >10 deg. +-- @field #number altitude [AIR] Flight altitude of the contact in meters. --- Cluster info. -- @type INTEL.Cluster @@ -142385,19 +164643,37 @@ INTEL = { -- @field #number threatlevelAve Average of threat levels. -- @field Core.Point#COORDINATE coordinate Coordinate of the cluster. -- @field Wrapper.Marker#MARKER marker F10 marker. --- @field Ops.Auftrag#AUFTRAG mission The current Auftrag attached to this cluster +-- @field #number markerID Marker ID. +-- @field Ops.Auftrag#AUFTRAG mission The current Auftrag attached to this cluster. +-- @field #string ctype Cluster type of #INTEL.Ctype. +-- @field #number altitude [AIR] Average flight altitude of the cluster in meters. +--- Contact or cluster type. +-- @type INTEL.Ctype +-- @field #string GROUND Ground. +-- @field #string NAVAL Ship. +-- @field #string AIRCRAFT Airpane or helicopter. +-- @field #string STRUCTURE Static structure. +INTEL.Ctype={ + GROUND="Ground", + NAVAL="Naval", + AIRCRAFT="Aircraft", + STRUCTURE="Structure" +} --- INTEL class version. -- @field #string version -INTEL.version="0.2.7" +INTEL.version="0.3.3" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- DONE: Filter detection methods. +-- TODO: Make forget times user inpupt. Currently these are hard coded. +-- TODO: Add min cluster size. Only create new clusters if they have a certain group size. -- TODO: process detected set asynchroniously for better performance. +-- DONE: Add statics. +-- DONE: Filter detection methods. -- DONE: Accept zones. -- DONE: Reject zones. -- NOGO: SetAttributeZone --> return groups of generalized attributes in a zone. @@ -142421,7 +164697,7 @@ function INTEL:New(DetectionSet, Coalition, Alias) -- Detection set. self.detectionset=DetectionSet or SET_GROUP:New() - + if Coalition and type(Coalition)=="string" then if Coalition=="blue" then Coalition=coalition.side.BLUE @@ -142433,44 +164709,44 @@ function INTEL:New(DetectionSet, Coalition, Alias) self:E("ERROR: Unknown coalition in INTEL!") end end - + -- Determine coalition from first group in set. self.coalition=Coalition or DetectionSet:CountAlive()>0 and DetectionSet:GetFirst():GetCoalition() or nil - + -- Filter coalition. if self.coalition then local coalitionname=UTILS.GetCoalitionName(self.coalition):lower() self.detectionset:FilterCoalitions(coalitionname) end - + -- Filter once. self.detectionset:FilterOnce() - + -- Set alias. if Alias then self.alias=tostring(Alias) else - self.alias="SPECTRE" + self.alias="INTEL SPECTRE" if self.coalition then if self.coalition==coalition.side.RED then - self.alias="KGB" + self.alias="INTEL KGB" elseif self.coalition==coalition.side.BLUE then - self.alias="CIA" + self.alias="INTEL CIA" end end - end - + end + self.DetectVisual = true self.DetectOptical = true self.DetectRadar = true self.DetectIRST = true self.DetectRWR = true self.DetectDLINK = true - + self.statusupdate = -60 - + -- Set some string id for output to DCS.log file. - self.lid=string.format("INTEL %s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown") + self.lid=string.format("%s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown") -- Start State. self:SetStartState("Stopped") @@ -142478,17 +164754,18 @@ function INTEL:New(DetectionSet, Coalition, Alias) -- Add FSM transitions. -- From State --> Event --> To State self:AddTransition("Stopped", "Start", "Running") -- Start FSM. - self:AddTransition("*", "Status", "*") -- INTEL status update - + self:AddTransition("*", "Status", "*") -- INTEL status update. + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + self:AddTransition("*", "Detect", "*") -- Start detection run. Not implemented yet! - + self:AddTransition("*", "NewContact", "*") -- New contact has been detected. self:AddTransition("*", "LostContact", "*") -- Contact could not be detected any more. - + self:AddTransition("*", "NewCluster", "*") -- New cluster has been detected. - self:AddTransition("*", "LostCluster", "*") -- Cluster could not be detected any more. - self:AddTransition("*", "Stop", "Stopped") - + self:AddTransition("*", "LostCluster", "*") -- Cluster could not be detected any more. + + -- Defaults self:SetForgetTime() self:SetAcceptZones() @@ -142507,6 +164784,7 @@ function INTEL:New(DetectionSet, Coalition, Alias) -- @param #INTEL self -- @param #number delay Delay in seconds. + --- Triggers the FSM event "Stop". Stops the INTEL and all its event handlers. -- @param #INTEL self @@ -142515,6 +164793,7 @@ function INTEL:New(DetectionSet, Coalition, Alias) -- @param #INTEL self -- @param #number delay Delay in seconds. + --- Triggers the FSM event "Status". -- @function [parent=#INTEL] Status -- @param #INTEL self @@ -142523,7 +164802,19 @@ function INTEL:New(DetectionSet, Coalition, Alias) -- @function [parent=#INTEL] __Status -- @param #INTEL self -- @param #number delay Delay in seconds. - + + + --- Triggers the FSM event "NewContact". + -- @function [parent=#INTEL] NewContact + -- @param #INTEL self + -- @param #INTEL.Contact Contact Detected contact. + + --- Triggers the FSM event "NewContact" after a delay. + -- @function [parent=#INTEL] NewContact + -- @param #INTEL self + -- @param #number delay Delay in seconds. + -- @param #INTEL.Contact Contact Detected contact. + --- On After "NewContact" event. -- @function [parent=#INTEL] OnAfterNewContact -- @param #INTEL self @@ -142531,7 +164822,19 @@ function INTEL:New(DetectionSet, Coalition, Alias) -- @param #string Event Event. -- @param #string To To state. -- @param #INTEL.Contact Contact Detected contact. - + + + --- Triggers the FSM event "LostContact". + -- @function [parent=#INTEL] LostContact + -- @param #INTEL self + -- @param #INTEL.Contact Contact Lost contact. + + --- Triggers the FSM event "LostContact" after a delay. + -- @function [parent=#INTEL] LostContact + -- @param #INTEL self + -- @param #number delay Delay in seconds. + -- @param #INTEL.Contact Contact Lost contact. + --- On After "LostContact" event. -- @function [parent=#INTEL] OnAfterLostContact -- @param #INTEL self @@ -142539,24 +164842,49 @@ function INTEL:New(DetectionSet, Coalition, Alias) -- @param #string Event Event. -- @param #string To To state. -- @param #INTEL.Contact Contact Lost contact. - + + + --- Triggers the FSM event "NewCluster". + -- @function [parent=#INTEL] NewCluster + -- @param #INTEL self + -- @param #INTEL.Cluster Cluster Detected cluster. + + --- Triggers the FSM event "NewCluster" after a delay. + -- @function [parent=#INTEL] NewCluster + -- @param #INTEL self + -- @param #number delay Delay in seconds. + -- @param #INTEL.Cluster Cluster Detected cluster. + --- On After "NewCluster" event. -- @function [parent=#INTEL] OnAfterNewCluster -- @param #INTEL self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. - -- @param #INTEL.Contact Contact Detected contact. - -- @param #INTEL.Cluster Cluster Detected cluster - + -- @param #INTEL.Cluster Cluster Detected cluster. + + + --- Triggers the FSM event "LostCluster". + -- @function [parent=#INTEL] LostCluster + -- @param #INTEL self + -- @param #INTEL.Cluster Cluster Lost cluster. + -- @param Ops.Auftrag#AUFTRAG Mission The Auftrag connected with this cluster or `nil`. + + --- Triggers the FSM event "LostCluster" after a delay. + -- @function [parent=#INTEL] LostCluster + -- @param #INTEL self + -- @param #number delay Delay in seconds. + -- @param #INTEL.Cluster Cluster Lost cluster. + -- @param Ops.Auftrag#AUFTRAG Mission The Auftrag connected with this cluster or `nil`. + --- On After "LostCluster" event. -- @function [parent=#INTEL] OnAfterLostCluster -- @param #INTEL self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. - -- @param #INTEL.Cluster Cluster Lost cluster - -- @param Ops.Auftrag#AUFTRAG Mission The Auftrag connected with this cluster or nil + -- @param #INTEL.Cluster Cluster Lost cluster. + -- @param Ops.Auftrag#AUFTRAG Mission The Auftrag connected with this cluster or `nil`. return self end @@ -142564,7 +164892,7 @@ end -- User functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Set accept zones. Only contacts detected in this/these zone(s) are considered. +--- Set accept zones. Only contacts detected in this/these zone(s) are considered. -- @param #INTEL self -- @param Core.Set#SET_ZONE AcceptZoneSet Set of accept zones. -- @return #INTEL self @@ -142620,25 +164948,24 @@ function INTEL:RemoveRejectZone(RejectZone) return self end ---- Set forget contacts time interval. +--- **OBSOLETE, will be removed in next version!** Set forget contacts time interval. -- Previously known contacts that are not detected any more, are "lost" after this time. -- This avoids fast oscillations between a contact being detected and undetected. -- @param #INTEL self -- @param #number TimeInterval Time interval in seconds. Default is 120 sec. -- @return #INTEL self function INTEL:SetForgetTime(TimeInterval) - self.dTforget=TimeInterval or 120 return self end --- Filter unit categories. Valid categories are: --- +-- -- * Unit.Category.AIRPLANE -- * Unit.Category.HELICOPTER -- * Unit.Category.GROUND_UNIT -- * Unit.Category.SHIP -- * Unit.Category.STRUCTURE --- +-- -- @param #INTEL self -- @param #table Categories Filter categories, e.g. {Unit.Category.AIRPLANE, Unit.Category.HELICOPTER}. -- @return #INTEL self @@ -142646,26 +164973,26 @@ function INTEL:SetFilterCategory(Categories) if type(Categories)~="table" then Categories={Categories} end - + self.filterCategory=Categories - + local text="Filter categories: " for _,category in pairs(self.filterCategory) do text=text..string.format("%d,", category) end self:T(self.lid..text) - + return self end --- Filter group categories. Valid categories are: --- +-- -- * Group.Category.AIRPLANE -- * Group.Category.HELICOPTER -- * Group.Category.GROUND -- * Group.Category.SHIP -- * Group.Category.TRAIN --- +-- -- @param #INTEL self -- @param #table GroupCategories Filter categories, e.g. `{Group.Category.AIRPLANE, Group.Category.HELICOPTER}`. -- @return #INTEL self @@ -142673,15 +165000,31 @@ function INTEL:FilterCategoryGroup(GroupCategories) if type(GroupCategories)~="table" then GroupCategories={GroupCategories} end - + self.filterCategoryGroup=GroupCategories - + local text="Filter group categories: " for _,category in pairs(self.filterCategoryGroup) do text=text..string.format("%d,", category) end self:T(self.lid..text) - + + return self +end + +--- Add a group to the detection set. +-- @param #INTEL self +-- @param Wrapper.Group#GROUP AgentGroup Group of agents. Can also be an @{Ops.OpsGroup#OPSGROUP} object. +-- @return #INTEL self +function INTEL:AddAgent(AgentGroup) + + -- Check if this was an OPS group. + if AgentGroup:IsInstanceOf("OPSGROUP") then + AgentGroup=AgentGroup:GetGroup() + end + + -- Add to detection set. + self.detectionset:AddGroup(AgentGroup) return self end @@ -142690,14 +165033,29 @@ end -- @param #INTEL self -- @param #boolean Switch If true, enable cluster analysis. -- @param #boolean Markers If true, place markers on F10 map. +-- @param #boolean Arrows If true, draws arrows on F10 map. -- @return #INTEL self -function INTEL:SetClusterAnalysis(Switch, Markers) +function INTEL:SetClusterAnalysis(Switch, Markers, Arrows) self.clusteranalysis=Switch self.clustermarkers=Markers + self.clusterarrows=Arrows return self end ---- Set verbosity level for debugging. +--- Set whether STATIC objects are detected. +-- @param #INTEL self +-- @param #boolean Switch If `true`, statics are detected. +-- @return #INTEL self +function INTEL:SetDetectStatics(Switch) + if Switch and Switch==true then + self.detectStatics=true + else + self.detectStatics=false + end + return self +end + +--- Set verbosity level for debugging. -- @param #INTEL self -- @param #number Verbosity The higher, the noisier, e.g. 0=off, 2=debug -- @return #INTEL self @@ -142730,13 +165088,12 @@ function INTEL:AddMissionToCluster(Cluster, Mission) return self end ---- Change radius of the Clusters +--- Change radius of the Clusters. -- @param #INTEL self --- @param #number radius The radius of the clusters +-- @param #number radius The radius of the clusters in kilometers. Default 15 km. -- @return #INTEL self function INTEL:SetClusterRadius(radius) - local radius = radius or 15 - self.clusterradius = radius + self.clusterradius = (radius or 15)*1000 return self end @@ -142750,7 +165107,7 @@ end -- @param #boolean DetectDLINK Data link detection -- @return self function INTEL:SetDetectionTypes(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) - self.DetectVisual = DetectVisual and true + self.DetectVisual = DetectVisual and true self.DetectOptical = DetectOptical and true self.DetectRadar = DetectRadar and true self.DetectIRST = DetectIRST and true @@ -142781,6 +165138,55 @@ function INTEL:GetClusterTable() end end +--- Get name of a contact. +-- @param #INTEL self +-- @param #INTEL.Contact Contact The contact. +-- @return #string Name of the contact. +function INTEL:GetContactName(Contact) + return Contact.groupname +end + +--- Get group of a contact. +-- @param #INTEL self +-- @param #INTEL.Contact Contact The contact. +-- @return Wrapper.Group#GROUP Group object. +function INTEL:GetContactGroup(Contact) + return Contact.group +end + +--- Get threatlevel of a contact. +-- @param #INTEL self +-- @param #INTEL.Contact Contact The contact. +-- @return #number Threat level. +function INTEL:GetContactThreatlevel(Contact) + return Contact.threatlevel +end + + +--- Get type name of a contact. +-- @param #INTEL self +-- @param #INTEL.Contact Contact The contact. +-- @return #string Type name. +function INTEL:GetContactTypeName(Contact) + return Contact.typename +end + +--- Get category name of a contact. +-- @param #INTEL self +-- @param #INTEL.Contact Contact The contact. +-- @return #string Category name. +function INTEL:GetContactCategoryName(Contact) + return Contact.categoryname +end + +--- Get coordinate of a contact. +-- @param #INTEL self +-- @param #INTEL.Contact Contact The contact. +-- @return Core.Point#COORDINATE Coordinates. +function INTEL:GetContactCoordinate(Contact) + return Contact.position +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Start & Status ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -142798,6 +165204,7 @@ function INTEL:onafterStart(From, Event, To) -- Start the status monitoring. self:__Status(-math.random(10)) + return self end --- On after "Status" event. @@ -142809,14 +165216,14 @@ function INTEL:onafterStatus(From, Event, To) -- FSM state. local fsmstate=self:GetState() - + -- Fresh arrays. self.ContactsLost={} self.ContactsUnknown={} - + -- Check if group has detected any units. self:UpdateIntel() - + -- Number of total contacts. local Ncontacts=#self.Contacts local Nclusters=#self.Clusters @@ -142826,23 +165233,24 @@ function INTEL:onafterStatus(From, Event, To) local text=string.format("Status %s [Agents=%s]: Contacts=%d, Clusters=%d, New=%d, Lost=%d", fsmstate, self.detectionset:CountAlive(), Ncontacts, Nclusters, #self.ContactsUnknown, #self.ContactsLost) self:I(self.lid..text) end - + -- Detailed info. if self.verbose>=2 and Ncontacts>0 then local text="Detected Contacts:" for _,_contact in pairs(self.Contacts) do local contact=_contact --#INTEL.Contact local dT=timer.getAbsTime()-contact.Tdetected - text=text..string.format("\n- %s (%s): %s, units=%d, T=%d sec", contact.categoryname, contact.attribute, contact.groupname, contact.group:CountAliveUnits(), dT) + text=text..string.format("\n- %s (%s): %s, units=%d, T=%d sec", contact.categoryname, contact.attribute, contact.groupname, contact.isStatic and 1 or contact.group:CountAliveUnits(), dT) if contact.mission then local mission=contact.mission --Ops.Auftrag#AUFTRAG text=text..string.format(" mission name=%s type=%s target=%s", mission.name, mission.type, mission:GetTargetName() or "unknown") end end self:I(self.lid..text) - end + end - self:__Status(self.statusupdate) + self:__Status(self.statusupdate) + return self end @@ -142852,29 +165260,31 @@ function INTEL:UpdateIntel() -- Set of all detected units. local DetectedUnits={} + -- Set of which units was detected by which recce local RecceDetecting = {} + -- Loop over all units providing intel. for _,_group in pairs(self.detectionset.Set or {}) do local group=_group --Wrapper.Group#GROUP - + if group and group:IsAlive() then - + for _,_recce in pairs(group:GetUnits()) do local recce=_recce --Wrapper.Unit#UNIT - + -- Get detected units. self:GetDetectedUnits(recce, DetectedUnits, RecceDetecting, self.DetectVisual, self.DetectOptical, self.DetectRadar, self.DetectIRST, self.DetectRWR, self.DetectDLINK) - + end - - end + + end end - + local remove={} for unitname,_unit in pairs(DetectedUnits) do local unit=_unit --Wrapper.Unit#UNIT - + -- Check if unit is in any of the accept zones. if self.acceptzoneset:Count()>0 then local inzone=false @@ -142885,7 +165295,7 @@ function INTEL:UpdateIntel() break end end - + -- Unit is not in accept zone ==> remove! if not inzone then table.insert(remove, unitname) @@ -142902,13 +165312,13 @@ function INTEL:UpdateIntel() break end end - + -- Unit is inside a reject zone ==> remove! if inzone then table.insert(remove, unitname) end end - + -- Filter unit categories. if #self.filterCategory>0 then local unitcategory=unit:GetUnitCategory() @@ -142923,79 +165333,103 @@ function INTEL:UpdateIntel() self:T(self.lid..string.format("Removing unit %s category=%d", unitname, unit:GetCategory())) table.insert(remove, unitname) end - end - + end + end - + -- Remove filtered units. for _,unitname in pairs(remove) do DetectedUnits[unitname]=nil end - + -- Create detected groups. local DetectedGroups={} - local RecceGroups={} + local DetectedStatics={} + local RecceGroups={} for unitname,_unit in pairs(DetectedUnits) do local unit=_unit --Wrapper.Unit#UNIT - local group=unit:GetGroup() - if group then - local groupname = group:GetName() - DetectedGroups[groupname]=group - RecceGroups[groupname]=RecceDetecting[unitname] + if unit:IsInstanceOf("UNIT") then + local group=unit:GetGroup() + if group then + local groupname = group:GetName() + DetectedGroups[groupname]=group + RecceGroups[groupname]=RecceDetecting[unitname] + end + else + if self.detectStatics then + DetectedStatics[unitname]=unit + RecceGroups[unitname]=RecceDetecting[unitname] + end end end - - -- Create detected contacts. - self:CreateDetectedItems(DetectedGroups, RecceGroups) - + + -- Create detected contacts. + self:CreateDetectedItems(DetectedGroups, DetectedStatics, RecceGroups) + -- Paint a picture of the battlefield. if self.clusteranalysis then self:PaintPicture() end - + + return self end - - - - ---- Create detected items. +--- Update an #INTEL.Contact item. -- @param #INTEL self --- @param #table DetectedGroups Table of detected Groups --- @param #table RecceDetecting Table of detecting recce names -function INTEL:CreateDetectedItems(DetectedGroups, RecceDetecting) - self:F({RecceDetecting=RecceDetecting}) - -- Current time. - local Tnow=timer.getAbsTime() - - for groupname,_group in pairs(DetectedGroups) do - local group=_group --Wrapper.Group#GROUP - - - -- Get contact if already known. - local detecteditem=self:GetContactByName(groupname) - - if detecteditem then - --- - -- Detected item already exists ==> Update data. - --- - - detecteditem.Tdetected=Tnow - detecteditem.position=group:GetCoordinate() - detecteditem.velocity=group:GetVelocityVec3() - detecteditem.speed=group:GetVelocityMPS() - - else - --- - -- Detected item does not exist in our list yet. - --- - - -- Create new contact. - local item={} --#INTEL.Contact - - item.groupname=groupname +-- @param #INTEL.Contact Contact Contact. +-- @return #INTEL.Contact The contact. +function INTEL:_UpdateContact(Contact) + + if Contact.isStatic then + + -- Statics don't need to be updated. + + else + + if Contact.group and Contact.group:IsAlive() then + + Contact.Tdetected=timer.getAbsTime() + Contact.position=Contact.group:GetCoordinate() + Contact.velocity=Contact.group:GetVelocityVec3() + Contact.speed=Contact.group:GetVelocityMPS() + if Contact.group:IsAir() then + Contact.altitude=Contact.group:GetAltitude() + local oldheading = Contact.heading or 1 + local newheading = Contact.group:GetHeading() + if newheading == 0 then newheading = 1 end + local changeh = math.abs(((oldheading - newheading) + 360) % 360) + Contact.heading = newheading + if changeh > 10 then + Contact.maneuvering = true + else + Contact.maneuvering = false + end + end + end + + end + return self +end + +--- Create an #INTEL.Contact item from a given GROUP or STATIC object. +-- @param #INTEL self +-- @param Wrapper.Positionable#POSITIONABLE Positionable The GROUP or STATIC object. +-- @param #string RecceName The name of the recce group that has detected this contact. +-- @return #INTEL.Contact The contact. +function INTEL:_CreateContact(Positionable, RecceName) + + if Positionable and Positionable:IsAlive() then + + -- Create new contact. + local item={} --#INTEL.Contact + + if Positionable:IsInstanceOf("GROUP") then + + local group=Positionable --Wrapper.Group#GROUP + + item.groupname=group:GetName() item.group=group - item.Tdetected=Tnow + item.Tdetected=timer.getAbsTime() item.typename=group:GetTypeName() item.attribute=group:GetAttribute() item.category=group:GetCategory() @@ -143004,35 +165438,106 @@ function INTEL:CreateDetectedItems(DetectedGroups, RecceDetecting) item.position=group:GetCoordinate() item.velocity=group:GetVelocityVec3() item.speed=group:GetVelocityMPS() - item.recce=RecceDetecting[groupname] + item.recce=RecceName item.isground = group:IsGround() or false item.isship = group:IsShip() or false - self:T(string.format("%s group detect by %s/%s", groupname, RecceDetecting[groupname] or "unknown", item.recce or "unknown")) - -- Add contact to table. - self:AddContact(item) - - -- Trigger new contact event. - self:NewContact(item) + item.isStatic=false + if group:IsAir() then + item.platform=group:GetNatoReportingName() + item.heading = group:GetHeading() + item.maneuvering = false + item.altitude = group:GetAltitude() + else + -- TODO optionally add ground types? + item.platform="Unknown" + item.altitude = group:GetAltitude(true) + end + if item.category==Group.Category.AIRPLANE or item.category==Group.Category.HELICOPTER then + item.ctype=INTEL.Ctype.AIRCRAFT + elseif item.category==Group.Category.GROUND or item.category==Group.Category.TRAIN then + item.ctype=INTEL.Ctype.GROUND + elseif item.category==Group.Category.SHIP then + item.ctype=INTEL.Ctype.NAVAL + end + + return item + + elseif Positionable:IsInstanceOf("STATIC") then + + local static=Positionable --Wrapper.Static#STATIC + + item.groupname=static:GetName() + item.group=static + item.Tdetected=timer.getAbsTime() + item.typename=static:GetTypeName() or "Unknown" + item.attribute="Static" + item.category=3 --static:GetCategory() + item.categoryname=static:GetCategoryName() or "Unknown" + item.threatlevel=static:GetThreatLevel() or 0 + item.position=static:GetCoordinate() + item.velocity=static:GetVelocityVec3() + item.speed=0 + item.recce=RecceName + item.isground = true + item.isship = false + item.isStatic=true + item.ctype=INTEL.Ctype.STRUCTURE + + return item + else + self:E(self.lid..string.format("ERROR: object needs to be a GROUP or STATIC!")) end - + end - + + return nil +end + +--- Create detected items. +-- @param #INTEL self +-- @param #table DetectedGroups Table of detected Groups. +-- @param #table DetectedStatics Table of detected Statics. +-- @param #table RecceDetecting Table of detecting recce names. +function INTEL:CreateDetectedItems(DetectedGroups, DetectedStatics, RecceDetecting) + self:F({RecceDetecting=RecceDetecting}) + + -- Current time. + local Tnow=timer.getAbsTime() + + -- Loop over groups. + for groupname,_group in pairs(DetectedGroups) do + local group=_group --Wrapper.Group#GROUP + + -- Create or update contact for this group. + self:KnowObject(group, RecceDetecting[groupname]) + + end + + -- Loop over statics. + for staticname,_static in pairs(DetectedStatics) do + local static=_static --Wrapper.Static#STATIC + + -- Create or update contact for this group. + self:KnowObject(static, RecceDetecting[staticname]) + + end + -- Now check if there some groups could not be detected any more. for i=#self.Contacts,1,-1 do local item=self.Contacts[i] --#INTEL.Contact - + -- Check if deltaT>Tforget. We dont want quick oscillations between detected and undetected states. if self:_CheckContactLost(item) then - + -- Trigger LostContact event. This also adds the contact to the self.ContactsLost table. self:LostContact(item) - + -- Remove contact from table. self:RemoveContact(item) - + end end - + return self end --- (Internal) Return the detected target groups of the controllable as a @{SET_GROUP}. @@ -143040,8 +165545,8 @@ end -- If no detection method is given, the detection will use all the available methods by default. -- @param #INTEL self -- @param Wrapper.Unit#UNIT Unit The unit detecting. --- @param #table DetectedUnits Table of detected units to be filled --- @param #table RecceDetecting Table of recce per unit to be filled +-- @param #table DetectedUnits Table of detected units to be filled. +-- @param #table RecceDetecting Table of recce per unit to be filled. -- @param #boolean DetectVisual (Optional) If *false*, do not include visually detected targets. -- @param #boolean DetectOptical (Optional) If *false*, do not include optically detected targets. -- @param #boolean DetectRadar (Optional) If *false*, do not include targets detected by radar. @@ -143053,21 +165558,40 @@ function INTEL:GetDetectedUnits(Unit, DetectedUnits, RecceDetecting, DetectVisua -- Get detected DCS units. local reccename = Unit:GetName() local detectedtargets=Unit:GetDetectedTargets(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) - + for DetectionObjectID, Detection in pairs(detectedtargets or {}) do local DetectedObject=Detection.object -- DCS#Object + -- NOTE: Got an object that exists but when trying UNIT:Find() the DCS getName() function failed. ID of the object was 5,000,031 if DetectedObject and DetectedObject:isExist() and DetectedObject.id_<50000000 then - - local unit=UNIT:Find(DetectedObject) - if unit and unit:IsAlive() then - - local unitname=unit:GetName() - - DetectedUnits[unitname]=unit - RecceDetecting[unitname]=reccename - self:T(string.format("Unit %s detect by %s", unitname, reccename)) + -- Protected call to get the name of the object. + local status,name = pcall( + function() + local name=DetectedObject:getName() + return name + end) + + if status then + + local unit=UNIT:FindByName(name) + + if unit and unit:IsAlive() then + DetectedUnits[name]=unit + RecceDetecting[name]=reccename + self:T(string.format("Unit %s detect by %s", name, reccename)) + else + local static=STATIC:FindByName(name, false) + if static then + --env.info("FF found static "..name) + DetectedUnits[name]=static + RecceDetecting[name]=reccename + end + end + + else + -- Warning! + self:T(self.lid..string.format("WARNING: Could not get name of detected object ID=%s! Detected by %s", DetectedObject.id_, reccename)) end end end @@ -143085,8 +165609,13 @@ end -- @param #string To To state. -- @param #INTEL.Contact Contact Detected contact. function INTEL:onafterNewContact(From, Event, To, Contact) + + -- Debug text. self:F(self.lid..string.format("NEW contact %s", Contact.groupname)) - table.insert(self.ContactsUnknown, Contact) + + -- Add to table of unknown contacts. + table.insert(self.ContactsUnknown, Contact) + return self end --- On after "LostContact" event. @@ -143094,10 +165623,15 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #INTEL.Contact Contact Detected contact. +-- @param #INTEL.Contact Contact Lost contact. function INTEL:onafterLostContact(From, Event, To, Contact) + + -- Debug text. self:F(self.lid..string.format("LOST contact %s", Contact.groupname)) + + -- Add to table of lost contacts. table.insert(self.ContactsLost, Contact) + return self end --- On after "NewCluster" event. @@ -143105,10 +165639,15 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #INTEL.Contact Contact Detected contact. --- @param #INTEL.Cluster Cluster Detected cluster -function INTEL:onafterNewCluster(From, Event, To, Contact, Cluster) - self:F(self.lid..string.format("NEW cluster %d size %d with contact %s", Cluster.index, Cluster.size, Contact.groupname)) +-- @param #INTEL.Cluster Cluster Detected cluster. +function INTEL:onafterNewCluster(From, Event, To, Cluster) + + -- Debug text. + self:F(self.lid..string.format("NEW cluster #%d [%s] of size %d", Cluster.index, Cluster.ctype, Cluster.size)) + + -- Add cluster to table. + self:_AddCluster(Cluster) + return self end --- On after "LostCluster" event. @@ -143116,21 +165655,80 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #INTEL.Cluster Cluster Lost cluster --- @param Ops.Auftrag#AUFTRAG Mission The Auftrag connected with this cluster or nil +-- @param #INTEL.Cluster Cluster Lost cluster. +-- @param Ops.Auftrag#AUFTRAG Mission The Auftrag connected with this cluster or `nil`. function INTEL:onafterLostCluster(From, Event, To, Cluster, Mission) - local text = self.lid..string.format("LOST cluster %d", Cluster.index) + + -- Debug text. + local text = self.lid..string.format("LOST cluster #%d [%s]", Cluster.index, Cluster.ctype) + if Mission then local mission=Mission --Ops.Auftrag#AUFTRAG text=text..string.format(" mission name=%s type=%s target=%s", mission.name, mission.type, mission:GetTargetName() or "unknown") end + self:T(text) + return self end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Misc Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Make the INTEL aware of a object that was not detected (yet). This will add the object to the contacts table and trigger a `NewContact` event. +-- @param #INTEL self +-- @param Wrapper.Positionable#POSITIONABLE Positionable Group or static object. +-- @param #string RecceName Name of the recce group that detected this object. +-- @param #number Tdetected Abs. mission time in seconds, when the object is detected. Default now. +-- @return #INTEL self +function INTEL:KnowObject(Positionable, RecceName, Tdetected) + + local Tnow=timer.getAbsTime() + Tdetected=Tdetected or Tnow + + if Positionable and Positionable:IsAlive() then + + if Tdetected>Tnow then + -- Delay call. + self:ScheduleOnce(Tdetected-Tnow, self.KnowObject, self, Positionable, RecceName) + else + + -- Name of the object. + local name=Positionable:GetName() + + -- Try to get the contact by name. + local contact=self:GetContactByName(name) + + if contact then + + -- Update contact info. + self:_UpdateContact(contact) + + else + + -- Create new contact. + contact=self:_CreateContact(Positionable, RecceName) + + if contact then + + -- Debug info. + self:T(string.format("%s contact detected by %s", contact.groupname, RecceName or "unknown")) + + -- Add contact to table. + self:AddContact(contact) + + -- Trigger new contact event. + self:NewContact(contact) + + end + + end + end + end + + return self +end + --- Get a contact by name. -- @param #INTEL self -- @param #string groupname Name of the contact group. @@ -143147,11 +165745,38 @@ function INTEL:GetContactByName(groupname) return nil end +--- Check if a Contact is already known. It is checked, whether the contact is in the contacts table. +-- @param #INTEL self +-- @param #INTEL.Contact Contact The contact to be added. +-- @return #boolean If `true`, contact is already known. +function INTEL:_IsContactKnown(Contact) + + for i,_contact in pairs(self.Contacts) do + local contact=_contact --#INTEL.Contact + if contact.groupname==Contact.groupname then + return true + end + end + + return false +end + + --- Add a contact to our list. -- @param #INTEL self -- @param #INTEL.Contact Contact The contact to be added. +-- @return #INTEL self function INTEL:AddContact(Contact) - table.insert(self.Contacts, Contact) + + -- First check if the contact is already in the table. + if self:_IsContactKnown(Contact) then + self:E(self.lid..string.format("WARNING: Contact %s is already in the contact table!", tostring(Contact.groupname))) + else + self:T(self.lid..string.format("Adding new Contact %s to table", tostring(Contact.groupname))) + table.insert(self.Contacts, Contact) + end + + return self end --- Remove a contact from our list. @@ -143161,13 +165786,13 @@ function INTEL:RemoveContact(Contact) for i,_contact in pairs(self.Contacts) do local contact=_contact --#INTEL.Contact - + if contact.groupname==Contact.groupname then table.remove(self.Contacts, i) end - - end + end + return self end --- Check if a contact was lost. @@ -143181,10 +165806,16 @@ function INTEL:_CheckContactLost(Contact) return true end + -- We never forget statics as they don't move. + if Contact.isStatic then + return false + end + -- Time since last detected. local dT=timer.getAbsTime()-Contact.Tdetected - - local dTforget=self.dTforget + + local dTforget=nil + if Contact.category==Group.Category.GROUND then dTforget=60*60*2 -- 2 hours elseif Contact.category==Group.Category.AIRPLANE then @@ -143196,13 +165827,13 @@ function INTEL:_CheckContactLost(Contact) elseif Contact.category==Group.Category.TRAIN then dTforget=60*60 -- 1 hour end - + if dT>dTforget then return true else return false end - + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -143210,191 +165841,291 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- [Internal] Paint picture of the battle field. Does Cluster analysis and updates clusters. Sets markers if markers are enabled. --- @param #INTEL self +-- @param #INTEL self function INTEL:PaintPicture() + self:F(self.lid.."Painting Picture!") -- First remove all lost contacts from clusters. for _,_contact in pairs(self.ContactsLost) do local contact=_contact --#INTEL.Contact + + -- Get cluster this contact belongs to (if any). local cluster=self:GetClusterOfContact(contact) + if cluster then self:RemoveContactFromCluster(contact, cluster) end end - -- clean up cluster table + + -- Clean up cluster table. local ClusterSet = {} + + -- Now check if whole clusters were lost. for _i,_cluster in pairs(self.Clusters) do - if (_cluster.size > 0) and (self:ClusterCountUnits(_cluster) > 0) then + local cluster=_cluster --#INTEL.Cluster + + if cluster.size>0 and self:ClusterCountUnits(cluster)>0 then + -- This one has size>0 and units>0 table.insert(ClusterSet,_cluster) else - local mission = _cluster.mission or nil - local marker = _cluster.marker - local markerID = _cluster.markerID - if marker then - marker:Remove() + + -- This cluster is gone. + + -- Remove marker. + if cluster.marker then + cluster.marker:Remove() end - if markerID then - COORDINATE:RemoveMark(markerID) + + -- Marker of the arrow. + if cluster.markerID then + COORDINATE:RemoveMark(cluster.markerID) end - self:LostCluster(_cluster, mission) + + -- Lost cluster. + self:LostCluster(cluster, cluster.mission) end end + + -- Set Clusters. self.Clusters = ClusterSet - -- update positions + + -- Update positions. self:_UpdateClusterPositions() - + + for _,_contact in pairs(self.Contacts) do local contact=_contact --#INTEL.Contact + + -- Debug info. self:T(string.format("Paint Picture: checking for %s",contact.groupname)) - -- Check if this contact is in any cluster. - local isincluster=self:CheckContactInClusters(contact) - + -- Get the current cluster (if any) this contact belongs to. local currentcluster=self:GetClusterOfContact(contact) - + if currentcluster then - --self:I(string.format("Paint Picture: %s has current cluster",contact.groupname)) --- -- Contact is currently part of a cluster. --- - + -- Check if the contact is still connected to the cluster. local isconnected=self:IsContactConnectedToCluster(contact, currentcluster) - - if (not isconnected) and (currentcluster.size > 1) then - --self:I(string.format("Paint Picture: %s has LOST current cluster",contact.groupname)) - local cluster=self:IsContactPartOfAnyClusters(contact) - + + if isconnected then + + else + + --- Not connected to current cluster any more. + + -- Remove from current cluster. + self:RemoveContactFromCluster(contact, currentcluster) + + -- Find new cluster. + local cluster=self:_GetClosestClusterOfContact(contact) + if cluster then + -- Add contact to cluster. self:AddContactToCluster(contact, cluster) else - - local newcluster=self:CreateCluster(contact.position) - self:AddContactToCluster(contact, newcluster) - self:NewCluster(contact, newcluster) + + -- Create a new cluster. + local newcluster=self:_CreateClusterFromContact(contact) + + -- Trigger new cluster event. + self:NewCluster(newcluster) end - + end - - + else - + --- -- Contact is not in any cluster yet. --- - --self:I(string.format("Paint Picture: %s has NO current cluster",contact.groupname)) - local cluster=self:IsContactPartOfAnyClusters(contact) - + + -- Debug info. + self:T(self.lid..string.format("Paint Picture: contact %s has NO current cluster", contact.groupname)) + + -- Get the closest existing cluster of this contact. + local cluster=self:_GetClosestClusterOfContact(contact) + if cluster then + + -- Debug info. + self:T(self.lid..string.format("Paint Picture: contact %s has closest cluster #%d",contact.groupname, cluster.index)) + + -- Add contact to this cluster. self:AddContactToCluster(contact, cluster) + else - local newcluster=self:CreateCluster(contact.position) - self:AddContactToCluster(contact, newcluster) - self:NewCluster(contact, newcluster) - end - - end - - end - + -- Debug info. + self:T(self.lid..string.format("Paint Picture: contact %s has no closest cluster ==> Create new cluster", contact.groupname)) + + -- Create a brand new cluster. + local newcluster=self:_CreateClusterFromContact(contact) + + -- Trigger event for a new cluster. + self:NewCluster(newcluster) + end + + end + + end + + -- Update positions. + self:_UpdateClusterPositions() - -- Update F10 marker text if cluster has changed. if self.clustermarkers then for _,_cluster in pairs(self.Clusters) do local cluster=_cluster --#INTEL.Cluster --local coordinate=self:GetClusterCoordinate(cluster) + -- Update F10 marker. + if self.verbose >= 1 then + BASE:I("Updating cluster marker and future position") + end + + -- Update cluster markers. self:UpdateClusterMarker(cluster) - self:CalcClusterFuturePosition(cluster,self.prediction) + + -- Extrapolate future position of the cluster. + self:CalcClusterFuturePosition(cluster, 300) + end end + + return self end --- Create a new cluster. -- @param #INTEL self --- @param Core.Point#COORDINATE coordinate The coordinate of the cluster. --- @return #INTEL.Cluster cluster The cluster. -function INTEL:CreateCluster(coordinate) +-- @return #INTEL.Cluster cluster The cluster. +function INTEL:_CreateCluster() - -- Create new cluster + -- Create new cluster. local cluster={} --#INTEL.Cluster - + cluster.index=self.clustercounter - cluster.coordinate=coordinate + cluster.coordinate=COORDINATE:New(0, 0, 0) cluster.threatlevelSum=0 cluster.threatlevelMax=0 cluster.size=0 cluster.Contacts={} - - -- Add cluster. - table.insert(self.Clusters, cluster) - + cluster.altitude=0 + -- Increase counter. - self.clustercounter=self.clustercounter+1 + self.clustercounter=self.clustercounter+1 return cluster end +--- Create a new cluster from a first contact. The contact is automatically added to the cluster. +-- @param #INTEL self +-- @param #INTEL.Contact Contact The first contact. +-- @return #INTEL.Cluster cluster The cluster. +function INTEL:_CreateClusterFromContact(Contact) + + local cluster=self:_CreateCluster() + + self:T(self.lid..string.format("Created NEW cluster #%d with first contact %s", cluster.index, Contact.groupname)) + + cluster.coordinate:UpdateFromCoordinate(Contact.position) + + cluster.ctype=Contact.ctype + + self:AddContactToCluster(Contact, cluster) + + return cluster +end + +--- Add cluster to table. +-- @param #INTEL self +-- @param #INTEL.Cluster Cluster The cluster to add. +function INTEL:_AddCluster(Cluster) + + --TODO: Check if cluster is already in the table. + + -- Add cluster. + table.insert(self.Clusters, Cluster) + + return self +end + --- Add a contact to the cluster. -- @param #INTEL self -- @param #INTEL.Contact contact The contact. --- @param #INTEL.Cluster cluster The cluster. +-- @param #INTEL.Cluster cluster The cluster. function INTEL:AddContactToCluster(contact, cluster) if contact and cluster then - + -- Add neighbour to cluster contacts. table.insert(cluster.Contacts, contact) - + + -- Add to threat level sum. cluster.threatlevelSum=cluster.threatlevelSum+contact.threatlevel - + + -- Increase size. cluster.size=cluster.size+1 + + -- alt + self:GetClusterAltitude(cluster,true) + + -- Debug info. + self:T(self.lid..string.format("Adding contact %s to cluster #%d [%s] ==> New size=%d", contact.groupname, cluster.index, cluster.ctype, cluster.size)) end + return self end --- Remove a contact from a cluster. -- @param #INTEL self -- @param #INTEL.Contact contact The contact. --- @param #INTEL.Cluster cluster The cluster. +-- @param #INTEL.Cluster cluster The cluster. function INTEL:RemoveContactFromCluster(contact, cluster) if contact and cluster then - - for i,_contact in pairs(cluster.Contacts) do - local Contact=_contact --#INTEL.Contact - - if Contact.groupname==contact.groupname then - - cluster.threatlevelSum=cluster.threatlevelSum-contact.threatlevel - cluster.size=cluster.size-1 - - table.remove(cluster.Contacts, i) - - return - end - - end - - end + for i=#cluster.Contacts,1,-1 do + local Contact=cluster.Contacts[i] --#INTEL.Contact + + if Contact.groupname==contact.groupname then + + -- Remove threat level sum. + cluster.threatlevelSum=cluster.threatlevelSum-contact.threatlevel + + -- Decrease cluster size. + cluster.size=cluster.size-1 + + -- Remove from table. + table.remove(cluster.Contacts, i) + + -- Debug info. + self:T(self.lid..string.format("Removing contact %s from cluster #%d ==> New cluster size=%d", contact.groupname, cluster.index, cluster.size)) + + return self + end + + end + + end + return self end --- Calculate cluster threat level sum. -- @param #INTEL self -- @param #INTEL.Cluster cluster The cluster of contacts. --- @return #number Sum of all threat levels of all groups in the cluster. +-- @return #number Sum of all threat levels of all groups in the cluster. function INTEL:CalcClusterThreatlevelSum(cluster) local threatlevel=0 - + for _,_contact in pairs(cluster.Contacts) do local contact=_contact --#INTEL.Contact - + threatlevel=threatlevel+contact.threatlevel - + end cluster.threatlevelSum = threatlevel return threatlevel @@ -143403,10 +166134,10 @@ end --- Calculate cluster threat level average. -- @param #INTEL self -- @param #INTEL.Cluster cluster The cluster of contacts. --- @return #number Average of all threat levels of all groups in the cluster. +-- @return #number Average of all threat levels of all groups in the cluster. function INTEL:CalcClusterThreatlevelAverage(cluster) - local threatlevel=self:CalcClusterThreatlevelSum(cluster) + local threatlevel=self:CalcClusterThreatlevelSum(cluster) threatlevel=threatlevel/cluster.size cluster.threatlevelAve = threatlevel return threatlevel @@ -143415,19 +166146,19 @@ end --- Calculate max cluster threat level. -- @param #INTEL self -- @param #INTEL.Cluster cluster The cluster of contacts. --- @return #number Max threat levels of all groups in the cluster. +-- @return #number Max threat levels of all groups in the cluster. function INTEL:CalcClusterThreatlevelMax(cluster) local threatlevel=0 - + for _,_contact in pairs(cluster.Contacts) do - + local contact=_contact --#INTEL.Contact - + if contact.threatlevel>threatlevel then threatlevel=contact.threatlevel end - + end cluster.threatlevelMax = threatlevel return threatlevel @@ -143436,59 +166167,116 @@ end --- Calculate cluster heading. -- @param #INTEL self -- @param #INTEL.Cluster cluster The cluster of contacts. --- @return #number Heading average of all groups in the cluster. +-- @return #number Heading average of all groups in the cluster. function INTEL:CalcClusterDirection(cluster) local direction = 0 + local speedsum = 0 local n=0 for _,_contact in pairs(cluster.Contacts) do - local group = _contact.group -- Wrapper.Group#GROUP - if group:IsAlive() then - direction = direction + group:GetHeading() + local contact=_contact --#INTEL.Contact + + if (not contact.isStatic) and contact.group:IsAlive() then + local speed = contact.group:GetVelocityKNOTS() + direction = direction + (contact.group:GetHeading()*speed) n=n+1 + speedsum = speedsum + speed end - end - return math.floor(direction / n) + end + + --TODO: This calculation is WRONG! + -- Simple example for two groups: + -- First group is going West, i.e. heading 090 + -- Second group is going East, i.e. heading 270 + -- Total is 360/2=180, i.e. South! + -- It should not go anywhere as the two movements cancel each other. + -- Correct, edge case for N=2^x, but when 2 pairs of groups drive in exact opposite directions, the cluster will split at some point? + -- maybe add the speed as weight to get a weighted factor + + if n==0 then + return 0 + else + return math.floor(direction / (speedsum * n )) + end end --- Calculate cluster speed. -- @param #INTEL self -- @param #INTEL.Cluster cluster The cluster of contacts. --- @return #number Speed average of all groups in the cluster in MPS. +-- @return #number Speed average of all groups in the cluster in MPS. function INTEL:CalcClusterSpeed(cluster) - local velocity = 0 - local n=0 + local velocity = 0 ; local n=0 for _,_contact in pairs(cluster.Contacts) do - local group = _contact.group -- Wrapper.Group#GROUP - if group:IsAlive() then - velocity = velocity + group:GetVelocityMPS() + local contact=_contact --#INTEL.Contact + + if (not contact.isStatic) and contact.group:IsAlive() then + velocity = velocity + contact.group:GetVelocityMPS() n=n+1 end - end - return math.floor(velocity / n) - + + end + + if n==0 then + return 0 + else + return math.floor(velocity / n) + end +end + +--- Calculate cluster velocity vector. +-- @param #INTEL self +-- @param #INTEL.Cluster cluster The cluster of contacts. +-- @return DCS#Vec3 Velocity vector in m/s. +function INTEL:CalcClusterVelocityVec3(cluster) + + local v={x=0, y=0, z=0} --DCS#Vec3 + + for _,_contact in pairs(cluster.Contacts) do + local contact=_contact --#INTEL.Contact + + if (not contact.isStatic) and contact.group:IsAlive() then + local vec=contact.group:GetVelocityVec3() + v.x=v.x+vec.x + v.y=v.y+vec.y + v.z=v.y+vec.z + end + end + + return v end --- Calculate cluster future position after given seconds. -- @param #INTEL self -- @param #INTEL.Cluster cluster The cluster of contacts. --- @param #number seconds Timeframe in seconds. +-- @param #number seconds Time interval in seconds. Default is `self.prediction`. -- @return Core.Point#COORDINATE Calculated future position of the cluster. -function INTEL:CalcClusterFuturePosition(cluster,seconds) - local speed = self:CalcClusterSpeed(cluster) -- #number MPS - local direction = self:CalcClusterDirection(cluster) -- #number heading - -- local currposition = cluster.coordinate -- Core.Point#COORDINATE - local currposition = self:GetClusterCoordinate(cluster) -- Core.Point#COORDINATE - local distance = speed * seconds -- #number in meters the cluster will travel - local futureposition = currposition:Translate(distance,direction,true,false) - if self.clustermarkers and (self.verbose > 1) then +function INTEL:CalcClusterFuturePosition(cluster, seconds) + + -- Get current position of the cluster. + local p=self:GetClusterCoordinate(cluster) + + -- Velocity vector in m/s. + local v=self:CalcClusterVelocityVec3(cluster) + + -- Time in seconds. + local t=seconds or self.prediction + + -- Extrapolated vec3. + local Vec3={x=p.x+v.x*t, y=p.y+v.y*t, z=p.z+v.z*t} + + -- Future position. + local futureposition=COORDINATE:NewFromVec3(Vec3) + + -- Create an arrow pointing in the direction of the movement. + if self.clustermarkers and self.clusterarrows then if cluster.markerID then COORDINATE:RemoveMark(cluster.markerID) end - cluster.markerID = currposition:ArrowToAll(futureposition,self.coalition,{1,0,0},1,{1,1,0},0.5,2,true,"Postion Calc") + cluster.markerID = p:ArrowToAll(futureposition, self.coalition, {1,0,0}, 1, {1,1,0}, 0.5, 2, true, "Position Calc") end + return futureposition end @@ -143496,15 +166284,15 @@ end --- Check if contact is in any known cluster. -- @param #INTEL self -- @param #INTEL.Contact contact The contact. --- @return #boolean If true, contact is in clusters +-- @return #boolean If true, contact is in clusters function INTEL:CheckContactInClusters(contact) for _,_cluster in pairs(self.Clusters) do local cluster=_cluster --#INTEL.Cluster - + for _,_contact in pairs(cluster.Contacts) do local Contact=_contact --#INTEL.Contact - + if Contact.groupname==contact.groupname then return true end @@ -143518,26 +166306,42 @@ end -- @param #INTEL self -- @param #INTEL.Contact contact The contact. -- @param #INTEL.Cluster cluster The cluster the check. --- @return #boolean If true, contact is connected to this cluster. +-- @return #boolean If `true`, contact is connected to this cluster. +-- @return #number Distance to cluster in meters. function INTEL:IsContactConnectedToCluster(contact, cluster) + -- Must be of the same type. We do not want to mix aircraft with ground units. + if contact.ctype~=cluster.ctype then + return false, math.huge + end + for _,_contact in pairs(cluster.Contacts) do local Contact=_contact --#INTEL.Contact - - if Contact.groupname~=contact.groupname then - + + -- Do not calcuate the distance to the contact itself unless it is the only contact in the cluster. + if Contact.groupname~=contact.groupname or cluster.size==1 then + --local dist=Contact.position:Get2DDistance(contact.position) local dist=Contact.position:DistanceFromPointVec2(contact.position) - - local radius = self.clusterradius or 15 - if dist UTILS.FeetToMeters(10000) then -- limit to 10kft + airprox = false + end end - + + if dist UTILS.FeetToMeters(10000) then + airprox = false + end + end + + if dist0 then + avgalt = newalt/n + end + + -- Update cluster coordinate. + Cluster.altitude = avgalt + + self:T(string.format("Updating Cluster Altitude: %d",Cluster.altitude)) + + return Cluster.altitude end --- Get the coordinate of a cluster. -- @param #INTEL self --- @param #INTEL.Cluster cluster The cluster. --- @param Core.Point#COORDINATE coordinate (Optional) Coordinate of the new cluster. Default is to calculate the current coordinate. --- @return #boolean -function INTEL:CheckClusterCoordinateChanged(cluster, coordinate) +-- @param #INTEL.Cluster Cluster The cluster. +-- @param #boolean Update If `true`, update the coordinate. Default is to just return the last stored position. +-- @return Core.Point#COORDINATE The coordinate of this cluster. +function INTEL:GetClusterCoordinate(Cluster, Update) - coordinate=coordinate or self:GetClusterCoordinate(cluster) - - --local dist=cluster.coordinate:Get2DDistance(coordinate) - local dist=cluster.coordinate:DistanceFromPointVec2(coordinate) - - if dist>1000 then + -- Init. + local x=0 ; local y=0 ; local z=0 ; local n=0 + + -- Loop over all contacts. + for _,_contact in pairs(Cluster.Contacts) do + local contact=_contact --#INTEL.Contact + + local vec3=nil --DCS#Vec3 + + if Update and contact.group and contact.group:IsAlive() then + vec3 = contact.group:GetVec3() + end + + if not vec3 then + vec3=contact.position + end + + if vec3 then + + -- Sum up posits. + x=x+vec3.x + y=y+vec3.y + z=z+vec3.z + + -- Increase counter. + n=n+1 + + end + + end + + if n>0 then + + -- Average. + local Vec3={x=x/n, y=y/n, z=z/n} --DCS#Vec3 + + -- Update cluster coordinate. + Cluster.coordinate:UpdateFromVec3(Vec3) + + end + + return Cluster.coordinate +end + +--- Check if the coorindate of the cluster changed. +-- @param #INTEL self +-- @param #INTEL.Cluster Cluster The cluster. +-- @param #number Threshold in meters. Default 100 m. +-- @param Core.Point#COORDINATE Coordinate Reference coordinate. Default is the last known coordinate of the cluster. +-- @return #boolean If `true`, the coordinate changed by more than the given threshold. +function INTEL:_CheckClusterCoordinateChanged(Cluster, Coordinate, Threshold) + + Threshold=Threshold or 100 + + Coordinate=Coordinate or Cluster.coordinate + + -- Positions of cluster. + local a=Coordinate:GetVec3() + local b=self:GetClusterCoordinate(Cluster, true):GetVec3() + + local dist=UTILS.VecDist3D(a,b) + + if dist>Threshold then return true else return false @@ -143636,20 +166570,48 @@ end -- @param #INTEL self function INTEL:_UpdateClusterPositions() for _,_cluster in pairs (self.Clusters) do - local coord = self:GetClusterCoordinate(_cluster) - _cluster.coordinate = coord - self:T(self.lid..string.format("Cluster size: %s", _cluster.size)) + local cluster=_cluster --#INTEL.Cluster + + -- Update cluster coordinate. + local coord = self:GetClusterCoordinate(cluster, true) + local alt = self:GetClusterAltitude(cluster,true) + + -- Debug info. + self:T(self.lid..string.format("Updating Cluster position size: %s", cluster.size)) + end + return self +end + +--- Count number of alive units in contact. +-- @param #INTEL self +-- @param #INTEL.Contact Contact The contact. +-- @return #number unitcount +function INTEL:ContactCountUnits(Contact) + if Contact.isStatic then + if Contact.group and Contact.group:IsAlive() then + return 1 + else + return 0 + end + else + if Contact.group then + local n=Contact.group:CountAliveUnits() + return n + else + return 0 + end end end ---- Count number of units in cluster +--- Count number of alive units in cluster. -- @param #INTEL self -- @param #INTEL.Cluster Cluster The cluster -- @return #number unitcount function INTEL:ClusterCountUnits(Cluster) local unitcount = 0 - for _,_group in pairs (Cluster.Contacts) do -- get Wrapper.GROUP#GROUP _group - unitcount = unitcount + _group.group:CountAliveUnits() + for _,_contact in pairs (Cluster.Contacts) do + local contact=_contact --#INTEL.Contact + unitcount = unitcount + self:ContactCountUnits(contact) end return unitcount end @@ -143662,34 +166624,35 @@ function INTEL:UpdateClusterMarker(cluster) -- Create a marker. local unitcount = self:ClusterCountUnits(cluster) - local text=string.format("Cluster #%d. Size %d, Units %d, TLsum=%d", cluster.index, cluster.size, unitcount, cluster.threatlevelSum) + local text=string.format("Cluster #%d: %s\nSize %d\nUnits %d\nTLsum=%d", cluster.index, cluster.ctype, cluster.size, unitcount, cluster.threatlevelSum) if not cluster.marker then - if self.coalition == coalition.side.RED then - cluster.marker=MARKER:New(cluster.coordinate, text):ToRed() - elseif self.coalition == coalition.side.BLUE then - cluster.marker=MARKER:New(cluster.coordinate, text):ToBlue() - else - cluster.marker=MARKER:New(cluster.coordinate, text):ToNeutral() - end + + -- First time ==> need to create a new marker object. + cluster.marker=MARKER:New(cluster.coordinate, text):ToCoalition(self.coalition) + else - + + -- Need to refresh? local refresh=false - + + -- Check if marker text changed. if cluster.marker.text~=text then cluster.marker.text=text refresh=true end - - if cluster.marker.coordinate~=cluster.coordinate then - cluster.marker.coordinate=cluster.coordinate + + -- Check if coordinate changed. + local coordchange=self:_CheckClusterCoordinateChanged(cluster, cluster.marker.coordinate) + if coordchange then + cluster.marker.coordinate:UpdateFromCoordinate(cluster.coordinate) refresh=true end - + if refresh then cluster.marker:Refresh() end - + end return self @@ -143710,7 +166673,7 @@ end -- * Overcome limitations of (non-available) datalinks between ground radars -- * Detect and track contacts consistently across INTEL instances -- * Use FSM events to link functionality into your scripts --- * Easy setup +-- * Easy setup -- --- === -- @@ -143753,53 +166716,58 @@ INTEL_DLINK.version = "0.0.1" -- @param #string Alias (optional) Name of this instance. Default "SPECTRE" -- @param #number Interval (optional) When to query #INTEL objects for detected items (default 20 seconds). -- @param #number Cachetime (optional) How long to cache detected items (default 300 seconds). --- @usage Use #INTEL_DLINK if you want to merge data from a number of #INTEL objects into one. This might be useful to simulate a +-- @usage Use #INTEL_DLINK if you want to merge data from a number of #INTEL objects into one. This might be useful to simulate a -- Data Link, e.g. for Russian-tech based EWR, realising a Star Topology @{https://en.wikipedia.org/wiki/Network_topology#Star} --- in a basic setup. It will collect the contacts and clusters from the #INTEL objects. +-- in a basic setup. It will collect the contacts and clusters from the #INTEL objects. -- Contact duplicates are removed. Clusters might contain duplicates (Might fix that later, WIP). -- --- Basic setup: --- local datalink = INTEL_DLINK:New({myintel1,myintel2}), "FSB", 20, 300) --- datalink:__Start(2) +-- Basic setup: +-- +-- local datalink = INTEL_DLINK:New({myintel1,myintel2}), "FSB", 20, 300) +-- datalink:__Start(2) -- -- Add an Intel while running: --- datalink:AddIntel(myintel3) +-- +-- datalink:AddIntel(myintel3) -- -- Gather the data: --- datalink:GetContactTable() -- #table of #INTEL.Contact contacts. --- datalink:GetClusterTable() -- #table of #INTEL.Cluster clusters. --- datalink:GetDetectedItemCoordinates() -- #table of contact coordinates, to be compatible with @{Functional.Detection#DETECTION}. -- --- Gather data with the event function: --- function datalink:OnAfterCollected(From, Event, To, Contacts, Clusters) --- ... ... --- end --- +-- datalink:GetContactTable() -- #table of #INTEL.Contact contacts. +-- datalink:GetClusterTable() -- #table of #INTEL.Cluster clusters. +-- datalink:GetDetectedItemCoordinates() -- #table of contact coordinates, to be compatible with @{Functional.Detection#DETECTION}. +-- +-- Gather data with the event function: +-- +-- function datalink:OnAfterCollected(From, Event, To, Contacts, Clusters) +-- ... ... +-- end +-- function INTEL_DLINK:New(Intels, Alias, Interval, Cachetime) + -- Inherit everything from FSM class. - local self=BASE:Inherit(self, FSM:New()) -- #INTEL - + local self=BASE:Inherit(self, FSM:New()) -- #INTEL_DLINK + self.intels = Intels or {} self.contacts = {} self.clusters = {} self.contactcoords = {} - + -- Set alias. if Alias then self.alias=tostring(Alias) else self.alias="SPECTRE" end - + -- Cache time self.cachetime = Cachetime or 300 - + -- Interval self.interval = Interval or 20 - + -- Set some string id for output to DCS.log file. self.lid=string.format("INTEL_DLINK %s | ", self.alias) - + -- Start State. self:SetStartState("Stopped") @@ -143809,7 +166777,7 @@ function INTEL_DLINK:New(Intels, Alias, Interval, Cachetime) self:AddTransition("*", "Collect", "*") -- Collect data. self:AddTransition("*", "Collected", "*") -- Collection of data done. self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. - + ---------------------------------------------------------------------------------------------- -- Pseudo Functions ---------------------------------------------------------------------------------------------- @@ -143838,7 +166806,7 @@ function INTEL_DLINK:New(Intels, Alias, Interval, Cachetime) -- @function [parent=#INTEL_DLINK] __Status -- @param #INTEL_DLINK self -- @param #number delay Delay in seconds. - + --- On After "Collected" event. Data tables have been refreshed. -- @function [parent=#INTEL_DLINK] OnAfterCollected -- @param #INTEL_DLINK self @@ -143847,7 +166815,7 @@ function INTEL_DLINK:New(Intels, Alias, Interval, Cachetime) -- @param #string To To state. -- @param #table Contacts Table of #INTEL.Contact Contacts. -- @param #table Clusters Table of #INTEL.Cluster Clusters. - + return self end ---------------------------------------------------------------------------------------------- @@ -143944,7 +166912,7 @@ function INTEL_DLINK:onbeforeCollect(From, Event, To) self:__Collected(1, contacttable, newclusters) -- make table available via FSM Event -- schedule next round local interv = self.interval * -1 - self:__Collect(interv) + self:__Collect(interv) return self end @@ -144001,6 +166969,4230 @@ end ---------------------------------------------------------------------------------------------- -- End INTEL_DLINK ---------------------------------------------------------------------------------------------- +--- **Ops** - Commander of Airwings, Brigades and Fleets. +-- +-- **Main Features:** +-- +-- * Manages AIRWINGS, BRIGADEs and FLEETs +-- * Handles missions (AUFTRAG) and finds the best assets for the job +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module Ops.Commander +-- @image OPS_Commander.png + + +--- COMMANDER class. +-- @type COMMANDER +-- @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 #number coalition Coalition side of the commander. +-- @field #string alias Alias name. +-- @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`. +-- @field #table gcicapZones GCICAP zones. Each element is of type `#AIRWING.PatrolZone`. +-- @field #table awacsZones AWACS zones. Each element is of type `#AIRWING.PatrolZone`. +-- @field #table tankerZones Tanker zones. Each element is of type `#AIRWING.TankerZone`. +-- @field Ops.Chief#CHIEF chief Chief of staff. +-- @field #table limitMission Table of limits for mission types. +-- @extends Core.Fsm#FSM + +--- *He who has never leared to obey cannot be a good commander.* -- Aristotle +-- +-- === +-- +-- # The COMMANDER Concept +-- +-- A commander is the head of legions. He/she will find the best LEGIONs to perform an assigned AUFTRAG (mission) or OPSTRANSPORT. +-- A legion can be an AIRWING, BRIGADE or FLEET. +-- +-- # Constructor +-- +-- A new COMMANDER object is created with the @{#COMMANDER.New}(*Coalition, Alias*) function, where the parameter *Coalition* is the coalition side. +-- It can be `coalition.side.RED`, `coalition.side.BLUE` or `coalition.side.NEUTRAL`. This parameter is mandatory! +-- +-- The second parameter *Alias* is optional and can be used to give the COMMANDER a "name", which is used for output in the dcs.log file. +-- +-- local myCommander=COMANDER:New(coalition.side.BLUE, "General Patton") +-- +-- # Adding Legions +-- +-- Legions, i.e. AIRWINGS, BRIGADES and FLEETS can be added via the @{#COMMANDER.AddLegion}(*Legion*) command: +-- +-- myCommander:AddLegion(myLegion) +-- +-- ## Adding Airwings +-- +-- It is also possible to use @{#COMMANDER.AddAirwing}(*myAirwing*) function. This does the same as the `AddLegion` function but might be a bit more intuitive. +-- +-- ## Adding Brigades +-- +-- It is also possible to use @{#COMMANDER.AddBrigade}(*myBrigade*) function. This does the same as the `AddLegion` function but might be a bit more intuitive. +-- +-- ## Adding Fleets +-- +-- It is also possible to use @{#COMMANDER.AddFleet}(*myFleet*) function. This does the same as the `AddLegion` function but might be a bit more intuitive. +-- +-- # Adding Missions +-- +-- Mission can be added via the @{#COMMANDER.AddMission}(*myMission*) function. +-- +-- # Adding OPS Transports +-- +-- Transportation assignments can be added via the @{#COMMANDER.AddOpsTransport}(*myTransport*) function. +-- +-- # Adding CAP Zones +-- +-- A CAP zone can be added via the @{#COMMANDER.AddCapZone}() function. +-- +-- # Adding Rearming Zones +-- +-- A rearming zone can be added via the @{#COMMANDER.AddRearmingZone}() function. +-- +-- # Adding Refuelling Zones +-- +-- A refuelling zone can be added via the @{#COMMANDER.AddRefuellingZone}() function. +-- +-- +-- # FSM Events +-- +-- The COMMANDER will +-- +-- ## OPSGROUP on Mission +-- +-- Whenever an OPSGROUP (FLIGHTGROUP, ARMYGROUP or NAVYGROUP) is send on a mission, the `OnAfterOpsOnMission()` event is triggered. +-- Mission designers can hook into the event with the @{#COMMANDER.OnAfterOpsOnMission}() function +-- +-- function myCommander:OnAfterOpsOnMission(From, Event, To, OpsGroup, Mission) +-- -- Your code +-- end +-- +-- ## Canceling a Mission +-- +-- A mission can be cancelled with the @{#COMMMANDER.MissionCancel}() function +-- +-- myCommander:MissionCancel(myMission) +-- +-- or +-- myCommander:__MissionCancel(5*60, myMission) +-- +-- The last commander cancels the mission after 5 minutes (300 seconds). +-- +-- The cancel command will be forwarded to all assigned legions and OPS groups, which will abort their mission or remove it from their queue. +-- +-- @field #COMMANDER +COMMANDER = { + ClassName = "COMMANDER", + verbose = 0, + coalition = nil, + legions = {}, + missionqueue = {}, + transportqueue = {}, + targetqueue = {}, + opsqueue = {}, + rearmingZones = {}, + refuellingZones = {}, + capZones = {}, + gcicapZones = {}, + awacsZones = {}, + tankerZones = {}, + limitMission = {}, +} + +--- COMMANDER class version. +-- @field #string version +COMMANDER.version="0.1.3" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- DONE: Add CAP zones. +-- DONE: Add tanker zones. +-- DONE: Improve legion selection. Mostly done! +-- DONE: Find solution for missions, which require a transport. This is not as easy as it sounds since the selected mission assets restrict the possible transport assets. +-- DONE: Add ops transports. +-- DONE: Allow multiple Legions for one mission. +-- NOGO: Maybe it's possible to preselect the assets for the mission. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new COMMANDER object and start the FSM. +-- @param #COMMANDER self +-- @param #number Coalition Coaliton of the commander. +-- @param #string Alias Some name you want the commander to be called. +-- @return #COMMANDER self +function COMMANDER:New(Coalition, Alias) + + -- Inherit everything from INTEL class. + local self=BASE:Inherit(self, FSM:New()) --#COMMANDER + + if Coalition==nil then + env.error("ERROR: Coalition parameter is nil in COMMANDER:New() call!") + return nil + end + + -- Set coaliton. + self.coalition=Coalition + + -- Alias name. + self.alias=Alias + + -- Choose a name for red or blue. + if self.alias==nil then + if Coalition==coalition.side.BLUE then + self.alias="George S. Patton" + elseif Coalition==coalition.side.RED then + self.alias="Georgy Zhukov" + elseif Coalition==coalition.side.NEUTRAL then + self.alias="Mahatma Gandhi" + end + end + + -- Log ID. + self.lid=string.format("COMMANDER %s [%s] | ", self.alias, UTILS.GetCoalitionName(self.coalition)) + + -- Start state. + self:SetStartState("NotReadyYet") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("NotReadyYet", "Start", "OnDuty") -- Start COMMANDER. + self:AddTransition("*", "Status", "*") -- Status report. + self:AddTransition("*", "Stop", "Stopped") -- Stop COMMANDER. + + self:AddTransition("*", "MissionAssign", "*") -- Mission is assigned to a or multiple LEGIONs. + self:AddTransition("*", "MissionCancel", "*") -- COMMANDER cancels a mission. + + self:AddTransition("*", "TransportAssign", "*") -- Transport is assigned to a or multiple LEGIONs. + self:AddTransition("*", "TransportCancel", "*") -- COMMANDER cancels a Transport. + + self:AddTransition("*", "OpsOnMission", "*") -- An OPSGROUP was send on a Mission (AUFTRAG). + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the COMMANDER. + -- @function [parent=#COMMANDER] Start + -- @param #COMMANDER self + + --- Triggers the FSM event "Start" after a delay. Starts the COMMANDER. + -- @function [parent=#COMMANDER] __Start + -- @param #COMMANDER self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Stop". Stops the COMMANDER. + -- @param #COMMANDER self + + --- Triggers the FSM event "Stop" after a delay. Stops the COMMANDER. + -- @function [parent=#COMMANDER] __Stop + -- @param #COMMANDER self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Status". + -- @function [parent=#COMMANDER] Status + -- @param #COMMANDER self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#COMMANDER] __Status + -- @param #COMMANDER self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "MissionAssign". Mission is added to a LEGION mission queue and already requested. Needs assets to be added to the mission! + -- @function [parent=#COMMANDER] MissionAssign + -- @param #COMMANDER self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Legions The Legion(s) to which the mission is assigned. + + --- Triggers the FSM event "MissionAssign" after a delay. Mission is added to a LEGION mission queue and already requested. Needs assets to be added to the mission! + -- @function [parent=#COMMANDER] __MissionAssign + -- @param #COMMANDER self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Legions The Legion(s) to which the mission is assigned. + + --- On after "MissionAssign" event. + -- @function [parent=#COMMANDER] OnAfterMissionAssign + -- @param #COMMANDER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Legions The Legion(s) to which the mission is assigned. + + + --- Triggers the FSM event "MissionCancel". + -- @function [parent=#COMMANDER] MissionCancel + -- @param #COMMANDER self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "MissionCancel" after a delay. + -- @function [parent=#COMMANDER] __MissionCancel + -- @param #COMMANDER self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "MissionCancel" event. + -- @function [parent=#COMMANDER] OnAfterMissionCancel + -- @param #COMMANDER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "TransportAssign". + -- @function [parent=#COMMANDER] TransportAssign + -- @param #COMMANDER self + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + -- @param #table Legions The legion(s) to which this transport is assigned. + + --- Triggers the FSM event "TransportAssign" after a delay. + -- @function [parent=#COMMANDER] __TransportAssign + -- @param #COMMANDER self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + -- @param #table Legions The legion(s) to which this transport is assigned. + + --- On after "TransportAssign" event. + -- @function [parent=#COMMANDER] OnAfterTransportAssign + -- @param #COMMANDER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + -- @param #table Legions The legion(s) to which this transport is assigned. + + + --- Triggers the FSM event "TransportCancel". + -- @function [parent=#COMMANDER] TransportCancel + -- @param #COMMANDER self + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- Triggers the FSM event "TransportCancel" after a delay. + -- @function [parent=#COMMANDER] __TransportCancel + -- @param #COMMANDER self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- On after "TransportCancel" event. + -- @function [parent=#COMMANDER] OnAfterTransportCancel + -- @param #COMMANDER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + + --- Triggers the FSM event "OpsOnMission". + -- @function [parent=#COMMANDER] OpsOnMission + -- @param #COMMANDER self + -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "OpsOnMission" after a delay. + -- @function [parent=#COMMANDER] __OpsOnMission + -- @param #COMMANDER self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "OpsOnMission" event. + -- @function [parent=#COMMANDER] OnAfterOpsOnMission + -- @param #COMMANDER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set verbosity level. +-- @param #COMMANDER self +-- @param #number VerbosityLevel Level of output (higher=more). Default 0. +-- @return #COMMANDER self +function COMMANDER:SetVerbosity(VerbosityLevel) + self.verbose=VerbosityLevel or 0 + return self +end + +--- Set limit for number of total or specific missions to be executed simultaniously. +-- @param #COMMANDER self +-- @param #number Limit Number of max. mission of this type. Default 10. +-- @param #string MissionType Type of mission, e.g. `AUFTRAG.Type.BAI`. Default `"Total"` for total number of missions. +-- @return #COMMANDER self +function COMMANDER:SetLimitMission(Limit, MissionType) + MissionType=MissionType or "Total" + if MissionType then + self.limitMission[MissionType]=Limit or 10 + else + self:E(self.lid.."ERROR: No mission type given for setting limit!") + end + return self +end + + +--- Get coalition. +-- @param #COMMANDER self +-- @return #number Coalition. +function COMMANDER:GetCoalition() + return self.coalition +end + +--- Add an AIRWING to the commander. +-- @param #COMMANDER self +-- @param Ops.AirWing#AIRWING Airwing The airwing to add. +-- @return #COMMANDER self +function COMMANDER:AddAirwing(Airwing) + + -- Add legion. + self:AddLegion(Airwing) + + return self +end + +--- Add a BRIGADE to the commander. +-- @param #COMMANDER self +-- @param Ops.Brigade#BRIGADE Brigade The brigade to add. +-- @return #COMMANDER self +function COMMANDER:AddBrigade(Brigade) + + -- Add legion. + self:AddLegion(Brigade) + + return self +end + +--- Add a FLEET to the commander. +-- @param #COMMANDER self +-- @param Ops.Fleet#FLEET Fleet The fleet to add. +-- @return #COMMANDER self +function COMMANDER:AddFleet(Fleet) + + -- Add legion. + self:AddLegion(Fleet) + + return self +end + + +--- Add a LEGION to the commander. +-- @param #COMMANDER self +-- @param Ops.Legion#LEGION Legion The legion to add. +-- @return #COMMANDER self +function COMMANDER:AddLegion(Legion) + + -- This legion is managed by the commander. + Legion.commander=self + + -- Add to legions. + table.insert(self.legions, Legion) + + return self +end + +--- Add mission to mission queue. +-- @param #COMMANDER self +-- @param Ops.Auftrag#AUFTRAG Mission Mission to be added. +-- @return #COMMANDER self +function COMMANDER:AddMission(Mission) + + if not self:IsMission(Mission) then + + Mission.commander=self + + Mission.statusCommander=AUFTRAG.Status.PLANNED + + table.insert(self.missionqueue, Mission) + + end + + return self +end + +--- Add transport to queue. +-- @param #COMMANDER self +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The OPS transport to be added. +-- @return #COMMANDER self +function COMMANDER:AddOpsTransport(Transport) + + Transport.commander=self + + Transport.statusCommander=TRANSPORT.Status.PLANNED + + table.insert(self.transportqueue, Transport) + + return self +end + +--- Remove mission from queue. +-- @param #COMMANDER self +-- @param Ops.Auftrag#AUFTRAG Mission Mission to be removed. +-- @return #COMMANDER self +function COMMANDER:RemoveMission(Mission) + + for i,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission.auftragsnummer==Mission.auftragsnummer then + self:T(self.lid..string.format("Removing mission %s (%s) status=%s from queue", Mission.name, Mission.type, Mission.status)) + mission.commander=nil + table.remove(self.missionqueue, i) + break + end + + end + + return self +end + +--- Remove transport from queue. +-- @param #COMMANDER self +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The OPS transport to be removed. +-- @return #COMMANDER self +function COMMANDER:RemoveTransport(Transport) + + for i,_transport in pairs(self.transportqueue) do + local transport=_transport --Ops.OpsTransport#OPSTRANSPORT + + if transport.uid==Transport.uid then + self:T(self.lid..string.format("Removing transport UID=%d status=%s from queue", transport.uid, transport:GetState())) + transport.commander=nil + table.remove(self.transportqueue, i) + break + end + + end + + 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. +-- @return Ops.Brigade#BRIGADE.SupplyZone The rearming zone data. +function COMMANDER:AddRearmingZone(RearmingZone) + + local rearmingzone={} --Ops.Brigade#BRIGADE.SupplyZone + + rearmingzone.zone=RearmingZone + rearmingzone.mission=nil + --rearmingzone.marker=MARKER:New(rearmingzone.zone:GetCoordinate(), "Rearming Zone"):ToCoalition(self:GetCoalition()) + + table.insert(self.rearmingZones, rearmingzone) + + return rearmingzone +end + +--- Add a refuelling zone. +-- @param #COMMANDER self +-- @param Core.Zone#ZONE RefuellingZone Refuelling zone. +-- @return Ops.Brigade#BRIGADE.SupplyZone The refuelling zone data. +function COMMANDER:AddRefuellingZone(RefuellingZone) + + local rearmingzone={} --Ops.Brigade#BRIGADE.SupplyZone + + rearmingzone.zone=RefuellingZone + rearmingzone.mission=nil + --rearmingzone.marker=MARKER:New(rearmingzone.zone:GetCoordinate(), "Refuelling Zone"):ToCoalition(self:GetCoalition()) + + table.insert(self.refuellingZones, rearmingzone) + + return rearmingzone +end + +--- Add a CAP zone. +-- @param #COMMANDER self +-- @param Core.Zone#ZONE Zone CapZone Zone. +-- @param #number Altitude Orbit altitude in feet. Default is 12,0000 feet. +-- @param #number Speed Orbit speed in KIAS. Default 350 kts. +-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West). +-- @param #number Leg Length of race-track in NM. Default 30 NM. +-- @return Ops.AirWing#AIRWING.PatrolZone The CAP zone data. +function COMMANDER:AddCapZone(Zone, Altitude, Speed, Heading, Leg) + + local patrolzone={} --Ops.AirWing#AIRWING.PatrolZone + + patrolzone.zone=Zone + patrolzone.altitude=Altitude or 12000 + patrolzone.heading=Heading or 270 + patrolzone.speed=UTILS.KnotsToAltKIAS(Speed or 350, patrolzone.altitude) + patrolzone.leg=Leg or 30 + patrolzone.mission=nil + --patrolzone.marker=MARKER:New(patrolzone.zone:GetCoordinate(), "CAP Zone"):ToCoalition(self:GetCoalition()) + + table.insert(self.capZones, patrolzone) + + return patrolzone +end + +--- Add a GCICAP zone. +-- @param #COMMANDER self +-- @param Core.Zone#ZONE Zone CapZone Zone. +-- @param #number Altitude Orbit altitude in feet. Default is 12,0000 feet. +-- @param #number Speed Orbit speed in KIAS. Default 350 kts. +-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West). +-- @param #number Leg Length of race-track in NM. Default 30 NM. +-- @return Ops.AirWing#AIRWING.PatrolZone The CAP zone data. +function COMMANDER:AddGciCapZone(Zone, Altitude, Speed, Heading, Leg) + + local patrolzone={} --Ops.AirWing#AIRWING.PatrolZone + + patrolzone.zone=Zone + patrolzone.altitude=Altitude or 12000 + patrolzone.heading=Heading or 270 + patrolzone.speed=UTILS.KnotsToAltKIAS(Speed or 350, patrolzone.altitude) + patrolzone.leg=Leg or 30 + patrolzone.mission=nil + --patrolzone.marker=MARKER:New(patrolzone.zone:GetCoordinate(), "GCICAP Zone"):ToCoalition(self:GetCoalition()) + + table.insert(self.gcicapZones, patrolzone) + + return patrolzone +end + +--- Add an AWACS zone. +-- @param #COMMANDER self +-- @param Core.Zone#ZONE Zone Zone. +-- @param #number Altitude Orbit altitude in feet. Default is 12,0000 feet. +-- @param #number Speed Orbit speed in KIAS. Default 350 kts. +-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West). +-- @param #number Leg Length of race-track in NM. Default 30 NM. +-- @return Ops.AirWing#AIRWING.PatrolZone The AWACS zone data. +function COMMANDER:AddAwacsZone(Zone, Altitude, Speed, Heading, Leg) + + local awacszone={} --Ops.AirWing#AIRWING.PatrolZone + + awacszone.zone=Zone + awacszone.altitude=Altitude or 12000 + awacszone.heading=Heading or 270 + awacszone.speed=UTILS.KnotsToAltKIAS(Speed or 350, awacszone.altitude) + awacszone.leg=Leg or 30 + awacszone.mission=nil + --awacszone.marker=MARKER:New(awacszone.zone:GetCoordinate(), "AWACS Zone"):ToCoalition(self:GetCoalition()) + + table.insert(self.awacsZones, awacszone) + + return awacszone +end + +--- Add a refuelling tanker zone. +-- @param #COMMANDER self +-- @param Core.Zone#ZONE Zone Zone. +-- @param #number Altitude Orbit altitude in feet. Default is 12,0000 feet. +-- @param #number Speed Orbit speed in KIAS. Default 350 kts. +-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West). +-- @param #number Leg Length of race-track in NM. Default 30 NM. +-- @param #number RefuelSystem Refuelling system. +-- @return Ops.AirWing#AIRWING.TankerZone The tanker zone data. +function COMMANDER:AddTankerZone(Zone, Altitude, Speed, Heading, Leg, RefuelSystem) + + local tankerzone={} --Ops.AirWing#AIRWING.TankerZone + + tankerzone.zone=Zone + tankerzone.altitude=Altitude or 12000 + tankerzone.heading=Heading or 270 + tankerzone.speed=UTILS.KnotsToAltKIAS(Speed or 350, tankerzone.altitude) + tankerzone.leg=Leg or 30 + tankerzone.refuelsystem=RefuelSystem + tankerzone.mission=nil + tankerzone.marker=MARKER:New(tankerzone.zone:GetCoordinate(), "Tanker Zone"):ToCoalition(self:GetCoalition()) + + table.insert(self.tankerZones, tankerzone) + + return tankerzone +end + +--- Check if this mission is already in the queue. +-- @param #COMMANDER self +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @return #boolean If `true`, this mission is in the queue. +function COMMANDER:IsMission(Mission) + + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + if mission.auftragsnummer==Mission.auftragsnummer then + return true + end + end + + return false +end + +--- Relocate a cohort to another legion. +-- Assets in stock are spawned and routed to the new legion. +-- If assets are spawned, running missions will be cancelled. +-- Cohort assets will not be available until relocation is finished. +-- @param #COMMANDER self +-- @param Ops.Cohort#COHORT Cohort The cohort to be relocated. +-- @param Ops.Legion#LEGION Legion The legion where the cohort is relocated to. +-- @param #number Delay Delay in seconds before relocation takes place. Default `nil`, *i.e.* ASAP. +-- @param #number NcarriersMin Min number of transport carriers in case the troops should be transported. Default `nil` for no transport. +-- @param #number NcarriersMax Max number of transport carriers. +-- @param #table TransportLegions Legion(s) assigned for transportation. Default is all legions of the commander. +-- @return #COMMANDER self +function COMMANDER:RelocateCohort(Cohort, Legion, Delay, NcarriersMin, NcarriersMax, TransportLegions) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, COMMANDER.RelocateCohort, self, Cohort, Legion, 0, NcarriersMin, NcarriersMax, TransportLegions) + else + + -- Add cohort to legion. + if Legion:IsCohort(Cohort.name) then + self:E(self.lid..string.format("ERROR: Cohort %s is already part of new legion %s ==> CANNOT Relocate!", Cohort.name, Legion.alias)) + return self + else + table.insert(Legion.cohorts, Cohort) + end + + -- Old legion. + local LegionOld=Cohort.legion + + -- Check that cohort is part of this legion + if not LegionOld:IsCohort(Cohort.name) then + self:E(self.lid..string.format("ERROR: Cohort %s is NOT part of this legion %s ==> CANNOT Relocate!", Cohort.name, self.alias)) + return self + end + + -- Check that legions are different. + if LegionOld.alias==Legion.alias then + self:E(self.lid..string.format("ERROR: old legion %s is same as new legion %s ==> CANNOT Relocate!", LegionOld.alias, Legion.alias)) + return self + end + + -- Trigger Relocate event. + Cohort:Relocate() + + -- Create a relocation mission. + local mission=AUFTRAG:_NewRELOCATECOHORT(Legion, Cohort) + + -- Assign cohort to mission. + mission:AssignCohort(Cohort) + + -- All assets required. + mission:SetRequiredAssets(#Cohort.assets) + + -- Set transportation. + if NcarriersMin and NcarriersMin>0 then + mission:SetRequiredTransport(Legion.spawnzone, NcarriersMin, NcarriersMax) + end + + -- Assign transport legions. + if TransportLegions then + for _,legion in pairs(TransportLegions) do + mission:AssignTransportLegion(legion) + end + else + for _,legion in pairs(self.legions) do + mission:AssignTransportLegion(legion) + end + end + + -- Set mission range very large. Mission designer should know... + mission:SetMissionRange(10000) + + -- Add mission. + self:AddMission(mission) + + end + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Start & Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the FLIGHTGROUP FSM and event handlers. +-- @param #COMMANDER self +-- @param Wrapper.Group#GROUP Group Flight group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function COMMANDER:onafterStart(From, Event, To) + + -- Short info. + local text=string.format("Starting Commander") + self:I(self.lid..text) + + -- Start attached legions. + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + if legion:GetState()=="NotReadyYet" then + legion:Start() + end + end + + self:__Status(-1) +end + +--- On after "Status" event. +-- @param #COMMANDER self +-- @param Wrapper.Group#GROUP Group Flight group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function COMMANDER:onafterStatus(From, Event, To) + + -- FSM state. + local fsmstate=self:GetState() + + -- Status. + if self.verbose>=1 then + 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() + + -- Check transport queue and assign one PLANNED transport. + self:CheckTransportQueue() + + -- Check rearming zones. + for _,_rearmingzone in pairs(self.rearmingZones) do + local rearmingzone=_rearmingzone --Ops.Brigade#BRIGADE.SupplyZone + -- Check if mission is nil or over. + if (not rearmingzone.mission) or rearmingzone.mission:IsOver() then + rearmingzone.mission=AUFTRAG:NewAMMOSUPPLY(rearmingzone.zone) + self:AddMission(rearmingzone.mission) + end + end + + -- Check refuelling zones. + for _,_supplyzone in pairs(self.refuellingZones) do + local supplyzone=_supplyzone --Ops.Brigade#BRIGADE.SupplyZone + -- Check if mission is nil or over. + if (not supplyzone.mission) or supplyzone.mission:IsOver() then + supplyzone.mission=AUFTRAG:NewFUELSUPPLY(supplyzone.zone) + self:AddMission(supplyzone.mission) + end + end + + + -- Check CAP zones. + for _,_patrolzone in pairs(self.capZones) do + local patrolzone=_patrolzone --Ops.AirWing#AIRWING.PatrolZone + -- Check if mission is nil or over. + if (not patrolzone.mission) or patrolzone.mission:IsOver() then + local Coordinate=patrolzone.zone:GetCoordinate() + patrolzone.mission=AUFTRAG:NewCAP(patrolzone.zone, patrolzone.altitude, patrolzone.speed, Coordinate, patrolzone.heading, patrolzone.leg) + self:AddMission(patrolzone.mission) + end + end + + -- Check GCICAP zones. + for _,_patrolzone in pairs(self.gcicapZones) do + local patrolzone=_patrolzone --Ops.AirWing#AIRWING.PatrolZone + -- Check if mission is nil or over. + if (not patrolzone.mission) or patrolzone.mission:IsOver() then + local Coordinate=patrolzone.zone:GetCoordinate() + patrolzone.mission=AUFTRAG:NewGCICAP(Coordinate, patrolzone.altitude, patrolzone.speed, patrolzone.heading, patrolzone.leg) + self:AddMission(patrolzone.mission) + end + end + + -- Check AWACS zones. + for _,_awacszone in pairs(self.awacsZones) do + local awacszone=_awacszone --Ops.AirWing#AIRWING.Patrol + -- Check if mission is nil or over. + if (not awacszone.mission) or awacszone.mission:IsOver() then + local Coordinate=awacszone.zone:GetCoordinate() + awacszone.mission=AUFTRAG:NewAWACS(Coordinate, awacszone.altitude, awacszone.speed, awacszone.heading, awacszone.leg) + self:AddMission(awacszone.mission) + end + end + + -- Check Tanker zones. + for _,_tankerzone in pairs(self.tankerZones) do + local tankerzone=_tankerzone --Ops.AirWing#AIRWING.TankerZone + -- Check if mission is nil or over. + if (not tankerzone.mission) or tankerzone.mission:IsOver() then + local Coordinate=tankerzone.zone:GetCoordinate() + tankerzone.mission=AUFTRAG:NewTANKER(Coordinate, tankerzone.altitude, tankerzone.speed, tankerzone.heading, tankerzone.leg, tankerzone.refuelsystem) + self:AddMission(tankerzone.mission) + end + end + + --- + -- LEGIONS + --- + + if self.verbose>=2 and #self.legions>0 then + + local text="Legions:" + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + local Nassets=legion:CountAssets() + local Nastock=legion:CountAssets(true) + text=text..string.format("\n* %s [%s]: Assets=%s stock=%s", legion.alias, legion:GetState(), Nassets, Nastock) + for _,aname in pairs(AUFTRAG.Type) do + local na=legion:CountAssets(true, {aname}) + local np=legion:CountPayloadsInStock({aname}) + local nm=legion:CountAssetsOnMission({aname}) + if na>0 or np>0 then + text=text..string.format("\n - %s: assets=%d, payloads=%d, on mission=%d", aname, na, np, nm) + end + end + end + self:T(self.lid..text) + + + if self.verbose>=3 then + + -- Count numbers + local Ntotal=0 + local Nspawned=0 + local Nrequested=0 + local Nreserved=0 + local Nstock=0 + + local text="\n===========================================\n" + text=text.."Assets:" + 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 + + for _,_asset in pairs(cohort.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + local state="In Stock" + if asset.flightgroup then + state=asset.flightgroup:GetState() + local mission=legion:GetAssetCurrentMission(asset) + if mission then + state=state..string.format(", Mission \"%s\" [%s]", mission:GetName(), mission:GetType()) + end + else + if asset.spawned then + env.info("FF ERROR: asset has opsgroup but is NOT spawned!") + end + if asset.requested and asset.isReserved then + env.info("FF ERROR: asset is requested and reserved. Should not be both!") + state="Reserved+Requested!" + elseif asset.isReserved then + state="Reserved" + elseif asset.requested then + state="Requested" + end + end + + -- Text. + text=text..string.format("\n[UID=%03d] %s Legion=%s [%s]: State=%s [RID=%s]", + asset.uid, asset.spawngroupname, legion.alias, cohort.name, state, tostring(asset.rid)) + + + if asset.spawned then + Nspawned=Nspawned+1 + end + if asset.requested then + Nrequested=Nrequested+1 + end + if asset.isReserved then + Nreserved=Nreserved+1 + end + if not (asset.spawned or asset.requested or asset.isReserved) then + Nstock=Nstock+1 + end + + Ntotal=Ntotal+1 + + end + + end + + end + text=text.."\n-------------------------------------------" + text=text..string.format("\nNstock = %d", Nstock) + text=text..string.format("\nNreserved = %d", Nreserved) + text=text..string.format("\nNrequested = %d", Nrequested) + text=text..string.format("\nNspawned = %d", Nspawned) + text=text..string.format("\nNtotal = %d (=%d)", Ntotal, Nstock+Nspawned+Nrequested+Nreserved) + text=text.."\n===========================================" + self:I(self.lid..text) + end + + end + + --- + -- MISSIONS + --- + + -- Mission queue. + if self.verbose>=2 and #self.missionqueue>0 then + local text="Mission queue:" + for i,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + local target=mission:GetTargetName() or "unknown" + text=text..string.format("\n[%d] %s (%s): status=%s, target=%s", i, mission.name, mission.type, mission.status, target) + 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 + --- + + -- Transport queue. + if self.verbose>=2 and #self.transportqueue>0 then + local text="Transport queue:" + for i,_transport in pairs(self.transportqueue) do + local transport=_transport --Ops.OpsTransport#OPSTRANSPORT + text=text..string.format("\n[%d] UID=%d: status=%s", i, transport.uid, transport:GetState()) + end + self:I(self.lid..text) + end + + self:__Status(-30) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Events +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "MissionAssign" event. Mission is added to a LEGION mission queue and already requested. Needs assets to be added to the mission already. +-- @param #COMMANDER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @param #table Legions The Legion(s) to which the mission is assigned. +function COMMANDER:onafterMissionAssign(From, Event, To, Mission, Legions) + + -- Add mission to queue. + self:AddMission(Mission) + + -- Set mission commander status to QUEUED as it is now queued at a legion. + Mission.statusCommander=AUFTRAG.Status.QUEUED + + for _,_Legion in pairs(Legions) do + local Legion=_Legion --Ops.Legion#LEGION + + -- Debug info. + self:T(self.lid..string.format("Assigning mission \"%s\" [%s] to legion \"%s\"", Mission.name, Mission.type, Legion.alias)) + + -- Add mission to legion. + Legion:AddMission(Mission) + + -- Directly request the mission as the assets have already been selected. + Legion:MissionRequest(Mission) + + end + +end + +--- On after "MissionCancel" event. +-- @param #COMMANDER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +function COMMANDER:onafterMissionCancel(From, Event, To, Mission) + + -- Debug info. + self:T(self.lid..string.format("Cancelling mission \"%s\" [%s] in status %s", Mission.name, Mission.type, Mission.status)) + + -- Set commander status. + Mission.statusCommander=AUFTRAG.Status.CANCELLED + + if Mission:IsPlanned() then + + -- Mission is still in planning stage. Should not have a legion assigned ==> Just remove it form the queue. + self:RemoveMission(Mission) + + else + + -- Legion will cancel mission. + if #Mission.legions>0 then + for _,_legion in pairs(Mission.legions) do + local legion=_legion --Ops.Legion#LEGION + + -- TODO: Should check that this legions actually belongs to this commander. + + -- Legion will cancel the mission. + legion:MissionCancel(Mission) + end + end + + end + +end + +--- On after "TransportAssign" event. Transport is added to a LEGION transport queue. +-- @param #COMMANDER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + -- @param #table Legions The legion(s) to which this transport is assigned. +function COMMANDER:onafterTransportAssign(From, Event, To, Transport, Legions) + + -- Set mission commander status to QUEUED as it is now queued at a legion. + Transport.statusCommander=OPSTRANSPORT.Status.QUEUED + + for _,_Legion in pairs(Legions) do + local Legion=_Legion --Ops.Legion#LEGION + + -- Debug info. + self:T(self.lid..string.format("Assigning transport UID=%d to legion \"%s\"", Transport.uid, Legion.alias)) + + -- Add mission to legion. + Legion:AddOpsTransport(Transport) + + -- Directly request the mission as the assets have already been selected. + Legion:TransportRequest(Transport) + + end + +end + +--- On after "TransportCancel" event. +-- @param #COMMANDER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. +function COMMANDER:onafterTransportCancel(From, Event, To, Transport) + + -- Debug info. + self:T(self.lid..string.format("Cancelling Transport UID=%d in status %s", Transport.uid, Transport:GetState())) + + -- Set commander status. + Transport.statusCommander=OPSTRANSPORT.Status.CANCELLED + + if Transport:IsPlanned() then + + -- Transport is still in planning stage. Should not have a legion assigned ==> Just remove it form the queue. + self:RemoveTransport(Transport) + + else + + -- Legion will cancel mission. + if #Transport.legions>0 then + for _,_legion in pairs(Transport.legions) do + local legion=_legion --Ops.Legion#LEGION + + -- TODO: Should check that this legions actually belongs to this commander. + + -- Legion will cancel the mission. + legion:TransportCancel(Transport) + end + end + + end + +end + +--- On after "OpsOnMission". +-- @param #COMMANDER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP OpsGroup Ops group on mission +-- @param Ops.Auftrag#AUFTRAG Mission The requested mission. +function COMMANDER:onafterOpsOnMission(From, Event, To, OpsGroup, Mission) + -- Debug info. + self:T2(self.lid..string.format("Group \"%s\" on mission \"%s\" [%s]", OpsGroup:GetName(), Mission:GetName(), Mission:GetType())) +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() + + -- Number of missions. + local Nmissions=#self.missionqueue + + -- Treat special cases. + if Nmissions==0 then + return nil + end + + local NoLimit=self:_CheckMissionLimit("Total") + if NoLimit==false then + return nil + end + + -- Sort results table wrt prio and start time. + local function _sort(a, b) + local taskA=a --Ops.Auftrag#AUFTRAG + local taskB=b --Ops.Auftrag#AUFTRAG + return (taskA.prio 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. +-- @return #boolean If `true` enough assets could be recruited. +-- @return #table Recruited assets. +-- @return #table Legions that have recruited assets. +function COMMANDER:RecruitAssetsForMission(Mission) + + -- Debug info. + self:T2(self.lid..string.format("Recruiting assets for mission \"%s\" [%s]", Mission:GetName(), Mission:GetType())) + + -- Cohorts. + local Cohorts=self:_GetCohorts(Mission.specialLegions, Mission.specialCohorts, Mission.operation) + + -- Debug info. + self:T(self.lid..string.format("Found %d cohort candidates for mission", #Cohorts)) + + -- Number of required assets. + local NreqMin, NreqMax=Mission:GetRequiredAssets() + + -- Target position. + local TargetVec2=Mission:GetTargetVec2() + + -- Special payloads. + local Payloads=Mission.payloads + + -- Recruite assets. + local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, Mission.type, Mission.alert5MissionType, NreqMin, NreqMax, TargetVec2, Payloads, + Mission.engageRange, Mission.refuelSystem, nil, nil, nil, Mission.attributes, Mission.properties, {Mission.engageWeaponType}) + + return recruited, assets, legions +end + +--- Recruit assets performing an escort mission for a given asset. +-- @param #COMMANDER self +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @param #table Assets Table of assets to be escorted. +-- @return #boolean If `true`, enough assets could be recruited or no escort was required in the first place. +function COMMANDER:RecruitAssetsForEscort(Mission, Assets) + + -- Is an escort requested in the first place? + if Mission.NescortMin and Mission.NescortMax and (Mission.NescortMin>0 or Mission.NescortMax>0) then + + -- Cohorts. + 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) + + return assigned + end + + 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 +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check transport queue and assign ONE planned transport. +-- @param #COMMANDER self +function COMMANDER:CheckTransportQueue() + + -- Number of missions. + local Ntransports=#self.transportqueue + + -- Treat special cases. + if Ntransports==0 then + return nil + end + + -- Sort results table wrt prio and start time. + local function _sort(a, b) + local taskA=a --Ops.Auftrag#AUFTRAG + local taskB=b --Ops.Auftrag#AUFTRAG + return (taskA.prio0 then + for _,_opsgroup in pairs(cargoOpsGroups) do + local opsgroup=_opsgroup --Ops.OpsGroup#OPSGROUP + local weight=opsgroup:GetWeightTotal() + if weight>weightGroup then + weightGroup=weight + end + TotalWeight=TotalWeight+weight + end + end + + if weightGroup>0 then + + -- Recruite assets from legions. + local recruited, assets, legions=self:RecruitAssetsForTransport(transport, weightGroup, TotalWeight) + + if recruited then + + -- Add asset to transport. + for _,_asset in pairs(assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + transport:AddAsset(asset) + end + + -- Assign transport to legion(s). + self:TransportAssign(transport, legions) + + -- Only ONE transport is assigned. + return + else + -- Not recruited. + LEGION.UnRecruitAssets(assets) + end + + end + + else + + --- + -- Missions NOT in PLANNED state + --- + + end + + end + +end + +--- Recruit assets for a given OPS transport. +-- @param #COMMANDER self +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The OPS transport. +-- @param #number CargoWeight Weight of the heaviest cargo group. +-- @param #number TotalWeight Total weight of all cargo groups. +-- @return #boolean If `true`, enough assets could be recruited. +-- @return #table Recruited assets. +-- @return #table Legions that have recruited assets. +function COMMANDER:RecruitAssetsForTransport(Transport, CargoWeight, TotalWeight) + + if CargoWeight==0 then + -- No cargo groups! + return false, {}, {} + end + + -- Cohorts. + local Cohorts=self:_GetCohorts() + + -- Target is the deploy zone. + local TargetVec2=Transport:GetDeployZone():GetVec2() + + -- Number of required carriers. + local NreqMin,NreqMax=Transport:GetRequiredCarriers() + + -- Recruit assets and legions. + local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, AUFTRAG.Type.OPSTRANSPORT, nil, NreqMin, NreqMax, TargetVec2, nil, nil, nil, CargoWeight, TotalWeight) + + return recruited, assets, legions +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Resources +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check if limit of missions has been reached. +-- @param #COMMANDER self +-- @param #string MissionType Type of mission. +-- @return #boolean If `true`, mission limit has **not** been reached. If `false`, limit has been reached. +function COMMANDER:_CheckMissionLimit(MissionType) + + local limit=self.limitMission[MissionType] + + if limit then + + if MissionType=="Total" then + MissionType=AUFTRAG.Type + end + + local N=self:CountMissions(MissionType, true) + + if N>=limit then + return false + end + end + + return true +end + + +--- Count assets of all assigned legions. +-- @param #COMMANDER self +-- @param #boolean InStock If true, only assets that are in the warehouse stock/inventory are counted. +-- @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 #number Amount of asset groups. +function COMMANDER:CountAssets(InStock, MissionTypes, Attributes) + + local N=0 + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + N=N+legion:CountAssets(InStock, MissionTypes, Attributes) + end + + return N +end + +--- Count assets of all assigned legions. +-- @param #COMMANDER self +-- @param #table MissionTypes (Optional) Count only missions of these types. Default is all types. +-- @param #boolean OnlyRunning If `true`, only count running missions. +-- @return #number Amount missions. +function COMMANDER:CountMissions(MissionTypes, OnlyRunning) + + local N=0 + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if (not OnlyRunning) or (mission.statusCommander~=AUFTRAG.Status.PLANNED) then + + -- Check if this mission type is requested. + if AUFTRAG.CheckMissionType(mission.type, MissionTypes) then + N=N+1 + end + + end + end + + + return N +end + +--- Count assets of all assigned legions. +-- @param #COMMANDER self +-- @param #boolean InStock If true, only assets that are in the warehouse stock/inventory are counted. +-- @param #table Legions (Optional) Table of legions. Default is all legions. +-- @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 #number Amount of asset groups. +function COMMANDER:GetAssets(InStock, Legions, MissionTypes, Attributes) + + -- Selected assets. + local assets={} + + for _,_legion in pairs(Legions or self.legions) do + local legion=_legion --Ops.Legion#LEGION + + --TODO Check if legion is running and maybe if runway is operational if air assets are requested. + + for _,_cohort in pairs(legion.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + for _,_asset in pairs(cohort.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + -- TODO: Check if repaired. + -- TODO: currently we take only unspawned assets. + if not (asset.spawned or asset.isReserved or asset.requested) then + table.insert(assets, asset) + end + + end + end + end + + return assets +end + +--- Check all legions if they are able to do a specific mission type at a certain location with a given number of assets. +-- @param #COMMANDER self +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @return #table Table of LEGIONs that can do the mission and have at least one asset available right now. +function COMMANDER:GetLegionsForMission(Mission) + + -- Table of legions that can do the mission. + local legions={} + + -- Loop over all legions. + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + + -- Count number of assets in stock. + local Nassets=0 + if legion:IsAirwing() then + Nassets=legion:CountAssetsWithPayloadsInStock(Mission.payloads, {Mission.type}, Attributes) + else + Nassets=legion:CountAssets(true, {Mission.type}, Attributes) --Could also specify the attribute if Air or Ground mission. + end + + -- Has it assets that can? + if Nassets>0 and false then + + -- Get coordinate of the target. + local coord=Mission:GetTargetCoordinate() + + if coord then + + -- Distance from legion to target. + local distance=UTILS.MetersToNM(coord:Get2DDistance(legion:GetCoordinate())) + + -- Round: 55 NM ==> 5.5 ==> 6, 63 NM ==> 6.3 ==> 6 + local dist=UTILS.Round(distance/10, 0) + + -- Debug info. + self:T(self.lid..string.format("Got legion %s with Nassets=%d and dist=%.1f NM, rounded=%.1f", legion.alias, Nassets, distance, dist)) + + -- Add legion to table of legions that can. + table.insert(legions, {airwing=legion, distance=distance, dist=dist, targetcoord=coord, nassets=Nassets}) + + end + + end + + -- Add legion if it can provide at least 1 asset. + if Nassets>0 then + table.insert(legions, legion) + end + + end + + return legions +end + +--- Get assets on given mission or missions. +-- @param #COMMANDER self +-- @param #table MissionTypes Types on mission to be checked. Default all. +-- @return #table Assets on pending requests. +function COMMANDER:GetAssetsOnMission(MissionTypes) + + local assets={} + + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + -- Check if this mission type is requested. + if AUFTRAG.CheckMissionType(mission.type, MissionTypes) then + + for _,_asset in pairs(mission.assets or {}) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + table.insert(assets, asset) + end + end + end + + return assets +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- **Ops** - Troop transport assignment for OPS groups. +-- +-- ## Main Features: +-- +-- * Transport troops from A to B +-- * Supports ground, naval and airborne (airplanes and helicopters) units as carriers +-- * Use combined forces (ground, naval, air) to transport the troops +-- * Additional FSM events to hook into and customize your mission design +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20Transport). +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module Ops.OpsTransport +-- @image OPS_OpsTransport.png + + +--- OPSTRANSPORT class. +-- @type OPSTRANSPORT +-- @field #string ClassName Name of the class. +-- @field #string lid Log ID. +-- @field #number uid Unique ID of the transport. +-- @field #number verbose Verbosity level. +-- +-- @field #number prio Priority of this transport. Should be a number between 0 (high prio) and 100 (low prio). +-- @field #boolean urgent If true, transport is urgent. +-- @field #number importance Importance of this transport. Smaller=higher. +-- @field #number Tstart Start time in *abs.* seconds. +-- @field #number Tstop Stop time in *abs.* seconds. Default `#nil` (never stops). +-- @field #number duration Duration (`Tstop-Tstart`) of the transport in seconds. +-- @field #table conditionStart Start conditions. +-- +-- @field #table carriers Carriers assigned for this transport. +-- @field #table carrierTransportStatus Status of each carrier. +-- +-- @field #table tzCombos Table of transport zone combos. Each element of the table is of type `#OPSTRANSPORT.TransportZoneCombo`. +-- @field #number tzcCounter Running number of added transport zone combos. +-- @field #OPSTRANSPORT.TransportZoneCombo tzcDefault Default transport zone combo. +-- +-- @field #number Ncargo Total number of cargo groups. +-- @field #number Ncarrier Total number of assigned carriers. +-- @field #number Ndelivered Total number of cargo groups delivered. +-- @field #number NcarrierDead Total number of dead carrier groups +-- @field #number NcargoDead Totalnumber of dead cargo groups. +-- +-- @field Ops.Auftrag#AUFTRAG mission The mission attached to this transport. +-- @field #table assets Warehouse assets assigned for this transport. +-- @field #table legions Assigned legions. +-- @field #table statusLegion Transport status of all assigned LEGIONs. +-- @field #string statusCommander Staus of the COMMANDER. +-- @field Ops.Commander#COMMANDER commander Commander of the transport. +-- @field Ops.Chief#CHIEF chief Chief of the transport. +-- @field Ops.OpsZone#OPSZONE opszone OPS zone. +-- @field #table requestID The ID of the queued warehouse request. Necessary to cancel the request if the transport was cancelled before the request is processed. +-- +-- @extends Core.Fsm#FSM + +--- *Victory is the beautiful, bright-colored flower; Transport is the stem without which it could never have blossomed* -- Winston Churchill +-- +-- === +-- +-- # The OPSTRANSPORT Concept +-- +-- This class simulates troop transport using carriers such as APCs, ships, helicopters or airplanes. The carriers and transported groups need to be OPSGROUPS (see ARMYGROUP, NAVYGROUP and FLIGHTGROUP classes). +-- +-- **IMPORTANT NOTES** +-- +-- * Cargo groups are **not** split and distributed into different carrier *units*. That means that the whole cargo group **must fit** into one of the carrier units. +-- * Cargo groups must be inside the pickup zones to be considered for loading. Groups not inside the pickup zone will not get the command to board. +-- +-- # Constructor +-- +-- A new cargo transport assignment is created with the @{#OPSTRANSPORT.New}() function +-- +-- local opstransport=OPSTRANSPORT:New(Cargo, PickupZone, DeployZone) +-- +-- Here `Cargo` is an object of the troops to be transported. This can be a GROUP, OPSGROUP, SET_GROUP or SET_OPSGROUP object. +-- +-- `PickupZone` is the zone where the troops are picked up by the transport carriers. **Note** that troops *must* be inside this zone to be considered for loading! +-- +-- `DeployZone` is the zone where the troops are transported to. +-- +-- ## Assign to Carrier(s) +-- +-- A transport can be assigned to one or multiple carrier OPSGROUPS with this @{Ops.OpsGroup#OPSGROUP.AddOpsTransport}() function +-- +-- myopsgroup:AddOpsTransport(opstransport) +-- +-- There is no restriction to the type of the carrier. It can be a ground group (e.g. an APC), a helicopter, an airplane or even a ship. +-- +-- You can also mix carrier types. For instance, you can assign the same transport to APCs and helicopters. Or to helicopters and airplanes. +-- +-- # Examples +-- +-- A carrier group is assigned to transport infantry troops from zone "Zone Kobuleti X" to zone "Zone Alpha". +-- +-- -- Carrier group. +-- local carrier=ARMYGROUP:New("TPz Fuchs Group") +-- +-- -- Set of groups to transport. +-- local infantryset=SET_GROUP:New():FilterPrefixes("Infantry Platoon Alpha"):FilterOnce() +-- +-- -- Cargo transport assignment. +-- local opstransport=OPSTRANSPORT:New(infantryset, ZONE:New("Zone Kobuleti X"), ZONE:New("Zone Alpha")) +-- +-- -- Assign transport to carrier. +-- carrier:AddOpsTransport(opstransport) +-- +-- +-- @field #OPSTRANSPORT +OPSTRANSPORT = { + ClassName = "OPSTRANSPORT", + verbose = 0, + carriers = {}, + carrierTransportStatus = {}, + tzCombos = {}, + tzcCounter = 0, + conditionStart = {}, + assets = {}, + legions = {}, + statusLegion = {}, + requestID = {}, +} + +--- Cargo transport status. +-- @type OPSTRANSPORT.Status +-- @field #string PLANNED Planning state. +-- @field #string QUEUED Queued state. +-- @field #string REQUESTED Requested state. +-- @field #string SCHEDULED Transport is scheduled in the cargo queue. +-- @field #string EXECUTING Transport is being executed. +-- @field #string DELIVERED Transport was delivered. +-- @field #string CANCELLED Transport was cancelled. +-- @field #string SUCCESS Transport was a success. +-- @field #string FAILED Transport failed. +OPSTRANSPORT.Status={ + PLANNED="planned", + QUEUED="queued", + REQUESTED="requested", + SCHEDULED="scheduled", + EXECUTING="executing", + DELIVERED="delivered", + CANCELLED="cancelled", + SUCCESS="success", + FAILED="failed", +} + +--- Transport zone combination. +-- @type OPSTRANSPORT.TransportZoneCombo +-- @field #number uid Unique ID of the TZ combo. +-- @field #number Ncarriers Number of carrier groups using this transport zone. +-- @field #number Ncargo Number of cargos assigned. This is a running number and *not* decreased if cargo is delivered or dead. +-- @field #table Cargos Cargo groups of the TZ combo. Each element is of type `Ops.OpsGroup#OPSGROUP.CargoGroup`. +-- @field Core.Zone#ZONE PickupZone Pickup zone. +-- @field Core.Zone#ZONE DeployZone Deploy zone. +-- @field Core.Zone#ZONE EmbarkZone Embark zone if different from pickup zone. +-- @field Core.Zone#ZONE DisembarkZone Zone where the troops are disembared to. +-- @field Wrapper.Airbase#AIRBASE PickupAirbase Airbase for pickup. +-- @field Wrapper.Airbase#AIRBASE DeployAirbase Airbase for deploy. +-- @field #table PickupPaths Paths for pickup. +-- @field #table TransportPaths Path for Transport. Each elment of the table is of type `#OPSTRANSPORT.Path`. +-- @field #table RequiredCargos Required cargos. +-- @field #table DisembarkCarriers Carriers where the cargo is directly disembarked to. +-- @field #boolean disembarkActivation If true, troops are spawned in late activated state when disembarked from carrier. +-- @field #boolean disembarkInUtero If true, troops are disembarked "in utero". +-- @field #boolean assets Cargo assets. +-- @field #number PickupFormation Formation used to pickup. +-- @field #number TransportFormation Formation used to transport. + +--- Path used for pickup or transport. +-- @type OPSTRANSPORT.Path +-- @field #table waypoints Table of waypoints. +-- @field #number category Category for which carriers this path is used. +-- @field #number radius Radomization radius for waypoints in meters. Default 0 m. +-- @field #boolean reverse If `true`, path is used in reversed order. + +--- Generic transport condition. +-- @type OPSTRANSPORT.Condition +-- @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. + +--- Transport ID. +_OPSTRANSPORTID=0 + +--- Army Group version. +-- @field #string version +OPSTRANSPORT.version="0.6.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Trains. +-- TODO: Stop transport. +-- TODO: Improve pickup and transport paths. +-- DONE: Special transport cohorts/legions. Similar to mission. +-- DONE: Cancel transport. +-- DONE: Allow multiple pickup/depoly zones. +-- DONE: Add start conditions. +-- DONE: Check carrier(s) dead. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new OPSTRANSPORT class object. Essential input are the troops that should be transported and the zones where the troops are picked up and deployed. +-- @param #OPSTRANSPORT self +-- @param Core.Set#SET_GROUP CargoGroups Groups to be transported as cargo. Can also be a single @{Wrapper.Group#GROUP} or @{Ops.OpsGroup#OPSGROUP} object. +-- @param Core.Zone#ZONE PickupZone Pickup zone. This is the zone, where the carrier is going to pickup the cargo. **Important**: only cargo is considered, if it is in this zone when the carrier starts loading! +-- @param Core.Zone#ZONE DeployZone Deploy zone. This is the zone, where the carrier is going to drop off the cargo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:New(CargoGroups, PickupZone, DeployZone) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #OPSTRANSPORT + + -- Increase ID counter. + _OPSTRANSPORTID=_OPSTRANSPORTID+1 + + -- Set some string id for output to DCS.log file. + self.lid=string.format("OPSTRANSPORT [UID=%d] | ", _OPSTRANSPORTID) + + -- UID of this transport. + self.uid=_OPSTRANSPORTID + + -- Defaults. + self:SetPriority() + self:SetTime() + self:SetRequiredCarriers() + + -- Init arrays and counters. + self.carriers={} + self.Ncargo=0 + self.Ncarrier=0 + self.Ndelivered=0 + self.NcargoDead=0 + self.NcarrierDead=0 + + -- Set default TZC. + self.tzcDefault=self:AddTransportZoneCombo(CargoGroups, PickupZone, DeployZone) + + -- FMS start state is PLANNED. + self:SetStartState(OPSTRANSPORT.Status.PLANNED) + + -- PLANNED --> SCHEDULED --> EXECUTING --> DELIVERED + self:AddTransition("*", "Planned", OPSTRANSPORT.Status.PLANNED) -- Cargo transport was planned. + self:AddTransition(OPSTRANSPORT.Status.PLANNED, "Queued", OPSTRANSPORT.Status.QUEUED) -- Cargo is queued at at least one carrier. + self:AddTransition(OPSTRANSPORT.Status.QUEUED, "Requested", OPSTRANSPORT.Status.REQUESTED) -- Transport assets have been requested from a warehouse. + self:AddTransition(OPSTRANSPORT.Status.REQUESTED, "Scheduled", OPSTRANSPORT.Status.SCHEDULED) -- Cargo is queued at at least one carrier. + self:AddTransition(OPSTRANSPORT.Status.PLANNED, "Scheduled", OPSTRANSPORT.Status.SCHEDULED) -- Cargo is queued at at least one carrier. + self:AddTransition(OPSTRANSPORT.Status.SCHEDULED, "Executing", OPSTRANSPORT.Status.EXECUTING) -- Cargo is being transported. + self:AddTransition("*", "Delivered", OPSTRANSPORT.Status.DELIVERED) -- Cargo was delivered. + + self:AddTransition("*", "StatusUpdate", "*") + self:AddTransition("*", "Stop", "*") + + self:AddTransition("*", "Cancel", OPSTRANSPORT.Status.CANCELLED) -- Command to cancel the transport. + + self:AddTransition("*", "Loaded", "*") + self:AddTransition("*", "Unloaded", "*") + + self:AddTransition("*", "DeadCarrierUnit", "*") + self:AddTransition("*", "DeadCarrierGroup", "*") + self:AddTransition("*", "DeadCarrierAll", "*") + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "StatusUpdate". + -- @function [parent=#OPSTRANSPORT] StatusUpdate + -- @param #OPSTRANSPORT self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#OPSTRANSPORT] __StatusUpdate + -- @param #OPSTRANSPORT self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Planned". + -- @function [parent=#OPSTRANSPORT] Planned + -- @param #OPSTRANSPORT self + + --- Triggers the FSM event "Planned" after a delay. + -- @function [parent=#OPSTRANSPORT] __Planned + -- @param #OPSTRANSPORT self + -- @param #number delay Delay in seconds. + + --- On after "Planned" event. + -- @function [parent=#OPSTRANSPORT] OnAfterPlanned + -- @param #OPSTRANSPORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Queued". + -- @function [parent=#OPSTRANSPORT] Queued + -- @param #OPSTRANSPORT self + + --- Triggers the FSM event "Queued" after a delay. + -- @function [parent=#OPSTRANSPORT] __Queued + -- @param #OPSTRANSPORT self + -- @param #number delay Delay in seconds. + + --- On after "Queued" event. + -- @function [parent=#OPSTRANSPORT] OnAfterQueued + -- @param #OPSTRANSPORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Requested". + -- @function [parent=#OPSTRANSPORT] Requested + -- @param #OPSTRANSPORT self + + --- Triggers the FSM event "Requested" after a delay. + -- @function [parent=#OPSTRANSPORT] __Requested + -- @param #OPSTRANSPORT self + -- @param #number delay Delay in seconds. + + --- On after "Requested" event. + -- @function [parent=#OPSTRANSPORT] OnAfterRequested + -- @param #OPSTRANSPORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Scheduled". + -- @function [parent=#OPSTRANSPORT] Scheduled + -- @param #OPSTRANSPORT self + + --- Triggers the FSM event "Scheduled" after a delay. + -- @function [parent=#OPSTRANSPORT] __Scheduled + -- @param #OPSTRANSPORT self + -- @param #number delay Delay in seconds. + + --- On after "Scheduled" event. + -- @function [parent=#OPSTRANSPORT] OnAfterScheduled + -- @param #OPSTRANSPORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Executing". + -- @function [parent=#OPSTRANSPORT] Executing + -- @param #OPSTRANSPORT self + + --- Triggers the FSM event "Executing" after a delay. + -- @function [parent=#OPSTRANSPORT] __Executing + -- @param #OPSTRANSPORT self + -- @param #number delay Delay in seconds. + + --- On after "Executing" event. + -- @function [parent=#OPSTRANSPORT] OnAfterExecuting + -- @param #OPSTRANSPORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Delivered". + -- @function [parent=#OPSTRANSPORT] Delivered + -- @param #OPSTRANSPORT self + + --- Triggers the FSM event "Delivered" after a delay. + -- @function [parent=#OPSTRANSPORT] __Delivered + -- @param #OPSTRANSPORT self + -- @param #number delay Delay in seconds. + + --- On after "Delivered" event. + -- @function [parent=#OPSTRANSPORT] OnAfterDelivered + -- @param #OPSTRANSPORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Cancel". + -- @function [parent=#OPSTRANSPORT] Cancel + -- @param #OPSTRANSPORT self + + --- Triggers the FSM event "Cancel" after a delay. + -- @function [parent=#OPSTRANSPORT] __Cancel + -- @param #OPSTRANSPORT self + -- @param #number delay Delay in seconds. + + --- On after "Cancel" event. + -- @function [parent=#OPSTRANSPORT] OnAfterCancel + -- @param #OPSTRANSPORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Loaded". + -- @function [parent=#OPSTRANSPORT] Loaded + -- @param #OPSTRANSPORT self + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo OPSGROUP that was loaded into a carrier. + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier OPSGROUP that was loaded into a carrier. + -- @param Ops.OpsGroup#OPSGROUP.Element CarrierElement Carrier element. + + --- Triggers the FSM event "Loaded" after a delay. + -- @function [parent=#OPSTRANSPORT] __Loaded + -- @param #OPSTRANSPORT self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo OPSGROUP that was loaded into a carrier. + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier OPSGROUP that was loaded into a carrier. + -- @param Ops.OpsGroup#OPSGROUP.Element CarrierElement Carrier element. + + --- On after "Loaded" event. + -- @function [parent=#OPSTRANSPORT] OnAfterLoaded + -- @param #OPSTRANSPORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo OPSGROUP that was loaded into a carrier. + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier OPSGROUP that was loaded into a carrier. + -- @param Ops.OpsGroup#OPSGROUP.Element CarrierElement Carrier element. + + + --- Triggers the FSM event "Unloaded". + -- @function [parent=#OPSTRANSPORT] Unloaded + -- @param #OPSTRANSPORT self + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo Cargo OPSGROUP that was unloaded from a carrier. + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier Carrier OPSGROUP that unloaded the cargo. + + --- Triggers the FSM event "Unloaded" after a delay. + -- @function [parent=#OPSTRANSPORT] __Unloaded + -- @param #OPSTRANSPORT self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo Cargo OPSGROUP that was unloaded from a carrier. + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier Carrier OPSGROUP that unloaded the cargo. + + --- On after "Unloaded" event. + -- @function [parent=#OPSTRANSPORT] OnAfterUnloaded + -- @param #OPSTRANSPORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo Cargo OPSGROUP that was unloaded from a carrier. + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier Carrier OPSGROUP that unloaded the cargo. + + + --TODO: Psydofunctions + + -- Call status update. + self:__StatusUpdate(-1) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add pickup and deploy zone combination. +-- @param #OPSTRANSPORT self +-- @param Core.Set#SET_GROUP CargoGroups Groups to be transported as cargo. Can also be a single @{Wrapper.Group#GROUP} or @{Ops.OpsGroup#OPSGROUP} object. +-- @param Core.Zone#ZONE PickupZone Zone where the troops are picked up. +-- @param Core.Zone#ZONE DeployZone Zone where the troops are picked up. +-- @return #OPSTRANSPORT.TransportZoneCombo Transport zone table. +function OPSTRANSPORT:AddTransportZoneCombo(CargoGroups, PickupZone, DeployZone) + + -- Increase counter. + self.tzcCounter=self.tzcCounter+1 + + local tzcombo={} --#OPSTRANSPORT.TransportZoneCombo + + -- Init. + tzcombo.uid=self.tzcCounter + tzcombo.Ncarriers=0 + tzcombo.Ncargo=0 + tzcombo.Cargos={} + tzcombo.RequiredCargos={} + tzcombo.DisembarkCarriers={} + tzcombo.PickupPaths={} + tzcombo.TransportPaths={} + + -- Set zones. + self:SetPickupZone(PickupZone, tzcombo) + self:SetDeployZone(DeployZone, tzcombo) + self:SetEmbarkZone(nil, tzcombo) + + -- Add cargo groups (could also be added later). + if CargoGroups then + self:AddCargoGroups(CargoGroups, tzcombo) + end + + -- Add to table. + table.insert(self.tzCombos, tzcombo) + + return tzcombo +end + +--- Add cargo groups to be transported. +-- @param #OPSTRANSPORT self +-- @param Core.Set#SET_GROUP GroupSet Set of groups to be transported. Can also be passed as a single GROUP or OPSGROUP object. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @param #boolean DisembarkActivation If `true`, cargo group is activated when disembarked. If `false`, cargo groups are late activated when disembarked. Default `nil` (usually activated). +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:AddCargoGroups(GroupSet, TransportZoneCombo, DisembarkActivation) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + -- Check type of GroupSet provided. + if GroupSet:IsInstanceOf("GROUP") or GroupSet:IsInstanceOf("OPSGROUP") then + + -- We got a single GROUP or OPSGROUP object. + local cargo=self:_CreateCargoGroupData(GroupSet, TransportZoneCombo, DisembarkActivation) + + if cargo then + + -- Add to main table. + --table.insert(self.cargos, cargo) + self.Ncargo=self.Ncargo+1 + + -- Add to TZC table. + table.insert(TransportZoneCombo.Cargos, cargo) + TransportZoneCombo.Ncargo=TransportZoneCombo.Ncargo+1 + + cargo.opsgroup:_AddMyLift(self) + + end + + else + + -- We got a SET_GROUP object. + + for _,group in pairs(GroupSet.Set) do + + -- Call iteravely for each group. + self:AddCargoGroups(group, TransportZoneCombo, DisembarkActivation) + + end + end + + -- Debug info. + if self.verbose>=1 then + local text=string.format("Added cargo groups:") + local Weight=0 + for _,_cargo in pairs(self:GetCargos()) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + local weight=cargo.opsgroup:GetWeightTotal() + Weight=Weight+weight + text=text..string.format("\n- %s [%s] weight=%.1f kg", cargo.opsgroup:GetName(), cargo.opsgroup:GetState(), weight) + end + text=text..string.format("\nTOTAL: Ncargo=%d, Weight=%.1f kg", self.Ncargo, Weight) + self:I(self.lid..text) + end + + + return self +end + + +--- Set pickup zone. +-- @param #OPSTRANSPORT self +-- @param Core.Zone#ZONE PickupZone Zone where the troops are picked up. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetPickupZone(PickupZone, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + TransportZoneCombo.PickupZone=PickupZone + + if PickupZone and PickupZone:IsInstanceOf("ZONE_AIRBASE") then + TransportZoneCombo.PickupAirbase=PickupZone._.ZoneAirbase + end + + return self +end + +--- Get pickup zone. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return Core.Zone#ZONE Zone where the troops are picked up. +function OPSTRANSPORT:GetPickupZone(TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + return TransportZoneCombo.PickupZone +end + +--- Set deploy zone. +-- @param #OPSTRANSPORT self +-- @param Core.Zone#ZONE DeployZone Zone where the troops are deployed. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetDeployZone(DeployZone, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + -- Set deploy zone. + TransportZoneCombo.DeployZone=DeployZone + + -- Check if this is an airbase. + if DeployZone and DeployZone:IsInstanceOf("ZONE_AIRBASE") then + TransportZoneCombo.DeployAirbase=DeployZone._.ZoneAirbase + end + + return self +end + +--- Get deploy zone. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return Core.Zone#ZONE Zone where the troops are deployed. +function OPSTRANSPORT:GetDeployZone(TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + return TransportZoneCombo.DeployZone +end + +--- Set embark zone. +-- @param #OPSTRANSPORT self +-- @param Core.Zone#ZONE EmbarkZone Zone where the troops are embarked. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetEmbarkZone(EmbarkZone, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + TransportZoneCombo.EmbarkZone=EmbarkZone or TransportZoneCombo.PickupZone + + return self +end + +--- Get embark zone. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return Core.Zone#ZONE Zone where the troops are embarked from. +function OPSTRANSPORT:GetEmbarkZone(TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + return TransportZoneCombo.EmbarkZone +end + +--- Set disembark zone. +-- @param #OPSTRANSPORT self +-- @param Core.Zone#ZONE DisembarkZone Zone where the troops are disembarked. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetDisembarkZone(DisembarkZone, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + TransportZoneCombo.DisembarkZone=DisembarkZone + + return self +end + +--- Get disembark zone. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return Core.Zone#ZONE Zone where the troops are disembarked to. +function OPSTRANSPORT:GetDisembarkZone(TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + return TransportZoneCombo.DisembarkZone +end + +--- Set activation status of group when disembarked from transport carrier. +-- @param #OPSTRANSPORT self +-- @param #boolean Active If `true` or `nil`, group is activated when disembarked. If `false`, group is late activated and needs to be activated manually. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetDisembarkActivation(Active, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + if Active==true or Active==nil then + TransportZoneCombo.disembarkActivation=true + else + TransportZoneCombo.disembarkActivation=false + end + + return self +end + +--- Get disembark activation. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #boolean If `true`, groups are spawned in late activated state. +function OPSTRANSPORT:GetDisembarkActivation(TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + return TransportZoneCombo.disembarkActivation +end + +--- Set transfer carrier(s). These are carrier groups, where the cargo is directly loaded into when disembarked. +-- @param #OPSTRANSPORT self +-- @param Core.Set#SET_GROUP Carriers Carrier set. Can also be passed as a #GROUP, #OPSGROUP or #SET_OPSGROUP object. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetDisembarkCarriers(Carriers, TransportZoneCombo) + + -- Debug info. + self:T(self.lid.."Setting transfer carriers!") + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + if Carriers:IsInstanceOf("GROUP") or Carriers:IsInstanceOf("OPSGROUP") then + + local carrier=self:_GetOpsGroupFromObject(Carriers) + if carrier then + table.insert(TransportZoneCombo.DisembarkCarriers, carrier) + end + + elseif Carriers:IsInstanceOf("SET_GROUP") or Carriers:IsInstanceOf("SET_OPSGROUP") then + + for _,object in pairs(Carriers:GetSet()) do + local carrier=self:_GetOpsGroupFromObject(object) + if carrier then + table.insert(TransportZoneCombo.DisembarkCarriers, carrier) + end + end + + else + self:E(self.lid.."ERROR: Carriers must be a GROUP, OPSGROUP, SET_GROUP or SET_OPSGROUP object!") + end + + return self +end + +--- Get transfer carrier(s). These are carrier groups, where the cargo is directly loaded into when disembarked. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #table Table of carrier OPS groups. +function OPSTRANSPORT:GetDisembarkCarriers(TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + return TransportZoneCombo.DisembarkCarriers +end + + +--- Set if group remains *in utero* after disembarkment from carrier. Can be used to directly load the group into another carrier. Similar to disembark in late activated state. +-- @param #OPSTRANSPORT self +-- @param #boolean InUtero If `true` or `nil`, group remains *in utero* after disembarkment. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetDisembarkInUtero(InUtero, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + if InUtero==true or InUtero==nil then + TransportZoneCombo.disembarkInUtero=true + else + TransportZoneCombo.disembarkInUtero=false + end + + return self +end + +--- Get disembark in utero. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #boolean If `true`, groups stay in utero after disembarkment. +function OPSTRANSPORT:GetDisembarkInUtero(TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + return TransportZoneCombo.disembarkInUtero +end + +--- Set pickup formation. +-- @param #OPSTRANSPORT self +-- @param #number Formation Pickup formation. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetFormationPickup(Formation, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + TransportZoneCombo.PickupFormation=Formation + + return self +end + +--- Get pickup formation. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #number Formation. +function OPSTRANSPORT:_GetFormationPickup(TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + return TransportZoneCombo.PickupFormation +end + +--- Set transport formation. +-- @param #OPSTRANSPORT self +-- @param #number Formation Pickup formation. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetFormationTransport(Formation, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + TransportZoneCombo.TransportFormation=Formation + + return self +end + +--- Get transport formation. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #number Formation. +function OPSTRANSPORT:_GetFormationTransport(TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + return TransportZoneCombo.TransportFormation +end + + + +--- Set required cargo. This is a list of cargo groups that need to be loaded before the **first** transport will start. +-- @param #OPSTRANSPORT self +-- @param Core.Set#SET_GROUP Cargos Required cargo set. Can also be passed as a #GROUP, #OPSGROUP or #SET_OPSGROUP object. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetRequiredCargos(Cargos, TransportZoneCombo) + + -- Debug info. + self:T(self.lid.."Setting required cargos!") + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + -- Create table. + TransportZoneCombo.RequiredCargos=TransportZoneCombo.RequiredCargos or {} + + if Cargos:IsInstanceOf("GROUP") or Cargos:IsInstanceOf("OPSGROUP") then + + local cargo=self:_GetOpsGroupFromObject(Cargos) + if cargo then + table.insert(TransportZoneCombo.RequiredCargos, cargo) + end + + elseif Cargos:IsInstanceOf("SET_GROUP") or Cargos:IsInstanceOf("SET_OPSGROUP") then + + for _,object in pairs(Cargos:GetSet()) do + local cargo=self:_GetOpsGroupFromObject(object) + if cargo then + table.insert(TransportZoneCombo.RequiredCargos, cargo) + end + end + + else + self:E(self.lid.."ERROR: Required Cargos must be a GROUP, OPSGROUP, SET_GROUP or SET_OPSGROUP object!") + end + + return self +end + +--- Get required cargos. This is a list of cargo groups that need to be loaded before the **first** transport will start. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #table Table of required cargo ops groups. +function OPSTRANSPORT:GetRequiredCargos(TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + return TransportZoneCombo.RequiredCargos +end + +--- Set number of required carrier groups for an OPSTRANSPORT assignment. Only used if transport is assigned at **LEGION** or higher level. +-- @param #OPSTRANSPORT self +-- @param #number NcarriersMin Number of carriers *at least* required. Default 1. +-- @param #number NcarriersMax Number of carriers *at most* used for transportation. Default is same as `NcarriersMin`. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetRequiredCarriers(NcarriersMin, NcarriersMax) + + self.nCarriersMin=NcarriersMin or 1 + + self.nCarriersMax=NcarriersMax or self.nCarriersMin + + -- Ensure that max is at least equal to min. + if self.nCarriersMax0 then + self:ScheduleOnce(Delay, OPSTRANSPORT._DelCarrier, self, CarrierGroup) + else + if self:IsCarrier(CarrierGroup) then + + for i=#self.carriers,1,-1 do + local carrier=self.carriers[i] --Ops.OpsGroup#OPSGROUP + if carrier.groupname==CarrierGroup.groupname then + self:T(self.lid..string.format("Removing carrier %s", CarrierGroup.groupname)) + table.remove(self.carriers, i) + end + end + + end + end + + return self +end + +--- Get a list of alive carriers. +-- @param #OPSTRANSPORT self +-- @return #table Names of all carriers +function OPSTRANSPORT:_GetCarrierNames() + + local names={} + for _,_carrier in pairs(self.carriers) do + local carrier=_carrier --Ops.OpsGroup#OPSGROUP + if carrier:IsAlive()~=nil then + table.insert(names, carrier.groupname) + end + end + + return names +end + +--- Get (all) cargo @{Ops.OpsGroup#OPSGROUP}s. Optionally, only delivered or undelivered groups can be returned. +-- @param #OPSTRANSPORT self +-- @param #boolean Delivered If `true`, only delivered groups are returned. If `false` only undelivered groups are returned. If `nil`, all groups are returned. +-- @param Ops.OpsGroup#OPSGROUP Carrier (Optional) Only count cargo groups that fit into the given carrier group. Current cargo is not a factor. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #table Cargo Ops groups. Can be and empty table `{}`. +function OPSTRANSPORT:GetCargoOpsGroups(Delivered, Carrier, TransportZoneCombo) + + local cargos=self:GetCargos(TransportZoneCombo) + + local opsgroups={} + for _,_cargo in pairs(cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + if Delivered==nil or cargo.delivered==Delivered then + if cargo.opsgroup and not (cargo.opsgroup:IsDead() or cargo.opsgroup:IsStopped()) then + if Carrier==nil or Carrier:CanCargo(cargo.opsgroup) then + table.insert(opsgroups, cargo.opsgroup) + end + end + end + end + + return opsgroups +end + +--- Get carriers. +-- @param #OPSTRANSPORT self +-- @return #table Carrier Ops groups. +function OPSTRANSPORT:GetCarriers() + return self.carriers +end + +--- Get cargos. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #table Cargos. +function OPSTRANSPORT:GetCargos(TransportZoneCombo) + + if TransportZoneCombo then + return TransportZoneCombo.Cargos + else + local cargos={} + for _,_tzc in pairs(self.tzCombos) do + local tzc=_tzc --#OPSTRANSPORT.TransportZoneCombo + for _,cargo in pairs(tzc.Cargos) do + table.insert(cargos, cargo) + end + end + return cargos + end + +end + +--- Set transport start and stop time. +-- @param #OPSTRANSPORT self +-- @param #string ClockStart Time the transport 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 transport 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 #OPSTRANSPORT self +function OPSTRANSPORT:SetTime(ClockStart, ClockStop) + + -- Current mission time. + local Tnow=timer.getAbsTime() + + -- Set start time. Default in 5 sec. + local Tstart=Tnow+5 + if ClockStart and type(ClockStart)=="number" then + Tstart=Tnow+ClockStart + elseif ClockStart and type(ClockStart)=="string" then + Tstart=UTILS.ClockToSeconds(ClockStart) + end + + -- Set stop time. Default nil. + local Tstop=nil + if ClockStop and type(ClockStop)=="number" then + Tstop=Tnow+ClockStop + elseif ClockStop and type(ClockStop)=="string" then + Tstop=UTILS.ClockToSeconds(ClockStop) + end + + self.Tstart=Tstart + self.Tstop=Tstop + + if Tstop then + self.duration=self.Tstop-self.Tstart + end + + return self +end + +--- Set mission priority and (optional) urgency. Urgent missions can cancel other running missions. +-- @param #OPSTRANSPORT self +-- @param #number Prio Priority 1=high, 100=low. Default 50. +-- @param #number Importance Number 1-10. If missions with lower value are in the queue, these have to be finished first. Default is `nil`. +-- @param #boolean Urgent If *true*, another running mission might be cancelled if it has a lower priority. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetPriority(Prio, Importance, Urgent) + self.prio=Prio or 50 + self.urgent=Urgent + self.importance=Importance + return self +end + +--- Set verbosity. +-- @param #OPSTRANSPORT self +-- @param #number Verbosity Be more verbose. Default 0 +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetVerbosity(Verbosity) + self.verbose=Verbosity or 0 + return self +end + +--- Add start condition. +-- @param #OPSTRANSPORT self +-- @param #function ConditionFunction Function that needs to be true before the transport can be started. Must return a #boolean. +-- @param ... Condition function arguments if any. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:AddConditionStart(ConditionFunction, ...) + + if ConditionFunction then + + local condition={} --#OPSTRANSPORT.Condition + + condition.func=ConditionFunction + condition.arg={} + if arg then + condition.arg=arg + end + + table.insert(self.conditionStart, condition) + + end + + return self +end + +--- Add path used for transportation from the pickup to the deploy zone. +-- If multiple paths are defined, a random one is chosen. The path is retrieved from the waypoints of a given group. +-- **NOTE** that the category group defines for which carriers this path is valid. +-- For example, if you specify a GROUND group to provide the waypoints, only assigned GROUND carriers will use the +-- path. +-- @param #OPSTRANSPORT self +-- @param Wrapper.Group#GROUP PathGroup A (late activated) GROUP defining a transport path by their waypoints. +-- @param #number Radius Randomization radius in meters. Default 0 m. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport Zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:AddPathTransport(PathGroup, Reversed, Radius, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + if type(PathGroup)=="string" then + PathGroup=GROUP:FindByName(PathGroup) + end + + local path={} --#OPSTRANSPORT.Path + path.category=PathGroup:GetCategory() + path.radius=Radius or 0 + path.waypoints=PathGroup:GetTaskRoute() + + -- TODO: Check that only flyover waypoints are given for aircraft. + + -- Add path. + table.insert(TransportZoneCombo.TransportPaths, path) + + return self +end + +--- Get a path for transportation. +-- @param #OPSTRANSPORT self +-- @param #number Category Group category. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport Zone combo. +-- @return #OPSTRANSPORT.Path The path object. +function OPSTRANSPORT:_GetPathTransport(Category, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + local pathsTransport=TransportZoneCombo.TransportPaths + + if pathsTransport and #pathsTransport>0 then + + local paths={} + + for _,_path in pairs(pathsTransport) do + local path=_path --#OPSTRANSPORT.Path + if path.category==Category then + table.insert(paths, path) + end + end + + if #paths>0 then + + local path=paths[math.random(#paths)] --#OPSTRANSPORT.Path + + return path + end + end + + return nil +end + +--- Add a carrier assigned for this transport. +-- @param #OPSTRANSPORT self +-- @param Ops.OpsGroup#OPSGROUP CarrierGroup Carrier OPSGROUP. +-- @param #string Status Carrier Status. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetCarrierTransportStatus(CarrierGroup, Status) + + -- Old status + local oldstatus=self:GetCarrierTransportStatus(CarrierGroup) + + -- Debug info. + self:T(self.lid..string.format("New carrier transport status for %s: %s --> %s", CarrierGroup:GetName(), oldstatus, Status)) + + -- Set new status. + self.carrierTransportStatus[CarrierGroup.groupname]=Status + + return self +end + +--- Get carrier transport status. +-- @param #OPSTRANSPORT self +-- @param Ops.OpsGroup#OPSGROUP CarrierGroup Carrier OPSGROUP. +-- @return #string Carrier status. +function OPSTRANSPORT:GetCarrierTransportStatus(CarrierGroup) + local status=self.carrierTransportStatus[CarrierGroup.groupname] or "unknown" + return status +end + +--- Get unique ID of the transport assignment. +-- @param #OPSTRANSPORT self +-- @return #number UID. +function OPSTRANSPORT:GetUID() + return self.uid +end + +--- Get number of delivered cargo groups. +-- @param #OPSTRANSPORT self +-- @return #number Total number of delivered cargo groups. +function OPSTRANSPORT:GetNcargoDelivered() + return self.Ndelivered +end + +--- Get number of cargo groups. +-- @param #OPSTRANSPORT self +-- @return #number Total number of cargo groups. +function OPSTRANSPORT:GetNcargoTotal() + return self.Ncargo +end + +--- Get number of carrier groups assigned for this transport. +-- @param #OPSTRANSPORT self +-- @return #number Total number of carrier groups. +function OPSTRANSPORT:GetNcarrier() + return self.Ncarrier +end + +--- Add carrier asset to transport. +-- @param #OPSTRANSPORT self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset to be added. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:AddAsset(Asset, TransportZoneCombo) + + -- Debug info + self:T(self.lid..string.format("Adding asset carrier \"%s\" to transport", tostring(Asset.spawngroupname))) + + -- Add asset to table. + self.assets=self.assets or {} + + table.insert(self.assets, Asset) + + return self +end + +--- Delete carrier asset from transport. +-- @param #OPSTRANSPORT self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset to be removed. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:DelAsset(Asset) + + for i,_asset in pairs(self.assets or {}) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + if asset.uid==Asset.uid then + self:T(self.lid..string.format("Removing asset \"%s\" from transport", tostring(Asset.spawngroupname))) + table.remove(self.assets, i) + return self + end + + end + + return self +end + +--- Add cargo asset. +-- @param #OPSTRANSPORT self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset to be added. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:AddAssetCargo(Asset, TransportZoneCombo) + + -- Debug info + self:T(self.lid..string.format("Adding asset cargo \"%s\" to transport and TZC=%s", tostring(Asset.spawngroupname), TransportZoneCombo and TransportZoneCombo.uid or "N/A")) + + -- Add asset to table. + self.assetsCargo=self.assetsCargo or {} + + table.insert(self.assetsCargo, Asset) + + TransportZoneCombo.assetsCargo=TransportZoneCombo.assetsCargo or {} + + TransportZoneCombo.assetsCargo[Asset.spawngroupname]=Asset + + return self +end + +--- Get transport zone combo of cargo group. +-- @param #OPSTRANSPORT self +-- @param #string GroupName Group name of cargo. +-- @return #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +function OPSTRANSPORT:GetTZCofCargo(GroupName) + + for _,_tzc in pairs(self.tzCombos) do + local tzc=_tzc --#OPSTRANSPORT.TransportZoneCombo + for _,_cargo in pairs(tzc.Cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + if cargo.opsgroup:GetName()==GroupName then + return tzc + end + end + end + + return nil +end + +--- Add LEGION to the transport. +-- @param #OPSTRANSPORT self +-- @param Ops.Legion#LEGION Legion The legion. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:AddLegion(Legion) + + -- Debug info. + self:T(self.lid..string.format("Adding legion %s", Legion.alias)) + + -- Add legion to table. + table.insert(self.legions, Legion) + + return self +end + +--- Remove LEGION from transport. +-- @param #OPSTRANSPORT self +-- @param Ops.Legion#LEGION Legion The legion. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:RemoveLegion(Legion) + + -- Loop over legions + for i=#self.legions,1,-1 do + local legion=self.legions[i] --Ops.Legion#LEGION + if legion.alias==Legion.alias then + -- Debug info. + self:T(self.lid..string.format("Removing legion %s", Legion.alias)) + table.remove(self.legions, i) + return self + end + end + + self:E(self.lid..string.format("ERROR: Legion %s not found and could not be removed!", Legion.alias)) + return self +end + +--- Check if an OPS group is assigned as carrier for this transport. +-- @param #OPSTRANSPORT self +-- @param Ops.OpsGroup#OPSGROUP CarrierGroup Potential carrier OPSGROUP. +-- @return #boolean If true, group is an assigned carrier. +function OPSTRANSPORT:IsCarrier(CarrierGroup) + + if CarrierGroup then + for _,_carrier in pairs(self.carriers) do + local carrier=_carrier --Ops.OpsGroup#OPSGROUP + if carrier.groupname==CarrierGroup.groupname then + return true + end + end + end + + return false +end + +--- Check if transport is ready to be started. +-- * Start time passed. +-- * Stop time did not pass already. +-- * All start conditions are true. +-- @param #OPSTRANSPORT self +-- @return #boolean If true, mission can be started. +function OPSTRANSPORT:IsReadyToGo() + + -- Debug text. + local text=self.lid.."Is ReadyToGo? " + + -- Current abs time. + local Tnow=timer.getAbsTime() + + -- Pickup AND deploy zones must be set. + local gotzones=false + for _,_tz in pairs(self.tzCombos) do + local tz=_tz --#OPSTRANSPORT.TransportZoneCombo + if tz.PickupZone and tz.DeployZone then + gotzones=true + break + end + end + if not gotzones then + text=text.."No, pickup/deploy zone combo not yet defined!" + return false + end + + -- Start time did not pass yet. + if self.Tstart and Tnowself.Tstop then + text=text.."Nope, stop time already passed!" + self:T(text) + return false + end + + -- All start conditions true? + local startme=self:EvalConditionsAll(self.conditionStart) + + -- Nope, not yet. + if not startme then + text=text..("No way, at least one start condition is not true!") + self:T(text) + return false + end + + -- We're good to go! + text=text.."Yes!" + self:T(text) + return true +end + +--- Set LEGION transport status. +-- @param #OPSTRANSPORT self +-- @param Ops.Legion#LEGION Legion The legion. +-- @param #string Status New status. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetLegionStatus(Legion, Status) + + -- Old status + local status=self:GetLegionStatus(Legion) + + -- Debug info. + self:T(self.lid..string.format("Setting LEGION %s to status %s-->%s", Legion.alias, tostring(status), tostring(Status))) + + -- New status. + self.statusLegion[Legion.alias]=Status + + return self +end + +--- Get LEGION transport status. +-- @param #OPSTRANSPORT self +-- @param Ops.Legion#LEGION Legion The legion. +-- @return #string status Current status. +function OPSTRANSPORT:GetLegionStatus(Legion) + + -- Current status. + local status=self.statusLegion[Legion.alias] or "unknown" + + return status +end + +--- Check if state is PLANNED. +-- @param #OPSTRANSPORT self +-- @return #boolean If true, status is PLANNED. +function OPSTRANSPORT:IsPlanned() + local is=self:is(OPSTRANSPORT.Status.PLANNED) + return is +end + +--- Check if state is QUEUED. +-- @param #OPSTRANSPORT self +-- @param Ops.Legion#LEGION Legion (Optional) Check if transport is queued at this legion. +-- @return #boolean If true, status is QUEUED. +function OPSTRANSPORT:IsQueued(Legion) + local is=self:is(OPSTRANSPORT.Status.QUEUED) + if Legion then + is=self:GetLegionStatus(Legion)==OPSTRANSPORT.Status.QUEUED + end + return is +end + +--- Check if state is REQUESTED. +-- @param #OPSTRANSPORT self +-- @param Ops.Legion#LEGION Legion (Optional) Check if transport is queued at this legion. +-- @return #boolean If true, status is REQUESTED. +function OPSTRANSPORT:IsRequested(Legion) + local is=self:is(OPSTRANSPORT.Status.REQUESTED) + if Legion then + is=self:GetLegionStatus(Legion)==OPSTRANSPORT.Status.REQUESTED + end + return is +end + +--- Check if state is SCHEDULED. +-- @param #OPSTRANSPORT self +-- @return #boolean If true, status is SCHEDULED. +function OPSTRANSPORT:IsScheduled() + local is=self:is(OPSTRANSPORT.Status.SCHEDULED) + return is +end + +--- Check if state is EXECUTING. +-- @param #OPSTRANSPORT self +-- @return #boolean If true, status is EXECUTING. +function OPSTRANSPORT:IsExecuting() + local is=self:is(OPSTRANSPORT.Status.EXECUTING) + return is +end + +--- Check if all cargo was delivered (or is dead). +-- @param #OPSTRANSPORT self +-- @param #number Nmin Number of groups that must be actually delivered (and are not dead). Default 0. +-- @return #boolean If true, all possible cargo was delivered. +function OPSTRANSPORT:IsDelivered(Nmin) + local is=self:is(OPSTRANSPORT.Status.DELIVERED) + Nmin=Nmin or 0 + if Nmin>self.Ncargo then + Nmin=self.Ncargo + end + if self.Ndelivered=1 then + + -- Info text. + local text=string.format("%s: Ncargo=%d/%d, Ncarrier=%d/%d, Nlegions=%d", fsmstate:upper(), self.Ncargo, self.Ndelivered, #self.carriers, self.Ncarrier, #self.legions) + + -- Info about cargo and carrier. + if self.verbose>=2 then + + for i,_tz in pairs(self.tzCombos) do + local tz=_tz --#OPSTRANSPORT.TransportZoneCombo + local pickupzone=tz.PickupZone and tz.PickupZone:GetName() or "Unknown" + local deployzone=tz.DeployZone and tz.DeployZone:GetName() or "Unknown" + text=text..string.format("\n[%d] %s --> %s: Ncarriers=%d, Ncargo=%d (%d)", i, pickupzone, deployzone, tz.Ncarriers, #tz.Cargos, tz.Ncargo) + end + + end + + -- Info about cargo and carrier. + if self.verbose>=3 then + + text=text..string.format("\nCargos:") + for _,_cargo in pairs(self:GetCargos()) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + local carrier=cargo.opsgroup:_GetMyCarrierElement() + local name=carrier and carrier.name or "none" + local cstate=carrier and carrier.status or "N/A" + text=text..string.format("\n- %s: %s [%s], weight=%d kg, carrier=%s [%s], delivered=%s [UID=%s]", + cargo.opsgroup:GetName(), cargo.opsgroup.cargoStatus:upper(), cargo.opsgroup:GetState(), cargo.opsgroup:GetWeightTotal(), name, cstate, tostring(cargo.delivered), tostring(cargo.opsgroup.cargoTransportUID)) + end + + text=text..string.format("\nCarriers:") + for _,_carrier in pairs(self.carriers) do + local carrier=_carrier --Ops.OpsGroup#OPSGROUP + text=text..string.format("\n- %s: %s [%s], Cargo Bay [current/reserved/total]=%d/%d/%d kg [free %d/%d/%d kg]", + carrier:GetName(), carrier.carrierStatus:upper(), carrier:GetState(), + carrier:GetWeightCargo(nil, false), carrier:GetWeightCargo(), carrier:GetWeightCargoMax(), + carrier:GetFreeCargobay(nil, false), carrier:GetFreeCargobay(), carrier:GetFreeCargobayMax()) + end + end + + self:I(self.lid..text) + end + + -- Check if all cargo was delivered (or is dead). + self:_CheckDelivered() + + -- Update status again. + if not self:IsDelivered() then + self:__StatusUpdate(-30) + end +end + +--- Check if a cargo group was delivered. +-- @param #OPSTRANSPORT self +-- @param #string GroupName Name of the group. +-- @return #boolean If `true`, cargo was delivered. +function OPSTRANSPORT:IsCargoDelivered(GroupName) + + for _,_cargo in pairs(self:GetCargos()) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + + if cargo.opsgroup:GetName()==GroupName then + return cargo.delivered + end + + end + + return nil +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Event Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "Planned" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSTRANSPORT:onafterPlanned(From, Event, To) + self:T(self.lid..string.format("New status: %s-->%s", From, To)) +end + +--- On after "Scheduled" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSTRANSPORT:onafterScheduled(From, Event, To) + self:T(self.lid..string.format("New status: %s-->%s", From, To)) +end + +--- On after "Executing" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSTRANSPORT:onafterExecuting(From, Event, To) + self:T(self.lid..string.format("New status: %s-->%s", From, To)) +end + +--- On before "Delivered" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSTRANSPORT:onbeforeDelivered(From, Event, To) + + -- Check that we do not call delivered again. + if From==OPSTRANSPORT.Status.DELIVERED then + return false + end + + return true +end + +--- On after "Delivered" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSTRANSPORT:onafterDelivered(From, Event, To) + self:T(self.lid..string.format("New status: %s-->%s", From, To)) + + -- Inform all assigned carriers that cargo was delivered. They can have this in the queue or are currently processing this transport. + for i=#self.carriers, 1, -1 do + local carrier=self.carriers[i] --Ops.OpsGroup#OPSGROUP + if self:GetCarrierTransportStatus(carrier)~=OPSTRANSPORT.Status.DELIVERED then + carrier:Delivered(self) + end + end + +end + +--- On after "Loaded" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo OPSGROUP that was loaded into a carrier. +-- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier OPSGROUP that was loaded into a carrier. +-- @param Ops.OpsGroup#OPSGROUP.Element CarrierElement Carrier element. +function OPSTRANSPORT:onafterLoaded(From, Event, To, OpsGroupCargo, OpsGroupCarrier, CarrierElement) + self:I(self.lid..string.format("Loaded OPSGROUP %s into carrier %s", OpsGroupCargo:GetName(), tostring(CarrierElement.name))) +end + +--- On after "Unloaded" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo Cargo OPSGROUP that was unloaded from a carrier. +-- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier Carrier OPSGROUP that unloaded the cargo. +function OPSTRANSPORT:onafterUnloaded(From, Event, To, OpsGroupCargo, OpsGroupCarrier) + self:I(self.lid..string.format("Unloaded OPSGROUP %s", OpsGroupCargo:GetName())) +end + +--- On after "DeadCarrierGroup" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP OpsGroup Carrier OPSGROUP that is dead. +function OPSTRANSPORT:onafterDeadCarrierGroup(From, Event, To, OpsGroup) + self:I(self.lid..string.format("Carrier OPSGROUP %s dead!", OpsGroup:GetName())) + + -- Increase dead counter. + self.NcarrierDead=self.NcarrierDead+1 + + -- Remove group from carrier list/table. + self:_DelCarrier(OpsGroup) + + if #self.carriers==0 then + self:DeadCarrierAll() + end +end + +--- On after "DeadCarrierAll" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSTRANSPORT:onafterDeadCarrierAll(From, Event, To) + self:I(self.lid..string.format("ALL Carrier OPSGROUPs are dead!")) + + if self.opszone then + + self:I(self.lid..string.format("Cancelling transport on CHIEF level")) + self.chief:TransportCancel(self) + + --for _,_legion in pairs(self.legions) do + -- local legion=_legion --Ops.Legion#LEGION + -- legion:TransportCancel(self) + --end + + else + + -- Check if cargo was delivered. + self:_CheckDelivered() + + -- Set state back to PLANNED if not delivered. + if not self:IsDelivered() then + self:Planned() + end + + end + +end + +--- On after "Cancel" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSTRANSPORT:onafterCancel(From, Event, To) + + -- Number of OPSGROUPS assigned and alive. + local Ngroups = #self.carriers + + -- Debug info. + self:I(self.lid..string.format("CANCELLING transport in status %s. Will wait for %d carrier groups to report DONE before evaluation", self:GetState(), Ngroups)) + + -- Time stamp. + self.Tover=timer.getAbsTime() + + + if self.chief then + + -- Debug info. + self:T(self.lid..string.format("CHIEF will cancel the transport. Will wait for mission DONE before evaluation!")) + + -- CHIEF will cancel the transport. + self.chief:TransportCancel(self) + + elseif self.commander then + + -- Debug info. + self:T(self.lid..string.format("COMMANDER will cancel the transport. Will wait for transport DELIVERED before evaluation!")) + + -- COMMANDER will cancel the transport. + self.commander:TransportCancel(self) + + elseif self.legions and #self.legions>0 then + + -- Loop over all LEGIONs. + for _,_legion in pairs(self.legions or {}) do + local legion=_legion --Ops.Legion#LEGION + + -- Debug info. + self:T(self.lid..string.format("LEGION %s will cancel the transport. Will wait for transport DELIVERED before evaluation!", legion.alias)) + + -- Legion will cancel all flight missions and remove queued request from warehouse queue. + legion:TransportCancel(self) + + end + + else + + -- Debug info. + self:T(self.lid..string.format("No legion, commander or chief. Attached OPS groups will cancel the transport on their own. Will wait for transport DELIVERED before evaluation!")) + + -- Loop over all carrier groups. + for _,_carrier in pairs(self:GetCarriers()) do + local carrier=_carrier --Ops.OpsGroup#OPSGROUP + carrier:TransportCancel(self) + end + + -- Delete awaited transport. + local cargos=self:GetCargoOpsGroups(false) + for _,_cargo in pairs(cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP + cargo:_DelMyLift(self) + end + + + end + + -- Special mission states. + if self:IsPlanned() or self:IsQueued() or self:IsRequested() or Ngroups==0 then + self:T(self.lid..string.format("Cancelled transport was in %s stage with %d carrier groups assigned and alive. Call it DELIVERED!", self:GetState(), Ngroups)) + self:Delivered() + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check if all cargo of this transport assignment was delivered. +-- @param #OPSTRANSPORT self +function OPSTRANSPORT:_CheckDelivered() + + -- First check that at least one cargo was added (as we allow to do that later). + if self.Ncargo>0 then + + local done=true + local dead=true + for _,_cargo in pairs(self:GetCargos()) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + + if cargo.delivered then + -- This one is delivered. + dead=false + elseif cargo.opsgroup==nil then + -- This one is nil?! + dead=false + elseif cargo.opsgroup:IsDestroyed() then + -- This one was destroyed. + elseif cargo.opsgroup:IsDead() then + -- This one is dead. + elseif cargo.opsgroup:IsStopped() then + -- This one is stopped. + dead=false + else + done=false --Someone is not done! + dead=false + end + + end + + if dead then + self:I(self.lid.."All cargo DEAD ==> Delivered!") + self:Delivered() + elseif done then + self:I(self.lid.."All cargo DONE ==> Delivered!") + self:Delivered() + end + + end + +end + +--- Check if all required cargos are loaded. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #boolean If true, all required cargos are loaded or there is no required cargo. +function OPSTRANSPORT:_CheckRequiredCargos(TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + local requiredCargos=TransportZoneCombo.RequiredCargos + + if requiredCargos==nil or #requiredCargos==0 then + return true + end + + local carrierNames=self:_GetCarrierNames() + + local gotit=true + for _,_cargo in pairs(requiredCargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP + + + if not cargo:IsLoaded(carrierNames) then + return false + end + + end + + return true +end + +--- Check if all given condition are true. +-- @param #OPSTRANSPORT 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 OPSTRANSPORT:EvalConditionsAll(Conditions) + + -- Any stop condition must be true. + for _,_condition in pairs(Conditions or {}) do + local condition=_condition --#OPSTRANSPORT.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 + + + +--- Find transfer carrier element for cargo group. +-- @param #OPSTRANSPORT self +-- @param Ops.OpsGroup#OPSGROUP CargoGroup The cargo group that needs to be loaded into a carrier unit/element of the carrier group. +-- @param Core.Zone#ZONE Zone (Optional) Zone where the carrier must be in. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return Ops.OpsGroup#OPSGROUP.Element New carrier element for cargo or nil. +-- @return Ops.OpsGroup#OPSGROUP New carrier group for cargo or nil. +function OPSTRANSPORT:FindTransferCarrierForCargo(CargoGroup, Zone, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + local carrier=nil --Ops.OpsGroup#OPSGROUP.Element + local carrierGroup=nil --Ops.OpsGroup#OPSGROUP + + --TODO: maybe sort the carriers wrt to largest free cargo bay. Or better smallest free cargo bay that can take the cargo group weight. + + for _,_carrier in pairs(TransportZoneCombo.DisembarkCarriers) do + local carrierGroup=_carrier --Ops.OpsGroup#OPSGROUP + + -- First check if carrier is alive and loading cargo. + if carrierGroup and carrierGroup:IsAlive() and (carrierGroup:IsLoading() or TransportZoneCombo.DeployAirbase) then + + -- Find an element of the group that has enough free space. + carrier=carrierGroup:FindCarrierForCargo(CargoGroup) + + if carrier then + if Zone==nil or Zone:IsVec2InZone(carrier.unit:GetVec2()) then + return carrier, carrierGroup + else + self:T2(self.lid.."Got transfer carrier but carrier not in zone (yet)!") + end + else + self:T2(self.lid.."No transfer carrier available!") + end + + end + end + + return nil, nil +end + +--- Create a cargo group data structure. +-- @param #OPSTRANSPORT self +-- @param Wrapper.Group#GROUP group The GROUP or OPSGROUP object. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @param #boolean DisembarkActivation If `true`, cargo group is activated when disembarked. +-- @return Ops.OpsGroup#OPSGROUP.CargoGroup Cargo group data. +function OPSTRANSPORT:_CreateCargoGroupData(group, TransportZoneCombo, DisembarkActivation) + + -- Get ops group. + local opsgroup=self:_GetOpsGroupFromObject(group) + + -- First check that this group is not already contained in this TZC. + for _,_cargo in pairs(TransportZoneCombo.Cargos or {}) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + if cargo.opsgroup.groupname==opsgroup.groupname then + -- Group is already contained. + return nil + end + end + + + -- Create a new data item. + local cargo={} --Ops.OpsGroup#OPSGROUP.CargoGroup + + cargo.opsgroup=opsgroup + cargo.delivered=false + cargo.status="Unknown" + cargo.disembarkActivation=DisembarkActivation + cargo.tzcUID=TransportZoneCombo + + return cargo +end + +--- Count how many cargo groups are inside a zone. +-- @param #OPSTRANSPORT self +-- @param Core.Zone#ZONE Zone The zone object. +-- @param #boolean Delivered If `true`, only delivered groups are returned. If `false` only undelivered groups are returned. If `nil`, all groups are returned. +-- @param Ops.OpsGroup#OPSGROUP Carrier (Optional) Only count cargo groups that fit into the given carrier group. Current cargo is not a factor. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #number Number of cargo groups. +function OPSTRANSPORT:_CountCargosInZone(Zone, Delivered, Carrier, TransportZoneCombo) + + -- Get cargo ops groups. + local cargos=self:GetCargoOpsGroups(Delivered, Carrier, TransportZoneCombo) + + --- Function to check if carrier is supposed to be disembarked to. + local function iscarrier(_cargo) + local cargo=_cargo --Ops.OpsGroup#OPSGROUP + + local mycarrier=cargo:_GetMyCarrierGroup() + + if mycarrier and mycarrier:IsUnloading() then + + local carriers=mycarrier.cargoTransport:GetDisembarkCarriers(mycarrier.cargoTZC) + + for _,_carrier in pairs(carriers) do + local carrier=_carrier --Ops.OpsGroup#OPSGROUP + if Carrier:GetName()==carrier:GetName() then + return true + end + end + + end + + return false + end + + local N=0 + for _,_cargo in pairs(cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP + + -- Is not cargo? + local isNotCargo=cargo:IsNotCargo(true) + if not isNotCargo then + isNotCargo=iscarrier(cargo) + end + + -- Is in zone? + local isInZone=cargo:IsInZone(Zone) + + -- Is in utero? + local isInUtero=cargo:IsInUtero() + + -- Debug info. + self:T(self.lid..string.format("Cargo=%s: notcargo=%s, iscarrier=%s inzone=%s, inutero=%s", cargo:GetName(), tostring(cargo:IsNotCargo(true)), tostring(iscarrier(cargo)), tostring(isInZone), tostring(isInUtero))) + + -- We look for groups that are not cargo, in the zone or in utero. + if isNotCargo and (isInZone or isInUtero) then + N=N+1 + end + end + + -- Debug info. + self:T(self.lid..string.format("Found %d units in zone %s", N, Zone:GetName())) + + return N +end + +--- Get a transport zone combination (TZC) for a carrier group. The pickup zone will be a zone, where the most cargo groups are located that fit into the carrier. +-- @param #OPSTRANSPORT self +-- @param Ops.OpsGroup#OPSGROUP Carrier The carrier OPS group. +-- @return Core.Zone#ZONE Pickup zone or `#nil`. +function OPSTRANSPORT:_GetTransportZoneCombo(Carrier) + + --- Selection criteria + -- * Distance: pickup zone should be as close as possible. + -- * Ncargo: Number of cargo groups. Pickup, where there is most cargo. + -- * Ncarrier: Number of carriers already "working" on this TZC. Would be better if not all carriers work on the same combo while others are ignored. + + -- Get carrier position. + local vec2=Carrier:GetVec2() + + --- Penalty function. + local function penalty(candidate) + local p=candidate.ncarriers*10-candidate.ncargo+candidate.distance/10 + return p + end + + -- TZC candidates. + local candidates={} + + for i,_transportzone in pairs(self.tzCombos) do + local tz=_transportzone --#OPSTRANSPORT.TransportZoneCombo + + -- Check that pickup and deploy zones were defined. + if tz.PickupZone and tz.DeployZone and tz.EmbarkZone then + + --TODO: Check if Carrier is an aircraft and if so, check that pickup AND deploy zones are airbases (not ships, not farps). + + -- Count undelivered cargos in embark(!) zone that fit into the carrier. + local ncargo=self:_CountCargosInZone(tz.EmbarkZone, false, Carrier, tz) + + -- At least one group in the zone. + if ncargo>=1 then + + -- Distance to the carrier in meters. + local dist=tz.PickupZone:Get2DDistance(vec2) + + local ncarriers=0 + for _,_carrier in pairs(self.carriers) do + local carrier=_carrier --Ops.OpsGroup#OPSGROUP + if carrier and carrier:IsAlive() and carrier.cargoTZC and carrier.cargoTZC.uid==tz.uid then + ncarriers=ncarriers+1 + end + end + + -- New candidate. + local candidate={tzc=tz, distance=dist/1000, ncargo=ncargo, ncarriers=ncarriers} + + -- Calculdate penalty of candidate. + candidate.penalty=penalty(candidate) + + -- Add candidate. + table.insert(candidates, candidate) + + end + end + end + + if #candidates>0 then + + -- Minimize penalty. + local function optTZC(candA, candB) + return candA.penalty=3 then + local text="TZC optimized" + for i,candidate in pairs(candidates) do + text=text..string.format("\n[%d] TPZ=%d, Ncarriers=%d, Ncargo=%d, Distance=%.1f km, PENALTY=%d", i, candidate.tzc.uid, candidate.ncarriers, candidate.ncargo, candidate.distance, candidate.penalty) + end + self:I(self.lid..text) + end + + -- Return best candidate. + return candidates[1].tzc + else + -- No candidates. + self:T(self.lid..string.format("Could NOT find a pickup zone (with cargo) for carrier group %s", Carrier:GetName())) + end + + return nil +end + +--- 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. +function OPSTRANSPORT:_GetOpsGroupFromObject(Object) + + local opsgroup=nil + + if Object:IsInstanceOf("OPSGROUP") then + -- We already have an OPSGROUP + opsgroup=Object + elseif Object:IsInstanceOf("GROUP") then + + -- Look into DB and try to find an existing OPSGROUP. + opsgroup=_DATABASE:GetOpsGroup(Object) + + if not opsgroup then + if Object:IsAir() then + opsgroup=FLIGHTGROUP:New(Object) + elseif Object:IsShip() then + opsgroup=NAVYGROUP:New(Object) + else + opsgroup=ARMYGROUP:New(Object) + end + end + + else + self:E(self.lid.."ERROR: Object must be a GROUP or OPSGROUP object!") + return nil + end + + return opsgroup +end --- **Ops** -- Combat Search and Rescue. -- -- === @@ -144009,6 +171201,14 @@ end -- -- === -- +-- ## Missions:--- **Ops** -- Combat Search and Rescue. +-- +-- === +-- +-- **CSAR** - MOOSE based Helicopter CSAR Operations. +-- +-- === +-- -- ## Missions: -- -- ### [CSAR - Combat Search & Rescue](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20CSAR) @@ -144025,7 +171225,7 @@ end -- @module Ops.CSAR -- @image OPS_CSAR.jpg --- Date: Oct 2021 +-- Date: June 2022 ------------------------------------------------------------------------- --- **CSAR** class, extends Core.Base#BASE, Core.Fsm#FSM @@ -144048,6 +171248,7 @@ end -- * Object oriented refactoring of Ciribob\'s fantastic CSAR script. -- * No need for extra MIST loading. -- * Additional events to tailor your mission. +-- * Optional SpawnCASEVAC to create casualties without beacon (e.g. handling dead ground vehicles and create CASVAC requests). -- -- ## 0. Prerequisites -- @@ -144070,61 +171271,69 @@ end -- -- The following options are available (with their defaults). Only set the ones you want changed: -- --- self.allowDownedPilotCAcontrol = false -- Set to false if you don\'t want to allow control by Combined Arms. --- self.allowFARPRescue = true -- allows pilots to be rescued by landing at a FARP or Airbase. Else MASH only! --- self.FARPRescueDistance = 1000 -- you need to be this close to a FARP or Airport for the pilot to be rescued. --- self.autosmoke = false -- automatically smoke a downed pilot\'s location when a heli is near. --- self.autosmokedistance = 1000 -- distance for autosmoke --- self.coordtype = 1 -- Use Lat/Long DDM (0), Lat/Long DMS (1), MGRS (2), Bullseye imperial (3) or Bullseye metric (4) for coordinates. --- self.csarOncrash = false -- (WIP) If set to true, will generate a downed pilot when a plane crashes as well. --- self.enableForAI = false -- set to false to disable AI pilots from being rescued. --- self.pilotRuntoExtractPoint = true -- Downed pilot will run to the rescue helicopter up to self.extractDistance in meters. --- self.extractDistance = 500 -- Distance the downed pilot will start to run to the rescue helicopter. --- self.immortalcrew = true -- Set to true to make wounded crew immortal. --- self.invisiblecrew = false -- Set to true to make wounded crew insvisible. --- self.loadDistance = 75 -- configure distance for pilots to get into helicopter in meters. --- self.mashprefix = {"MASH"} -- prefixes of #GROUP objects used as MASHes. --- self.max_units = 6 -- max number of pilots that can be carried if #CSAR.AircraftType is undefined. --- self.messageTime = 15 -- Time to show messages for in seconds. Doubled for long messages. --- self.radioSound = "beacon.ogg" -- the name of the sound file to use for the pilots\' radio beacons. --- self.smokecolor = 4 -- Color of smokemarker, 0 is green, 1 is red, 2 is white, 3 is orange and 4 is blue. --- self.useprefix = true -- Requires CSAR helicopter #GROUP names to have the prefix(es) defined below. --- self.csarPrefix = { "helicargo", "MEDEVAC"} -- #GROUP name prefixes used for useprefix=true - DO NOT use # in helicopter names in the Mission Editor! --- self.verbose = 0 -- set to > 1 for stats output for debugging. +-- mycsar.allowDownedPilotCAcontrol = false -- Set to false if you don\'t want to allow control by Combined Arms. +-- mycsar.allowFARPRescue = true -- allows pilots to be rescued by landing at a FARP or Airbase. Else MASH only! +-- mycsar.FARPRescueDistance = 1000 -- you need to be this close to a FARP or Airport for the pilot to be rescued. +-- mycsar.autosmoke = false -- automatically smoke a downed pilot\'s location when a heli is near. +-- mycsar.autosmokedistance = 1000 -- distance for autosmoke +-- mycsar.coordtype = 1 -- Use Lat/Long DDM (0), Lat/Long DMS (1), MGRS (2), Bullseye imperial (3) or Bullseye metric (4) for coordinates. +-- mycsar.csarOncrash = false -- (WIP) If set to true, will generate a downed pilot when a plane crashes as well. +-- mycsar.enableForAI = false -- set to false to disable AI pilots from being rescued. +-- mycsar.pilotRuntoExtractPoint = true -- Downed pilot will run to the rescue helicopter up to mycsar.extractDistance in meters. +-- mycsar.extractDistance = 500 -- Distance the downed pilot will start to run to the rescue helicopter. +-- mycsar.immortalcrew = true -- Set to true to make wounded crew immortal. +-- mycsar.invisiblecrew = false -- Set to true to make wounded crew insvisible. +-- mycsar.loadDistance = 75 -- configure distance for pilots to get into helicopter in meters. +-- mycsar.mashprefix = {"MASH"} -- prefixes of #GROUP objects used as MASHes. +-- mycsar.max_units = 6 -- max number of pilots that can be carried if #CSAR.AircraftType is undefined. +-- mycsar.messageTime = 15 -- Time to show messages for in seconds. Doubled for long messages. +-- mycsar.radioSound = "beacon.ogg" -- the name of the sound file to use for the pilots\' radio beacons. +-- mycsar.smokecolor = 4 -- Color of smokemarker, 0 is green, 1 is red, 2 is white, 3 is orange and 4 is blue. +-- mycsar.useprefix = true -- Requires CSAR helicopter #GROUP names to have the prefix(es) defined below. +-- mycsar.csarPrefix = { "helicargo", "MEDEVAC"} -- #GROUP name prefixes used for useprefix=true - DO NOT use # in helicopter names in the Mission Editor! +-- mycsar.verbose = 0 -- set to > 1 for stats output for debugging. -- -- (added 0.1.4) limit amount of downed pilots spawned by **ejection** events --- self.limitmaxdownedpilots = true --- self.maxdownedpilots = 10 +-- mycsar.limitmaxdownedpilots = true +-- mycsar.maxdownedpilots = 10 -- -- (added 0.1.8) - allow to set far/near distance for approach and optionally pilot must open doors --- self.approachdist_far = 5000 -- switch do 10 sec interval approach mode, meters --- self.approachdist_near = 3000 -- switch to 5 sec interval approach mode, meters --- self.pilotmustopendoors = false -- switch to true to enable check of open doors +-- mycsar.approachdist_far = 5000 -- switch do 10 sec interval approach mode, meters +-- mycsar.approachdist_near = 3000 -- switch to 5 sec interval approach mode, meters +-- mycsar.pilotmustopendoors = false -- switch to true to enable check of open doors -- -- (added 0.1.9) --- self.suppressmessages = false -- switch off all messaging if you want to do your own +-- mycsar.suppressmessages = false -- switch off all messaging if you want to do your own -- -- (added 0.1.11) --- self.rescuehoverheight = 20 -- max height for a hovering rescue in meters --- self.rescuehoverdistance = 10 -- max distance for a hovering rescue in meters +-- mycsar.rescuehoverheight = 20 -- max height for a hovering rescue in meters +-- mycsar.rescuehoverdistance = 10 -- max distance for a hovering rescue in meters -- -- (added 0.1.12) -- -- Country codes for spawned pilots --- self.countryblue= country.id.USA --- self.countryred = country.id.RUSSIA --- self.countryneutral = country.id.UN_PEACEKEEPERS --- +-- mycsar.countryblue= country.id.USA +-- mycsar.countryred = country.id.RUSSIA +-- mycsar.countryneutral = country.id.UN_PEACEKEEPERS +-- -- ## 2.1 Experimental Features -- -- WARNING - Here\'ll be dragons! -- DANGER - For this to work you need to de-sanitize your mission environment (all three entries) in \Scripts\MissionScripting.lua -- Needs SRS => 1.9.6 to work (works on the **server** side of SRS) --- self.useSRS = false -- Set true to use FF\'s SRS integration --- self.SRSPath = "E:\\Progra~1\\DCS-SimpleRadio-Standalone\\" -- adjust your own path in your SRS installation -- server(!) --- self.SRSchannel = 300 -- radio channel --- self.SRSModulation = radio.modulation.AM -- modulation --- +-- mycsar.useSRS = false -- Set true to use FF\'s SRS integration +-- mycsar.SRSPath = "C:\\Progra~1\\DCS-SimpleRadio-Standalone\\" -- adjust your own path in your SRS installation -- server(!) +-- mycsar.SRSchannel = 300 -- radio channel +-- mycsar.SRSModulation = radio.modulation.AM -- modulation +-- mycsar.SRSport = 5002 -- and SRS Server port +-- mycsar.SRSCulture = "en-GB" -- SRS voice culture +-- mycsar.SRSVoice = nil -- SRS voice, relevant for Google TTS +-- mycsar.SRSGPathToCredentials = nil -- Path to your Google credentials json file, set this if you want to use Google TTS +-- mycsar.SRSVolume = 1 -- Volume, between 0 and 1 +-- -- +-- mycsar.csarUsePara = false -- If set to true, will use the LandingAfterEjection Event instead of Ejection. Requires mycsar.enableForAI to be set to true. --shagrat +-- mycsar.wetfeettemplate = "man in floating thingy" -- if you use a mod to have a pilot in a rescue float, put the template name in here for wet feet spawns. Note: in conjunction with csarUsePara this might create dual ejected pilots in edge cases. +-- -- ## 3. Results -- -- Number of successful landings with save pilots and aggregated number of saved pilots is stored in these variables in the object: -- --- self.rescues -- number of successful landings *with* saved pilots --- self.rescuedpilots -- aggregated number of pilots rescued from the field (of *all* players) +-- mycsar.rescues -- number of successful landings *with* saved pilots +-- mycsar.rescuedpilots -- aggregated number of pilots rescued from the field (of *all* players) -- -- ## 4. Events -- @@ -144151,7 +171360,7 @@ end -- -- The pilot has been boarded to the helicopter. Use e.g. `function my_csar:OnAfterBoarded(...)` to link into this event: -- --- function my_csar:OnAfterBoarded(from, event, to, heliname, groupname) +-- function my_csar:OnAfterBoarded(from, event, to, heliname, groupname, description) -- ... your code here ... -- end -- @@ -144178,6 +171387,8 @@ end -- -- Create downed "Pilot Wagner" in #ZONE "CSAR_Start_1" at a random point for the blue coalition -- my_csar:SpawnCSARAtZone( "CSAR_Start_1", coalition.side.BLUE, "Pilot Wagner", true ) -- +-- --Create a casualty and CASEVAC request from a "Point" (VEC2) for the blue coalition --shagrat +-- my_csar:SpawnCASEVAC(Point, coalition.side.BLUE) -- -- @field #CSAR CSAR = { @@ -144225,8 +171436,9 @@ CSAR = { -- @field #number frequency Frequency of the NDB. -- @field #string player Player name if applicable. -- @field Wrapper.Group#GROUP group Spawned group object. --- @field #number timestamp Timestamp for approach process --- @field #boolean alive Group is alive or dead/rescued +-- @field #number timestamp Timestamp for approach process. +-- @field #boolean alive Group is alive or dead/rescued. +-- @field #boolean wetfeet Group is spawned over (deep) water. --- All slot / Limit settings -- @type CSAR.AircraftType @@ -144241,11 +171453,13 @@ CSAR.AircraftType["Mi-8MTV2"] = 12 CSAR.AircraftType["Mi-8MT"] = 12 CSAR.AircraftType["Mi-24P"] = 8 CSAR.AircraftType["Mi-24V"] = 8 -CSAR.AircraftType["Bell-47"] = 2 +CSAR.AircraftType["Bell-47"] = 2 +CSAR.AircraftType["UH-60L"] = 10 +CSAR.AircraftType["AH-64D_BLK_II"] = 2 --- CSAR class version. -- @field #string version -CSAR.version="0.1.11r2" +CSAR.version="1.0.6" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list @@ -144253,7 +171467,7 @@ CSAR.version="0.1.11r2" -- DONE: SRS Integration (to be tested) -- TODO: Maybe - add option to smoke/flare closest MASH - +-- TODO: shagrat Add cargoWeight to helicopter when pilot boarded ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -144314,6 +171528,7 @@ function CSAR:New(Coalition, Template, Alias) self:AddTransition("*", "Status", "*") -- CSAR status update. self:AddTransition("*", "PilotDown", "*") -- Downed Pilot added self:AddTransition("*", "Approach", "*") -- CSAR heli closing in. + self:AddTransition("*", "Landed", "*") -- CSAR heli landed self:AddTransition("*", "Boarded", "*") -- Pilot boarded. self:AddTransition("*", "Returning", "*") -- CSAR able to return to base. self:AddTransition("*", "Rescued", "*") -- Pilot at MASH. @@ -144354,6 +171569,7 @@ function CSAR:New(Coalition, Template, Alias) self.extractDistance = 500 -- Distance the Downed pilot will run to the rescue helicopter self.loadtimemax = 135 -- seconds self.radioSound = "beacon.ogg" -- the name of the sound file to use for the Pilot radio beacons. If this isnt added to the mission BEACONS WONT WORK! + self.beaconRefresher = 29 -- seconds self.allowFARPRescue = true --allows pilot to be rescued by landing at a FARP or Airbase self.FARPRescueDistance = 1000 -- you need to be this close to a FARP or Airport for the pilot to be rescued. self.max_units = 6 --max number of pilots that can be carried @@ -144383,7 +171599,14 @@ function CSAR:New(Coalition, Template, Alias) self.countryblue= country.id.USA self.countryred = country.id.RUSSIA self.countryneutral = country.id.UN_PEACEKEEPERS - + + -- added 0.1.3 + self.csarUsePara = false -- shagrat set to true, will use the LandingAfterEjection Event instead of Ejection + + -- added 0.1.4 + self.wetfeettemplate = nil + self.usewetfeet = false + -- WARNING - here\'ll be dragons -- for this to work you need to de-sanitize your mission environment in \Scripts\MissionScripting.lua -- needs SRS => 1.9.6 to work (works on the *server* side) @@ -144391,6 +171614,11 @@ function CSAR:New(Coalition, Template, Alias) self.SRSPath = "E:\\Progra~1\\DCS-SimpleRadio-Standalone\\" -- adjust your own path in your server(!) self.SRSchannel = 300 -- radio channel self.SRSModulation = radio.modulation.AM -- modulation + self.SRSport = 5002 -- port + self.SRSCulture = "en-GB" + self.SRSVoice = nil + self.SRSGPathToCredentials = nil + self.SRSVolume = 1 ------------------------ --- Pseudo Functions --- @@ -144442,7 +171670,16 @@ function CSAR:New(Coalition, Template, Alias) -- @param #string Heliname Name of the helicopter group. -- @param #string Woundedgroupname Name of the downed pilot\'s group. - --- On After "Boarded" event. Downed pilot boarded heli. + --- On After "Landed" event. Heli landed at an airbase. + -- @function [parent=#CSAR] OnAfterLanded + -- @param #CSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string HeliName Name of the #UNIT which has landed. + -- @param Wrapper.Airbase#AIRBASE Airbase Airbase where the heli landed. + + --- On After "Boarded" event. Downed pilot boarded heli. -- @function [parent=#CSAR] OnAfterBoarded -- @param #CSAR self -- @param #string From From state. @@ -144450,6 +171687,7 @@ function CSAR:New(Coalition, Template, Alias) -- @param #string To To state. -- @param #string Heliname Name of the helicopter group. -- @param #string Woundedgroupname Name of the downed pilot\'s group. + -- @param #string Description Descriptive name of the group. --- On After "Returning" event. Heli can return home with downed pilot(s). -- @function [parent=#CSAR] OnAfterReturning @@ -144495,8 +171733,9 @@ end -- @param #string Typename Typename of unit. -- @param #number Frequency Frequency of the NDB in Hz -- @param #string Playername Name of Player (if applicable) +-- @param #boolean Wetfeet Ejected over water -- @return #CSAR self. -function CSAR:_CreateDownedPilotTrack(Group,Groupname,Side,OriginalUnit,Description,Typename,Frequency,Playername) +function CSAR:_CreateDownedPilotTrack(Group,Groupname,Side,OriginalUnit,Description,Typename,Frequency,Playername,Wetfeet) self:T({"_CreateDownedPilotTrack",Groupname,Side,OriginalUnit,Description,Typename,Frequency,Playername}) -- create new entry @@ -144512,6 +171751,7 @@ function CSAR:_CreateDownedPilotTrack(Group,Groupname,Side,OriginalUnit,Descript DownedPilot.group = Group DownedPilot.timestamp = 0 DownedPilot.alive = true + DownedPilot.wetfeet = Wetfeet or false -- Add Pilot local PilotTable = self.downedPilots @@ -144561,17 +171801,23 @@ end -- @param #number country Country for template. -- @param Core.Point#COORDINATE point Coordinate to spawn at. -- @param #number frequency Frequency of the pilot's beacon +-- @param #boolean wetfeet Spawn is over water -- @return Wrapper.Group#GROUP group The #GROUP object. -- @return #string alias The alias name. -function CSAR:_SpawnPilotInField(country,point,frequency) - self:T({country,point,frequency}) +function CSAR:_SpawnPilotInField(country,point,frequency,wetfeet) + self:T({country,point,frequency,tostring(wetfeet)}) local freq = frequency or 1000 local freq = freq / 1000 -- kHz for i=1,10 do math.random(i,10000) end - if point:IsSurfaceTypeWater() then point.y = 0 end + if point:IsSurfaceTypeWater() or wetfeet then + point.y = 0 + end local template = self.template + if self.usewetfeet and wetfeet then + template = self.wetfeettemplate + end local alias = string.format("Pilot %.2fkHz-%d", freq, math.random(1,99)) local coalition = self.coalition local pilotcacontrol = self.allowDownedPilotCAcontrol -- Switch AI on/oof - is this really correct for CA? @@ -144637,21 +171883,31 @@ function CSAR:_AddCsar(_coalition , _country, _point, _typeName, _unitName, _pla self:T({_coalition , _country, _point, _typeName, _unitName, _playerName, _freq, noMessage, _description}) local template = self.template + local wetfeet = false + + local surface = _point:GetSurfaceType() + if surface == land.SurfaceType.WATER then + wetfeet = true + end if not _freq then _freq = self:_GenerateADFFrequency() if not _freq then _freq = 333000 end --noob catch end - local _spawnedGroup, _alias = self:_SpawnPilotInField(_country,_point,_freq) + local _spawnedGroup, _alias = self:_SpawnPilotInField(_country,_point,_freq,wetfeet) local _typeName = _typeName or "Pilot" if not noMessage then + if _freq ~= 0 then --shagrat different CASEVAC msg self:_DisplayToAllSAR("MAYDAY MAYDAY! " .. _typeName .. " is down. ", self.coalition, self.messageTime) + else + self:_DisplayToAllSAR("Troops In Contact. " .. _typeName .. " requests CASEVAC. ", self.coalition, self.messageTime) + end end - if _freq then + if (_freq and _freq ~= 0) then --shagrat only add beacon if _freq is NOT 0 self:_AddBeaconToGroup(_spawnedGroup, _freq) end @@ -144660,25 +171916,33 @@ function CSAR:_AddCsar(_coalition , _country, _point, _typeName, _unitName, _pla local _text = _description if not forcedesc then if _playerName ~= nil then - _text = "Pilot " .. _playerName - elseif _unitName ~= nil then - _text = "AI Pilot of " .. _unitName + if _freq ~= 0 then --shagrat + _text = "Pilot " .. _playerName + else + _text = "TIC - " .. _playerName end + elseif _unitName ~= nil then + if _freq ~= 0 then --shagrat + _text = "AI Pilot of " .. _unitName + else + _text = "TIC - " .. _unitName + end + end end self:T({_spawnedGroup, _alias}) local _GroupName = _spawnedGroup:GetName() or _alias - self:_CreateDownedPilotTrack(_spawnedGroup,_GroupName,_coalition,_unitName,_text,_typeName,_freq,_playerName) + self:_CreateDownedPilotTrack(_spawnedGroup,_GroupName,_coalition,_unitName,_text,_typeName,_freq,_playerName,wetfeet) - self:_InitSARForPilot(_spawnedGroup, _GroupName, _freq, noMessage) + self:_InitSARForPilot(_spawnedGroup, _unitName, _freq, noMessage) --shagrat use unitName to have the aircraft callsign / descriptive "name" etc. return self end --- (Internal) Function to add a CSAR object into the scene at a zone coordinate. For mission designers wanting to add e.g. PoWs to the scene. -- @param #CSAR self --- @param #string _zone Name of the zone. +-- @param #string _zone Name of the zone. Can also be passed as a (normal, round) ZONE object. -- @param #number _coalition Coalition. -- @param #string _description (optional) Description. -- @param #boolean _randomPoint (optional) Random yes or no. @@ -144689,7 +171953,16 @@ end function CSAR:_SpawnCsarAtZone( _zone, _coalition, _description, _randomPoint, _nomessage, unitname, typename, forcedesc) self:T(self.lid .. " _SpawnCsarAtZone") local freq = self:_GenerateADFFrequency() - local _triggerZone = ZONE:New(_zone) -- trigger to use as reference position + + local _triggerZone = nil + if type(_zone) == "string" then + _triggerZone = ZONE:New(_zone) -- trigger to use as reference position + elseif type(_zone) == "table" and _zone.ClassName then + if string.find(_zone.ClassName, "ZONE",1) then + _triggerZone = _zone -- is already a zone + end + end + if _triggerZone == nil then self:E(self.lid.."ERROR: Can\'t find zone called " .. _zone, 10) return @@ -144723,7 +171996,7 @@ end --- Function to add a CSAR object into the scene at a zone coordinate. For mission designers wanting to add e.g. PoWs to the scene. -- @param #CSAR self --- @param #string Zone Name of the zone. +-- @param #string Zone Name of the zone. Can also be passed as a (normal, round) ZONE object. -- @param #number Coalition Coalition. -- @param #string Description (optional) Description. -- @param #boolean RandomPoint (optional) Random yes or no. @@ -144740,6 +172013,58 @@ function CSAR:SpawnCSARAtZone(Zone, Coalition, Description, RandomPoint, Nomessa return self end +--- (Internal) Function to add a CSAR object into the scene at a Point coordinate (VEC_2). For mission designers wanting to add e.g. casualties to the scene, that don't use beacons. +-- @param #CSAR self +-- @param #string _Point a POINT_VEC2. +-- @param #number _coalition Coalition. +-- @param #string _description (optional) Description. +-- @param #boolean _nomessage (optional) If true, don\'t send a message to SAR. +-- @param #string unitname (optional) Name of the lost unit. +-- @param #string typename (optional) Type of plane. +-- @param #boolean forcedesc (optional) Force to use the description passed only for the pilot track entry. Use to have fully custom names. +function CSAR:_SpawnCASEVAC( _Point, _coalition, _description, _nomessage, unitname, typename, forcedesc) --shagrat added internal Function _SpawnCASEVAC + self:T(self.lid .. " _SpawnCASEVAC") + + local _description = _description or "CASEVAC" + local unitname = unitname or "CASEVAC" + local typename = typename or "Ground Commander" + + local pos = {} + pos = _Point + + local _country = 0 + if _coalition == coalition.side.BLUE then + _country = self.countryblue + elseif _coalition == coalition.side.RED then + _country = self.countryred + else + _country = self.countryneutral + end + --shagrat set frequency to 0 as "flag" for no beacon + self:_AddCsar(_coalition, _country, pos, typename, unitname, _description, 0, _nomessage, _description, forcedesc) + + return self +end + +--- Function to add a CSAR object into the scene at a zone coordinate. For mission designers wanting to add e.g. PoWs to the scene. +-- @param #CSAR self +-- @param #string Point a POINT_VEC2. +-- @param #number Coalition Coalition. +-- @param #string Description (optional) Description. +-- @param #boolean addBeacon (optional) yes or no. +-- @param #boolean Nomessage (optional) If true, don\'t send a message to SAR. +-- @param #string Unitname (optional) Name of the lost unit. +-- @param #string Typename (optional) Type of plane. +-- @param #boolean Forcedesc (optional) Force to use the **description passed only** for the pilot track entry. Use to have fully custom names. +-- @usage If missions designers want to spawn downed pilots into the field, e.g. at mission begin, to give the helicopter guys work, they can do this like so: +-- +-- -- Create casualty "CASEVAC" at Point #POINT_VEC2 for the blue coalition. +-- my_csar:SpawnCASEVAC( POINT_VEC2, coalition.side.BLUE ) +function CSAR:SpawnCASEVAC(Point, Coalition, Description, Nomessage, Unitname, Typename, Forcedesc) + self:_SpawnCASEVAC(Point, Coalition, Description, Nomessage, Unitname, Typename, Forcedesc) + return self +end --shagrat end added CASEVAC + --- (Internal) Event handler. -- @param #CSAR self function CSAR:_EventHandler(EventData) @@ -144748,9 +172073,14 @@ function CSAR:_EventHandler(EventData) local _event = EventData -- Core.Event#EVENTDATA + -- no Player + if self.enableForAI == false and _event.IniPlayerName == nil then + return self + end + -- no event if _event == nil or _event.initiator == nil then - return false + return self -- take off elseif _event.id == EVENTS.Takeoff then -- taken off @@ -144758,35 +172088,43 @@ function CSAR:_EventHandler(EventData) local _coalition = _event.IniCoalition if _coalition ~= self.coalition then - return --ignore! + return self --ignore! end if _event.IniGroupName then self.takenOff[_event.IniUnitName] = true end - return true + return self -- player enter unit elseif _event.id == EVENTS.PlayerEnterAircraft or _event.id == EVENTS.PlayerEnterUnit then --player entered unit self:T(self.lid .. " Event unit - Player Enter") local _coalition = _event.IniCoalition + self:T("Coalition = "..UTILS.GetCoalitionName(_coalition)) if _coalition ~= self.coalition then - return --ignore! + return self --ignore! end if _event.IniPlayerName then self.takenOff[_event.IniPlayerName] = nil end + -- jumped into flying plane? + self:T("Taken Off: "..tostring(_event.IniUnit:InAir(true))) + + if _event.IniUnit:InAir(true) then + self.takenOff[_event.IniPlayerName] = true + end + local _unit = _event.IniUnit local _group = _event.IniGroup if _unit:IsHelicopter() or _group:IsHelicopter() then self:_AddMedevacMenuItem() end - return true + return self elseif (_event.id == EVENTS.PilotDead and self.csarOncrash == false) then -- Pilot dead @@ -144798,29 +172136,29 @@ function CSAR:_EventHandler(EventData) local _group = _event.IniGroup if _unit == nil then - return -- error! + return self -- error! end local _coalition = _event.IniCoalition if _coalition ~= self.coalition then - return --ignore! + return self --ignore! end -- Catch multiple events here? if self.takenOff[_event.IniUnitName] == true or _group:IsAirborne() then if self:_DoubleEjection(_unitname) then - return + return self end else self:T(self.lid .. " Pilot has not taken off, ignore") end - return + return self elseif _event.id == EVENTS.PilotDead or _event.id == EVENTS.Ejection then if _event.id == EVENTS.PilotDead and self.csarOncrash == false then - return + return self end self:T(self.lid .. " Event unit - Pilot Ejected") @@ -144828,38 +172166,71 @@ function CSAR:_EventHandler(EventData) local _unitname = _event.IniUnitName local _group = _event.IniGroup + self:T({_unit.UnitName, _unitname, _group.GroupName}) + if _unit == nil then - return -- error! + self:T("Unit NIL!") + return self -- error! end - - local _coalition = _unit:GetCoalition() + + --local _coalition = _unit:GetCoalition() -- nil now for some reason + local _coalition = _group:GetCoalition() if _coalition ~= self.coalition then - return --ignore! + self:T("Wrong coalition! Coalition = "..UTILS.GetCoalitionName(_coalition)) + return self --ignore! end - - if self.enableForAI == false and _event.IniPlayerName == nil then - return - end - + + + self:T("Airborne: "..tostring(_group:IsAirborne())) + self:T("Taken Off: "..tostring(self.takenOff[_event.IniUnitName])) + if not self.takenOff[_event.IniUnitName] and not _group:IsAirborne() then self:T(self.lid .. " Pilot has not taken off, ignore") - return -- give up, pilot hasnt taken off + -- return self -- give up, pilot hasnt taken off end if self:_DoubleEjection(_unitname) then - return + self:T("Double Ejection!") + return self end -- limit no of pilots in the field. if self.limitmaxdownedpilots and self:_ReachedPilotLimit() then - return + self:T("Maxed Downed Pilot!") + return self end - - -- all checks passed, get going. + + + -- TODO: Over water check --- EVENTS.LandingAfterEjection NOT triggered by DCS, so handle csarUsePara = true case + -- might create dual pilots in edge cases + + local wetfeet = false + + local initdcscoord = nil + local initcoord = nil + if _event.id == EVENTS.Ejection then + initdcscoord = _event.TgtDCSUnit:getPoint() + initcoord = COORDINATE:NewFromVec3(initdcscoord) + self:T({initdcscoord}) + else + initdcscoord = _event.IniDCSUnit:getPoint() + initcoord = COORDINATE:NewFromVec3(initdcscoord) + self:T({initdcscoord}) + end + + --local surface = _unit:GetCoordinate():GetSurfaceType() + local surface = initcoord:GetSurfaceType() + + if surface == land.SurfaceType.WATER then + self:T("Wet feet!") + wetfeet = true + end + -- all checks passed, get going. + if self.csarUsePara == false or (self.csarUsePara and wetfeet ) then --shagrat check parameter LandingAfterEjection, if true don't spawn a Pilot from EJECTION event, wait for the Chute to land local _freq = self:_GenerateADFFrequency() - self:_AddCsar(_coalition, _unit:GetCountry(), _unit:GetCoordinate() , _unit:GetTypeName(), _unit:GetName(), _event.IniPlayerName, _freq, false, "none") - - return true + self:_AddCsar(_coalition, _unit:GetCountry(), initcoord , _unit:GetTypeName(), _unit:GetName(), _event.IniPlayerName, _freq, false, "none") + return self + end elseif _event.id == EVENTS.Land then self:T(self.lid .. " Landing") @@ -144874,38 +172245,60 @@ function CSAR:_EventHandler(EventData) if _unit == nil then self:T(self.lid .. " Unit nil on landing") - return -- error! + return self -- error! end - local _coalition = _event.IniCoalition + --local _coalition = _event.IniCoalition + local _coalition = _event.IniGroup:GetCoalition() if _coalition ~= self.coalition then - return --ignore! + self:T(self.lid .. " Wrong coalition") + return self --ignore! end self.takenOff[_event.IniUnitName] = nil local _place = _event.Place -- Wrapper.Airbase#AIRBASE - + if _place == nil then self:T(self.lid .. " Landing Place Nil") - return -- error! + return self -- error! end -- anyone on board? if self.inTransitGroups[_event.IniUnitName] == nil then -- ignore - return + return self end if _place:GetCoalition() == self.coalition or _place:GetCoalition() == coalition.side.NEUTRAL then + self:__Landed(2,_event.IniUnitName, _place) self:_ScheduledSARFlight(_event.IniUnitName,_event.IniGroupName,true) else self:T(string.format("Airfield %d, Unit %d", _place:GetCoalition(), _unit:GetCoalition())) end end - return true + return self end + + ---- shagrat on event LANDING_AFTER_EJECTION spawn pilot at parachute location + if (_event.id == EVENTS.LandingAfterEjection and self.csarUsePara == true) then + self:T("LANDING_AFTER_EJECTION") + local _LandingPos = COORDINATE:NewFromVec3(_event.initiator:getPosition().p) + local _unitname = "Aircraft" --_event.initiator:getName() or "Aircraft" --shagrat Optional use of Object name which is unfortunately 'f15_Pilot_Parachute' + local _typename = "Ejected Pilot" --_event.Initiator.getTypeName() or "Ejected Pilot" + local _country = _event.initiator:getCountry() + local _coalition = coalition.getCountryCoalition( _country ) + self:T("Country = ".._country.." Coalition = ".._coalition) + if _coalition == self.coalition then + local _freq = self:_GenerateADFFrequency() + self:I({coalition=_coalition,country= _country, coord=_LandingPos, name=_unitname, player=_event.IniPlayerName, freq=_freq}) + self:_AddCsar(_coalition, _country, _LandingPos, nil, _unitname, _event.IniPlayerName, _freq, false, "none")--shagrat add CSAR at Parachute location. + + Unit.destroy(_event.initiator) -- shagrat remove static Pilot model + end + end + return self end @@ -144924,8 +172317,13 @@ function CSAR:_InitSARForPilot(_downedGroup, _GroupName, _freq, _nomessage) local _leadername = _leader:GetName() if not _nomessage then - local _text = string.format("%s requests SAR at %s, beacon at %.2f KHz", _leadername, _coordinatesText, _freqk) + if _freq ~= 0 then --shagrat + local _text = string.format("%s requests SAR at %s, beacon at %.2f KHz", _groupName, _coordinatesText, _freqk)--shagrat _groupName to prevent 'f15_Pilot_Parachute' self:_DisplayToAllSAR(_text,self.coalition,self.messageTime) + else --shagrat CASEVAC msg + local _text = string.format("Pickup Zone at %s.", _coordinatesText ) + self:_DisplayToAllSAR(_text,self.coalition,self.messageTime) + end end for _,_heliName in pairs(self.csarUnits) do @@ -145000,7 +172398,7 @@ function CSAR:_CheckWoundedGroupStatus(heliname,woundedgroupname) self.heliVisibleMessage[_lookupKeyHeli] = nil self.heliCloseMessage[_lookupKeyHeli] = nil self.landedStatus[_lookupKeyHeli] = nil - self:T("...helinunit nil!") + self:T("...heliunit nil!") return end @@ -145029,9 +172427,9 @@ function CSAR:_CheckWoundedGroupStatus(heliname,woundedgroupname) local dist = UTILS.MetersToNM(self.autosmokedistance) disttext = string.format("%.0fnm",dist) end - self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. I hear you! Damn, that thing is loud!\nI'll pop a smoke when you are %s away.\nLand or hover by the smoke.", _heliName, _pilotName, disttext), self.messageTime,false,true) + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. I hear you! Finally, that is music in my ears!\nI'll pop a smoke when you are %s away.\nLand or hover by the smoke.", _heliName, _pilotName, disttext), self.messageTime,false,true) else - self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. I hear you! Damn, that thing is loud!\nRequest a flare or smoke if you need.", _heliName, _pilotName), self.messageTime,false,true) + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. I hear you! Finally, that is music in my ears!\nRequest a flare or smoke if you need.", _heliName, _pilotName), self.messageTime,false,true) end --mark as shown for THIS heli and THIS group self.heliVisibleMessage[_lookupKeyHeli] = true @@ -145063,7 +172461,7 @@ function CSAR:_PopSmokeForGroup(_woundedGroupName, _woundedLeader) if _lastSmoke == nil or timer.getTime() > _lastSmoke then local _smokecolor = self.smokecolor - local _smokecoord = _woundedLeader:GetCoordinate() + local _smokecoord = _woundedLeader:GetCoordinate():Translate( 6, math.random( 1, 360) ) --shagrat place smoke at a random 6 m distance, so smoke does not obscure the pilot _smokecoord:Smoke(_smokecolor) self.smokeMarkers[_woundedGroupName] = timer.getTime() + 300 -- next smoke time end @@ -145095,8 +172493,8 @@ function CSAR:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupNam _maxUnits = self.max_units end if _unitsInHelicopter + 1 > _maxUnits then - self:_DisplayMessageToSAR(_heliUnit, string.format("%s, %s. We\'re already crammed with %d guys! Sorry!", _pilotName, _heliName, _unitsInHelicopter, _unitsInHelicopter), self.messageTime) - return true + self:_DisplayMessageToSAR(_heliUnit, string.format("%s, %s. We\'re already crammed with %d guys! Sorry!", _pilotName, _heliName, _unitsInHelicopter, _unitsInHelicopter), self.messageTime,false,false,true) + return self end local found,downedgrouptable = self:_CheckNameInDownedPilots(_woundedGroupName) @@ -145115,9 +172513,9 @@ function CSAR:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupNam self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s I\'m in! Get to the MASH ASAP! ", _heliName, _pilotName), self.messageTime,true,true) - self:__Boarded(5,_heliName,_woundedGroupName) + self:__Boarded(5,_heliName,_woundedGroupName,grouptable.desc) - return true + return self end --- (Internal) Move group to destination. @@ -145183,32 +172581,34 @@ function CSAR:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedG local _time = self.landedStatus[_lookupKeyHeli] if _time == nil then self.landedStatus[_lookupKeyHeli] = math.floor( (_distance - self.loadDistance) / 3.6 ) - _time = self.landedStatus[_lookupKeyHeli] + _time = self.landedStatus[_lookupKeyHeli] + _woundedGroup:OptionAlarmStateGreen() self:_OrderGroupToMoveToPoint(_woundedGroup, _heliUnit:GetCoordinate()) self:_DisplayMessageToSAR(_heliUnit, "Wait till " .. _pilotName .. " gets in. \nETA " .. _time .. " more seconds.", self.messageTime, false) else _time = self.landedStatus[_lookupKeyHeli] - 10 self.landedStatus[_lookupKeyHeli] = _time end - if _time <= 0 or _distance < self.loadDistance then - if self.pilotmustopendoors and not self:_IsLoadingDoorOpen(_heliName) then - self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true) - return true + --if _time <= 0 or _distance < self.loadDistance then + if _distance < self.loadDistance + 5 or _distance <= 13 then + if self.pilotmustopendoors and (self:_IsLoadingDoorOpen(_heliName) == false) then + self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true, true) + return false else self.landedStatus[_lookupKeyHeli] = nil self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) - return false + return true end end end else if (_distance < self.loadDistance) then - if self.pilotmustopendoors and not self:_IsLoadingDoorOpen(_heliName) then - self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true) - return true + if self.pilotmustopendoors and (self:_IsLoadingDoorOpen(_heliName) == false) then + self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true, true) + return false else self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) - return false + return true end end end @@ -145245,18 +172645,19 @@ function CSAR:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedG if _time > 0 then self:_DisplayMessageToSAR(_heliUnit, "Hovering above " .. _pilotName .. ". \n\nHold hover for " .. _time .. " seconds to winch them up. \n\nIf the countdown stops you\'re too far away!", self.messageTime, true) else - if self.pilotmustopendoors and not self:_IsLoadingDoorOpen(_heliName) then - self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true) - return true + if self.pilotmustopendoors and (self:_IsLoadingDoorOpen(_heliName) == false) then + self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true, true) + return false else self.hoverStatus[_lookupKeyHeli] = nil self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) - return false + return true end end _reset = false else self:_DisplayMessageToSAR(_heliUnit, "Too high to winch " .. _pilotName .. " \nReduce height and hover for 10 seconds!", self.messageTime, true,true) + return false end end @@ -145305,7 +172706,7 @@ function CSAR:_ScheduledSARFlight(heliname,groupname, isairport) if ( _dist < self.FARPRescueDistance or isairport ) and _heliUnit:InAir() == false then if self.pilotmustopendoors and self:_IsLoadingDoorOpen(heliname) == false then - self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me out!", self.messageTime, true) + self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me out!", self.messageTime, true, true) else self:_RescuePilots(_heliUnit) return @@ -145363,12 +172764,13 @@ end -- @param #number _time Message show duration. -- @param #boolean _clear (optional) Clear screen. -- @param #boolean _speak (optional) Speak message via SRS. -function CSAR:_DisplayMessageToSAR(_unit, _text, _time, _clear, _speak) +-- @param #boolean _override (optional) Override message suppression +function CSAR:_DisplayMessageToSAR(_unit, _text, _time, _clear, _speak, _override) self:T(self.lid .. " _DisplayMessageToSAR") local group = _unit:GetGroup() local _clear = _clear or nil local _time = _time or self.messageTime - if not self.suppressmessages then + if _override or not self.suppressmessages then local m = MESSAGE:New(_text,_time,"Info",_clear):ToGroup(group) end -- integrate SRS @@ -145378,6 +172780,15 @@ function CSAR:_DisplayMessageToSAR(_unit, _text, _time, _clear, _speak) local modulation = self.SRSModulation local channel = self.SRSchannel local msrs = MSRS:New(path,channel,modulation) + msrs:SetPort(self.SRSport) + msrs:SetLabel("CSAR") + msrs:SetCulture(self.SRSCulture) + msrs:SetCoalition(self.coalition) + msrs:SetVoice(self.SRSVoice) + if self.SRSGPathToCredentials then + msrs:SetGoogle(self.SRSGPathToCredentials) + end + msrs:SetVolume(self.SRSVolume) msrs:PlaySoundText(srstext, 2) end return self @@ -145438,7 +172849,11 @@ function CSAR:_DisplayActiveSAR(_unitName) else distancetext = string.format("%.1fkm", _distance/1000.0) end - table.insert(_csarList, { dist = _distance, msg = string.format("%s at %s - %.2f KHz ADF - %s ", _value.desc, _coordinatesText, _value.frequency / 1000, distancetext) }) + if _value.frequency == 0 then--shagrat insert CASEVAC without Frequency + table.insert(_csarList, { dist = _distance, msg = string.format("%s at %s - %s ", _value.desc, _coordinatesText, distancetext) }) + else + table.insert(_csarList, { dist = _distance, msg = string.format("%s at %s - %.2f KHz ADF - %s ", _value.desc, _coordinatesText, _value.frequency / 1000, distancetext) }) + end end end @@ -145452,7 +172867,7 @@ function CSAR:_DisplayActiveSAR(_unitName) _msg = _msg .. "\n" .. _line.msg end - self:_DisplayMessageToSAR(_heli, _msg, self.messageTime*2) + self:_DisplayMessageToSAR(_heli, _msg, self.messageTime*2, false, false, true) return self end @@ -145510,7 +172925,7 @@ function CSAR:_SignalFlare(_unitName) local _closest = self:_GetClosestDownedPilot(_heli) local smokedist = 8000 if self.approachdist_far > smokedist then smokedist = self.approachdist_far end - if _closest ~= nil and _closest.pilot ~= nil and _closest.distance < smokedist then + if _closest ~= nil and _closest.pilot ~= nil and _closest.distance > 0 and _closest.distance < smokedist then local _clockDir = self:_GetClockDirection(_heli, _closest.pilot) local _distance = 0 @@ -145520,7 +172935,7 @@ function CSAR:_SignalFlare(_unitName) _distance = string.format("%.1fkm",_closest.distance) end local _msg = string.format("%s - Popping signal flare at your %s o\'clock. Distance %s", _unitName, _clockDir, _distance) - self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true) + self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true, true) local _coord = _closest.pilot:GetCoordinate() _coord:FlareRed(_clockDir) @@ -145531,7 +172946,7 @@ function CSAR:_SignalFlare(_unitName) else _distance = string.format("%.1fkm",smokedist/1000) end - self:_DisplayMessageToSAR(_heli, string.format("No Pilots within %s",_distance), self.messageTime) + self:_DisplayMessageToSAR(_heli, string.format("No Pilots within %s",_distance), self.messageTime, false, false, true) end return self end @@ -145565,16 +172980,16 @@ function CSAR:_Reqsmoke( _unitName ) local smokedist = 8000 if smokedist < self.approachdist_far then smokedist = self.approachdist_far end local _closest = self:_GetClosestDownedPilot(_heli) - if _closest ~= nil and _closest.pilot ~= nil and _closest.distance < smokedist then + if _closest ~= nil and _closest.pilot ~= nil and _closest.distance > 0 and _closest.distance < smokedist then local _clockDir = self:_GetClockDirection(_heli, _closest.pilot) local _distance = 0 if _SETTINGS:IsImperial() then _distance = string.format("%.1fnm",UTILS.MetersToNM(_closest.distance)) else - _distance = string.format("%.1fkm",_closest.distance) + _distance = string.format("%.1fkm",_closest.distance/1000) end - local _msg = string.format("%s - Popping signal smoke at your %s o\'clock. Distance %s", _unitName, _clockDir, _distance) - self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true) + local _msg = string.format("%s - Popping smoke at your %s o\'clock. Distance %s", _unitName, _clockDir, _distance) + self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true, true) local _coord = _closest.pilot:GetCoordinate() local color = self.smokecolor _coord:Smoke(color) @@ -145585,7 +173000,7 @@ function CSAR:_Reqsmoke( _unitName ) else _distance = string.format("%.1fkm",smokedist/1000) end - self:_DisplayMessageToSAR(_heli, string.format("No Pilots within %s",_distance), self.messageTime) + self:_DisplayMessageToSAR(_heli, string.format("No Pilots within %s",_distance), self.messageTime, false, false, true) end return self end @@ -145657,13 +173072,13 @@ function CSAR:_CheckOnboard(_unitName) --list onboard pilots local _inTransit = self.inTransitGroups[_unitName] if _inTransit == nil then - self:_DisplayMessageToSAR(_unit, "No Rescued Pilots onboard", self.messageTime) + self:_DisplayMessageToSAR(_unit, "No Rescued Pilots onboard", self.messageTime, false, false, true) else local _text = "Onboard - RTB to FARP/Airfield or MASH: " for _, _onboard in pairs(self.inTransitGroups[_unitName]) do _text = _text .. "\n" .. _onboard.desc end - self:_DisplayMessageToSAR(_unit, _text, self.messageTime*2) + self:_DisplayMessageToSAR(_unit, _text, self.messageTime*2, false, false, true) end return self end @@ -145874,11 +173289,12 @@ end -- @param #string To To state. function CSAR:onafterStart(From, Event, To) self:T({From, Event, To}) - self:I(self.lid .. "Started.") + self:I(self.lid .. "Started ("..self.version..")") -- event handler self:HandleEvent(EVENTS.Takeoff, self._EventHandler) self:HandleEvent(EVENTS.Land, self._EventHandler) self:HandleEvent(EVENTS.Ejection, self._EventHandler) + self:HandleEvent(EVENTS.LandingAfterEjection, self._EventHandler) --shagrat self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler) self:HandleEvent(EVENTS.PlayerEnterUnit, self._EventHandler) self:HandleEvent(EVENTS.PilotDead, self._EventHandler) @@ -145889,6 +173305,9 @@ function CSAR:onafterStart(From, Event, To) self.allheligroupset = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterCategoryHelicopter():FilterStart() end self.mash = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(self.mashprefix):FilterStart() -- currently only GROUP objects, maybe support STATICs also? + if self.wetfeettemplate then + self.usewetfeet = true + end self:__Status(-10) return self end @@ -145922,7 +173341,12 @@ function CSAR:onbeforeStatus(From, Event, To) self:T({From, Event, To}) -- housekeeping self:_AddMedevacMenuItem() - self:_RefreshRadioBeacons() + + if not self.BeaconTimer or (self.BeaconTimer and not self.BeaconTimer:IsRunning()) then + self.BeaconTimer = TIMER:New(self._RefreshRadioBeacons,self) + self.BeaconTimer:Start(2,self.beaconRefresher) + end + self:_CheckDownedPilotTable() for _,_sar in pairs (self.csarUnits) do local PilotTable = self.downedPilots @@ -145989,6 +173413,7 @@ function CSAR:onafterStop(From, Event, To) self:UnHandleEvent(EVENTS.Takeoff) self:UnHandleEvent(EVENTS.Land) self:UnHandleEvent(EVENTS.Ejection) + self:UnHandleEvent(EVENTS.LandingAfterEjection) -- shagrat self:UnHandleEvent(EVENTS.PlayerEnterUnit) self:UnHandleEvent(EVENTS.PlayerEnterAircraft) self:UnHandleEvent(EVENTS.PilotDead) @@ -146064,6 +173489,18 @@ function CSAR:onbeforePilotDown(From, Event, To, Group, Frequency, Leadername, C self:T({From, Event, To, Group, Frequency, Leadername, CoordinatesText}) return self end + +--- (Internal) Function called before Landed() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +-- @param #string HeliName Name of the #UNIT which has landed. +-- @param Wrapper.Airbase#AIRBASE Airbase Airbase where the heli landed. +function CSAR:onbeforeLanded(From, Event, To, HeliName, Airbase) + self:T({From, Event, To, HeliName, Airbase}) + return self +end -------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- End Ops.CSAR -------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -146091,9 +173528,10 @@ end -- @module Ops.CTLD -- @image OPS_CTLD.jpg --- Date: Oct 2021 +-- Date: Feb 2022 do + ------------------------------------------------------ --- **CTLD_ENGINEERING** class, extends Core.Base#BASE -- @type CTLD_ENGINEERING @@ -146106,6 +173544,9 @@ do -- @field Wrapper.Unit#UNIT HeliUnit -- @field #string State -- @extends Core.Base#BASE + +--- +-- @field #CTLD_ENGINEERING CTLD_ENGINEERING = { ClassName = "CTLD_ENGINEERING", lid = "", @@ -146368,9 +173809,14 @@ CTLD_ENGINEERING = { return -1 end end + +end + +do ------------------------------------------------------ --- **CTLD_CARGO** class, extends Core.Base#BASE -- @type CTLD_CARGO +-- @field #string ClassName Class name. -- @field #number ID ID of this cargo. -- @field #string Name Name for menu. -- @field #table Templates Table of #POSITIONABLE objects. @@ -146380,9 +173826,13 @@ CTLD_ENGINEERING = { -- @field #number CratesNeeded Crates needed to build. -- @field Wrapper.Positionable#POSITIONABLE Positionable Representation of cargo in the mission. -- @field #boolean HasBeenDropped True if dropped from heli. --- @field #number PerCrateMass Mass in kg --- @field #number Stock Number of builds available, -1 for unlimited +-- @field #number PerCrateMass Mass in kg. +-- @field #number Stock Number of builds available, -1 for unlimited. +-- @field #string Subcategory Sub-category name. -- @extends Core.Base#BASE + +--- +-- @field #CTLD_CARGO CTLD_CARGO = { ClassName = "CTLD_CARGO", ID = 0, @@ -146399,17 +173849,17 @@ CTLD_CARGO = { Mark = nil, } + --- --- Define cargo types. - -- @type CTLD_CARGO.Enum - -- @field #string Type Type of Cargo. + -- @field Enum CTLD_CARGO.Enum = { - ["VEHICLE"] = "Vehicle", -- #string vehicles - ["TROOPS"] = "Troops", -- #string troops - ["FOB"] = "FOB", -- #string FOB - ["CRATE"] = "Crate", -- #string crate - ["REPAIR"] = "Repair", -- #string repair - ["ENGINEERS"] = "Engineers", -- #string engineers - ["STATIC"] = "Static", -- #string engineers + VEHICLE = "Vehicle", -- #string vehicles + TROOPS = "Troops", -- #string troops + FOB = "FOB", -- #string FOB + CRATE = "Crate", -- #string crate + REPAIR = "Repair", -- #string repair + ENGINEERS = "Engineers", -- #string engineers + STATIC = "Static", -- #string engineers } --- Function to create new CTLD_CARGO object. @@ -146425,8 +173875,9 @@ CTLD_CARGO = { -- @param #boolean Dropped Cargo/Troops have been unloaded from a chopper. -- @param #number PerCrateMass Mass in kg -- @param #number Stock Number of builds available, nil for unlimited + -- @param #string Subcategory Name of subcategory, handy if using > 10 types to load. -- @return #CTLD_CARGO self - function CTLD_CARGO:New(ID, Name, Templates, Sorte, HasBeenMoved, LoadDirectly, CratesNeeded, Positionable, Dropped, PerCrateMass, Stock) + function CTLD_CARGO:New(ID, Name, Templates, Sorte, HasBeenMoved, LoadDirectly, CratesNeeded, Positionable, Dropped, PerCrateMass, Stock, Subcategory) -- Inherit everything from BASE class. local self=BASE:Inherit(self, BASE:New()) -- #CTLD self:T({ID, Name, Templates, Sorte, HasBeenMoved, LoadDirectly, CratesNeeded, Positionable, Dropped}) @@ -146442,6 +173893,7 @@ CTLD_CARGO = { self.PerCrateMass = PerCrateMass or 0 -- #number self.Stock = Stock or nil --#number self.Mark = nil + self.Subcategory = Subcategory or "Other" return self end @@ -146452,12 +173904,20 @@ CTLD_CARGO = { return self.ID end + --- Query Subcategory + -- @param #CTLD_CARGO self + -- @return #string SubCategory + function CTLD_CARGO:GetSubCat() + return self.Subcategory + end + --- Query Mass. -- @param #CTLD_CARGO self -- @return #number Mass in kg function CTLD_CARGO:GetMass() return self.PerCrateMass end + --- Query Name. -- @param #CTLD_CARGO self -- @return #string Name @@ -146637,11 +174097,12 @@ do -- * Additional events to tailor your mission. -- * ANY late activated group can serve as cargo, either as troops, crates, which have to be build on-location, or static like ammo chests. -- * Option to persist (save&load) your dropped troops, crates and vehicles. +-- * Weight checks on loaded cargo. -- -- ## 0. Prerequisites -- -- You need to load an .ogg soundfile for the pilot\'s beacons into the mission, e.g. "beacon.ogg", use a once trigger, "sound to country" for that. --- Create the late-activated troops, vehicles (no statics at this point!) that will make up your deployable forces. +-- Create the late-activated troops, vehicles, that will make up your deployable forces. -- -- ## 1. Basic Setup -- @@ -146675,7 +174136,7 @@ do -- -- add vehicle called "Humvee" using template "Humvee", of type VEHICLE, size 2, i.e. needs two crates to be build -- -- vehicles and FOB will be spawned as crates in a LOAD zone first. Once transported to DROP zones, they can be build into the objects -- my_ctld:AddCratesCargo("Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,2) --- -- if you want to add weight to your Heli, crates can have a weight in kg **per crate**. Currently no max weight checked. Fly carefully. +-- -- if you want to add weight to your Heli, crates can have a weight in kg **per crate**. Fly carefully. -- my_ctld:AddCratesCargo("Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,2,2775) -- -- if you want to limit your stock, add a number (here: 10) as parameter after weight. No parameter / nil means unlimited stock. -- my_ctld:AddCratesCargo("Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,2,2775,10) @@ -146683,7 +174144,8 @@ do -- -- add infantry unit called "Forward Ops Base" using template "FOB", of type FOB, size 4, i.e. needs four crates to be build: -- my_ctld:AddCratesCargo("Forward Ops Base",{"FOB"},CTLD_CARGO.Enum.FOB,4) -- --- -- add crates to repair FOB or VEHICLE type units - the 2nd parameter needs to match the template you want to repair +-- -- add crates to repair FOB or VEHICLE type units - the 2nd parameter needs to match the template you want to repair, +-- -- e.g. the "Humvee" here refers back to the "Humvee" crates cargo added above (same template!) -- my_ctld:AddCratesRepair("Humvee Repair","Humvee",CTLD_CARGO.Enum.REPAIR,1) -- my_ctld.repairtime = 300 -- takes 300 seconds to repair something -- @@ -146693,9 +174155,9 @@ do -- -- ## 1.3 Add logistics zones -- --- Add zones for loading troops and crates and dropping, building crates +-- Add (normal, round!) zones for loading troops and crates and dropping, building crates -- --- -- Add a zone of type LOAD to our setup. Players can load troops and crates. +-- -- Add a zone of type LOAD to our setup. Players can load any troops and crates here as defined in 1.2 above. -- -- "Loadzone" is the name of the zone from the ME. Players can load, if they are inside the zone. -- -- Smoke and Flare color for this zone is blue, it is active (can be used) and has a radio beacon. -- my_ctld:AddCTLDZone("Loadzone",CTLD.CargoZoneType.LOAD,SMOKECOLOR.Blue,true,true) @@ -146715,7 +174177,7 @@ do -- -- "Tarawa" is the unitname (callsign) of the ship from the ME. Players can load, if they are inside the zone. -- -- The ship is 240 meters long and 20 meters wide. -- -- Note that you need to adjust the max hover height to deck height plus 5 meters or so for loading to work. --- -- When the ship is moving, forcing hoverload might not be a good idea. +-- -- When the ship is moving, avoid forcing hoverload. -- my_ctld:AddCTLDZone("Tarawa",CTLD.CargoZoneType.SHIP,SMOKECOLOR.Blue,true,true,240,20) -- -- ## 2. Options @@ -146738,7 +174200,12 @@ do -- my_ctld.cratecountry = country.id.GERMANY -- ID of crates. Will default to country.id.RUSSIA for RED coalition setups. -- my_ctld.allowcratepickupagain = true -- allow re-pickup crates that were dropped. -- my_ctld.enableslingload = false -- allow cargos to be slingloaded - might not work for all cargo types --- my_ctld.pilotmustopendoors = false -- -- force opening of doors +-- my_ctld.pilotmustopendoors = false -- force opening of doors +-- my_ctld.SmokeColor = SMOKECOLOR.Red -- default color to use when dropping smoke from heli +-- my_ctld.FlareColor = FLARECOLOR.Red -- color to use when flaring from heli +-- my_ctld.basetype = "container_cargo" -- default shape of the cargo container +-- my_ctld.droppedbeacontimeout = 600 -- dropped beacon lasts 10 minutes +-- my_ctld.usesubcats = false -- use sub-category names for crates, adds an extra menu layer in "Get Crates", useful if you have > 10 crate types. -- -- ## 2.1 User functions -- @@ -146747,22 +174214,22 @@ do -- Use this function to adjust what a heli type can or cannot do: -- -- -- E.g. update unit capabilities for testing. Please stay realistic in your mission design. --- -- Make a Gazelle into a heavy truck, this type can load both crates and troops and eight of each type: --- my_ctld:UnitCapabilities("SA342L", true, true, 8, 8, 12) +-- -- Make a Gazelle into a heavy truck, this type can load both crates and troops and eight of each type, up to 4000 kgs: +-- my_ctld:UnitCapabilities("SA342L", true, true, 8, 8, 12, 4000) -- --- -- Default unit type capabilities are: --- --- ["SA342Mistral"] = {type="SA342Mistral", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 12}, --- ["SA342L"] = {type="SA342L", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 12}, --- ["SA342M"] = {type="SA342M", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 12}, --- ["SA342Minigun"] = {type="SA342Minigun", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 12}, --- ["UH-1H"] = {type="UH-1H", crates=true, troops=true, cratelimit = 1, trooplimit = 8, length = 15}, --- ["Mi-8MT"] = {type="Mi-8MTV2", crates=true, troops=true, cratelimit = 2, trooplimit = 12, length = 15}, --- ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0, length = 15}, --- ["Mi-24P"] = {type="Mi-24P", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18}, --- ["Mi-24V"] = {type="Mi-24V", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18}, --- ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64, length = 25}, --- +-- -- Default unit type capabilities are: +-- ["SA342Mistral"] = {type="SA342Mistral", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 12, cargoweightlimit = 400}, +-- ["SA342L"] = {type="SA342L", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 12, cargoweightlimit = 400}, +-- ["SA342M"] = {type="SA342M", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 12, cargoweightlimit = 400}, +-- ["SA342Minigun"] = {type="SA342Minigun", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 12, cargoweightlimit = 400}, +-- ["UH-1H"] = {type="UH-1H", crates=true, troops=true, cratelimit = 1, trooplimit = 8, length = 15, cargoweightlimit = 700}, +-- ["Mi-8MT"] = {type="Mi-8MT", crates=true, troops=true, cratelimit = 2, trooplimit = 12, length = 15, cargoweightlimit = 3000}, +-- ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0, length = 15, cargoweightlimit = 0}, +-- ["Mi-24P"] = {type="Mi-24P", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18, cargoweightlimit = 700}, +-- ["Mi-24V"] = {type="Mi-24V", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18, cargoweightlimit = 700}, +-- ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64, length = 25, cargoweightlimit = 19000}, +-- ["UH-60L"] = {type="UH-60L", crates=true, troops=true, cratelimit = 2, trooplimit = 20, length = 16, cargoweightlimit = 3500}, +-- ["AH-64D_BLK_II"] = {type="AH-64D_BLK_II", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 17, cargoweightlimit = 200}, -- -- ### 2.1.2 Activate and deactivate zones -- @@ -146881,7 +174348,7 @@ do -- -- ## 4.1 Manage Crates -- --- Use this entry to get, load, list nearby, drop, build and repair crates. Also @see options. +-- Use this entry to get, load, list nearby, drop, build and repair crates. Also see options. -- -- ## 4.2 Manage Troops -- @@ -146892,7 +174359,7 @@ do -- -- Lists what you have loaded. Shows load capabilities for number of crates and number of seats for troops. -- --- ## 4.4 Smoke & Flare zones nearby +-- ## 4.4 Smoke & Flare zones nearby or drop smoke, beacon or flare from Heli -- -- Does what it says. -- @@ -146910,8 +174377,13 @@ do -- -- ## 5. Support for Hercules mod by Anubis -- --- Basic support for the Hercules mod By Anubis has been build into CTLD. Currently this does **not** cover objects and troops which can --- be loaded from the Rearm/Refuel menu, i.e. you can drop them into the field, but you cannot use them in functions scripted with this class. +-- Basic support for the Hercules mod By Anubis has been build into CTLD - that is you can load/drop/build the same way and for the same objects as +-- the helicopters (main method). +-- To cover objects and troops which can be loaded from the groud crew Rearm/Refuel menu (F8), you need to use @{#CTLD_HERCULES.New}() and link +-- this object to your CTLD setup (alternative method). In this case, do **not** use the `Hercules_Cargo.lua` or `Hercules_Cargo_CTLD.lua` which are part of the mod +-- in your mission! +-- +-- ### 5.1 Create an own CTLD instance and allow the usage of the Hercules mod (main method) -- -- local my_ctld = CTLD:New(coalition.side.BLUE,{"Helicargo", "Hercules"},"Lufttransportbrigade I") -- @@ -146922,10 +174394,45 @@ do -- my_ctld.HercMaxAngels = 2000 -- for troop/cargo drop via chute in meters, ca 6000 ft -- my_ctld.HercMaxSpeed = 77 -- 77mps or 270kph or 150kn -- +-- Hint: you can **only** airdrop from the Hercules if you are "in parameters", i.e. at or below `HercMaxSpeed` and in the AGL bracket between +-- `HercMinAngels` and `HercMaxAngels`! +-- -- Also, the following options need to be set to `true`: -- -- my_ctld.useprefix = true -- this is true by default and MUST BE ON. -- +-- ### 5.2 Integrate Hercules ground crew (F8 Menu) loadable objects (alternative method) +-- +-- Integrate to your CTLD instance like so, where `my_ctld` is a previously created CTLD instance: +-- +-- my_ctld.enableHercules = false -- avoid dual loading via CTLD F10 and F8 ground crew +-- local herccargo = CTLD_HERCULES:New("blue", "Hercules Test", my_ctld) +-- +-- You also need: +-- +-- * A template called "Infantry" for 10 Paratroopers (as set via herccargo.infantrytemplate). +-- * Depending on what you are loading with the help of the ground crew, there are 42 more templates for the various vehicles that are loadable. +-- +-- There's a **quick check output in the `dcs.log`** which tells you what's there and what not. +-- E.g.: +-- +-- ...Checking template for APC BTR-82A Air [24998lb] (BTR-82A) ... MISSING) +-- ...Checking template for ART 2S9 NONA Skid [19030lb] (SAU 2-C9) ... MISSING) +-- ...Checking template for EWR SBORKA Air [21624lb] (Dog Ear radar) ... MISSING) +-- ...Checking template for Transport Tigr Air [15900lb] (Tigr_233036) ... OK) +-- +-- Expected template names are the ones in the rounded brackets. +-- +-- ### 5.2.1 Hints +-- +-- The script works on the EVENTS.Shot trigger, which is used by the mod when you **drop cargo from the Hercules while flying**. Unloading on the ground does +-- not achieve anything here. If you just want to unload on the ground, use the normal Moose CTLD (see 5.1). +-- +-- There are two ways of airdropping: +-- +-- 1) Very low and very slow (>5m and <10m AGL) - here you can drop stuff which has "Skid" at the end of the cargo name (loaded via F8 Ground Crew menu) +-- 2) Higher up and slow (>100m AGL) - here you can drop paratroopers and cargo which has "Air" at the end of the cargo name (loaded via F8 Ground Crew menu) +-- -- Standard transport capabilities as per the real Hercules are: -- -- ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64}, -- 19t cargo, 64 paratroopers @@ -146954,8 +174461,8 @@ do -- @field #CTLD CTLD = { ClassName = "CTLD", - verbose = 0, - lid = "", + verbose = 0, + lid = "", coalition = 1, coalitiontxt = "blue", PilotGroups = {}, -- #GROUP_SET of heli pilots @@ -146964,7 +174471,6 @@ CTLD = { FreeUHFFrequencies = {}, -- Table of UHF FreeFMFrequencies = {}, -- Table of FM CargoCounter = 0, - wpZones = {}, Cargo_Troops = {}, -- generic troops objects Cargo_Crates = {}, -- generic crate objects Loaded_Cargo = {}, -- cargo aboard units @@ -146989,7 +174495,7 @@ CTLD = { -- DONE: Added support for Hercules -- TODO: Possibly - either/or loading crates and troops -- DONE: Make inject respect existing cargo types --- TODO: Drop beacons or flares/smoke +-- DONE: Drop beacons or flares/smoke -- DONE: Add statics as cargo -- DONE: List cargo in stock -- DONE: Limit of troops, crates buildable? @@ -147014,6 +174520,7 @@ CTLD = { -- @field #table vhfbeacon Beacon info as #CTLD.ZoneBeacon -- @field #number shiplength For ships - length of ship -- @field #number shipwidth For ships - width of ship +-- @field #number timestamp For dropped beacons - time this was created --- Zone Type Info. -- @type CTLD.CargoZoneType @@ -147022,6 +174529,7 @@ CTLD.CargoZoneType = { DROP = "drop", MOVE = "move", SHIP = "ship", + BEACON = "beacon", } --- Buildable table info. @@ -147040,24 +174548,27 @@ CTLD.CargoZoneType = { -- @field #boolean troops Can transport troops. -- @field #number cratelimit Number of crates transportable. -- @field #number trooplimit Number of troop units transportable. +-- @field #number cargoweightlimit Max loadable kgs of cargo. CTLD.UnitTypes = { - ["SA342Mistral"] = {type="SA342Mistral", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 12}, - ["SA342L"] = {type="SA342L", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 12}, - ["SA342M"] = {type="SA342M", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 12}, - ["SA342Minigun"] = {type="SA342Minigun", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 12}, - ["UH-1H"] = {type="UH-1H", crates=true, troops=true, cratelimit = 1, trooplimit = 8, length = 15}, - ["Mi-8MTV2"] = {type="Mi-8MTV2", crates=true, troops=true, cratelimit = 2, trooplimit = 12, length = 15}, - ["Mi-8MT"] = {type="Mi-8MTV2", crates=true, troops=true, cratelimit = 2, trooplimit = 12, length = 15}, - ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0, length = 15}, - ["Mi-24P"] = {type="Mi-24P", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18}, - ["Mi-24V"] = {type="Mi-24V", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18}, - ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64, length = 25}, -- 19t cargo, 64 paratroopers. + ["SA342Mistral"] = {type="SA342Mistral", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 12, cargoweightlimit = 400}, + ["SA342L"] = {type="SA342L", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 12, cargoweightlimit = 400}, + ["SA342M"] = {type="SA342M", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 12, cargoweightlimit = 400}, + ["SA342Minigun"] = {type="SA342Minigun", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 12, cargoweightlimit = 400}, + ["UH-1H"] = {type="UH-1H", crates=true, troops=true, cratelimit = 1, trooplimit = 8, length = 15, cargoweightlimit = 700}, + ["Mi-8MTV2"] = {type="Mi-8MTV2", crates=true, troops=true, cratelimit = 2, trooplimit = 12, length = 15, cargoweightlimit = 3000}, + ["Mi-8MT"] = {type="Mi-8MTV2", crates=true, troops=true, cratelimit = 2, trooplimit = 12, length = 15, cargoweightlimit = 3000}, + ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0, length = 15, cargoweightlimit = 0}, + ["Mi-24P"] = {type="Mi-24P", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18, cargoweightlimit = 700}, + ["Mi-24V"] = {type="Mi-24V", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18, cargoweightlimit = 700}, + ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64, length = 25, cargoweightlimit = 19000}, -- 19t cargo, 64 paratroopers. --Actually it's longer, but the center coord is off-center of the model. + ["UH-60L"] = {type="UH-60L", crates=true, troops=true, cratelimit = 2, trooplimit = 20, length = 16, cargoweightlimit = 3500}, -- 4t cargo, 20 (unsec) seats + ["AH-64D_BLK_II"] = {type="AH-64D_BLK_II", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 17, cargoweightlimit = 200}, -- 2 ppl **outside** the helo } --- CTLD class version. -- @field #string version -CTLD.version="0.2.4" +CTLD.version="1.0.10" --- Instantiate a new CTLD. -- @param #CTLD self @@ -147146,6 +174657,9 @@ function CTLD:New(Coalition, Prefixes, Alias) self.dropOffZones = {} self.wpZones = {} self.shipZones = {} + self.droppedBeacons = {} + self.droppedbeaconref = {} + self.droppedbeacontimeout = 600 -- Cargo self.Cargo_Crates = {} @@ -147182,6 +174696,7 @@ function CTLD:New(Coalition, Prefixes, Alias) self.smokedistance = 2000 self.movetroopstowpzone = true self.movetroopsdistance = 5000 + self.troopdropzoneradius = 100 -- added support Hercules Mod self.enableHercules = false @@ -147214,6 +174729,10 @@ function CTLD:New(Coalition, Prefixes, Alias) self.saveinterval = 600 self.eventoninject = true + -- sub categories + self.usesubcats = false + self.subcats = {} + local AliaS = string.gsub(self.alias," ","_") self.filename = string.format("CTLD_%s_Persist.csv",AliaS) @@ -147222,6 +174741,11 @@ function CTLD:New(Coalition, Prefixes, Alias) -- slingload self.enableslingload = false + self.basetype = "container_cargo" -- shape of the container + + -- Smokes and Flares + self.SmokeColor = SMOKECOLOR.Red + self.FlareColor = FLARECOLOR.Red for i=1,100 do math.random() @@ -147408,6 +174932,7 @@ function CTLD:_GetUnitCapabilities(Unit) capabilities.trooplimit = 0 capabilities.type = "generic" capabilities.length = 20 + capabilities.cargoweightlimit = 0 end return capabilities end @@ -147441,6 +174966,17 @@ function CTLD:_GenerateVHFrequencies() return self end +--- (User) Set drop zone radius for troop drops in meters. Minimum distance is 25m for security reasons. +-- @param #CTLD self +-- @param #number Radius The radius to use. +function CTLD:SetTroopDropZoneRadius(Radius) + self:T(self.lid .. " SetTroopDropZoneRadius") + local tradius = Radius or 100 + if tradius < 25 then tradius = 25 end + self.troopdropzoneradius = tradius + return self +end + --- (Internal) Event handler function -- @param #CTLD self -- @param Core.Event#EVENTDATA EventData @@ -147463,6 +174999,7 @@ function CTLD:_EventHandler(EventData) -- Herc support --self:T_unit:GetTypeName()) if _unit:GetTypeName() == "Hercules" and self.enableHercules then + local unitname = event.IniUnitName or "none" self.Loaded_Cargo[unitname] = nil self:_RefreshF10Menus() end @@ -147832,6 +175369,8 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop) local drop = drop or false local ship = nil local width = 20 + local distance = nil + local zone = nil if not drop then inzone = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) if not inzone then @@ -147854,7 +175393,7 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop) local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitCapabilities local canloadcratesno = capabilities.cratelimit local loaddist = self.CrateDistance or 35 - local nearcrates, numbernearby = self:_FindCratesNearby(Group,Unit,loaddist) + local nearcrates, numbernearby = self:_FindCratesNearby(Group,Unit,loaddist,true) if numbernearby >= canloadcratesno and not drop then self:_SendMessage("There are enough crates nearby already! Take care of those first!", 10, false, Group) return self @@ -147919,7 +175458,7 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop) local cratecoord = position:Translate(cratedistance,rheading) local cratevec2 = cratecoord:GetVec2() self.CrateCounter = self.CrateCounter + 1 - local basetype = "container_cargo" + local basetype = self.basetype or "container_cargo" if isstatic then basetype = cratetemplate end @@ -147948,13 +175487,14 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop) end local templ = cargotype:GetTemplates() local sorte = cargotype:GetType() + local subcat = cargotype.Subcategory self.CargoCounter = self.CargoCounter + 1 local realcargo = nil if drop then - realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,true,false,cratesneeded,self.Spawned_Crates[self.CrateCounter],true,cargotype.PerCrateMass) + realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,true,false,cratesneeded,self.Spawned_Crates[self.CrateCounter],true,cargotype.PerCrateMass,subcat) table.insert(droppedcargo,realcargo) else - realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,false,false,cratesneeded,self.Spawned_Crates[self.CrateCounter],true,cargotype.PerCrateMass) + realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,false,false,cratesneeded,self.Spawned_Crates[self.CrateCounter],true,cargotype.PerCrateMass,subcat) Cargo:RemoveStock() end table.insert(self.Spawned_Cargo, realcargo) @@ -147997,7 +175537,7 @@ function CTLD:InjectStatics(Zone, Cargo, RandomCoord) cratetemplate = cargotype:GetTemplates() isstatic = true end - local basetype = "container_cargo" + local basetype = self.basetype or "container_cargo" if isstatic then basetype = cratetemplate end @@ -148036,7 +175576,7 @@ end function CTLD:_ListCratesNearby( _group, _unit) self:T(self.lid .. " _ListCratesNearby") local finddist = self.CrateDistance or 35 - local crates,number = self:_FindCratesNearby(_group,_unit, finddist) -- #table + local crates,number = self:_FindCratesNearby(_group,_unit, finddist,true) -- #table if number > 0 then local text = REPORT:New("Crates Found Nearby:") text:Add("------------------------------------------------------------") @@ -148093,9 +175633,10 @@ end -- @param Wrapper.Group#GROUP _group Group -- @param Wrapper.Unit#UNIT _unit Unit -- @param #number _dist Distance +-- @param #boolean _ignoreweight Find everything in range, ignore loadable weight -- @return #table Table of crates -- @return #number Number Number of crates found -function CTLD:_FindCratesNearby( _group, _unit, _dist) +function CTLD:_FindCratesNearby( _group, _unit, _dist, _ignoreweight) self:T(self.lid .. " _FindCratesNearby") local finddist = _dist local location = _group:GetCoordinate() @@ -148103,16 +175644,32 @@ function CTLD:_FindCratesNearby( _group, _unit, _dist) -- cycle local index = 0 local found = {} + local loadedmass = 0 + local unittype = "none" + local capabilities = {} + local maxmass = 2000 + local maxloadable = 2000 + if not _ignoreweight then + loadedmass = self:_GetUnitCargoMass(_unit) + unittype = _unit:GetTypeName() + capabilities = self:_GetUnitCapabilities(_unit) -- #CTLD.UnitCapabilities + maxmass = capabilities.cargoweightlimit or 2000 + maxloadable = maxmass - loadedmass + end + self:T(self.lid .. " Max loadable mass: " .. maxloadable) for _,_cargoobject in pairs (existingcrates) do local cargo = _cargoobject -- #CTLD_CARGO local static = cargo:GetPositionable() -- Wrapper.Static#STATIC -- crates local staticid = cargo:GetID() + local weight = cargo:GetMass() -- weight in kgs of this cargo + self:T(self.lid .. " Found cargo mass: " .. weight) if static and static:IsAlive() then local staticpos = static:GetCoordinate() local distance = self:_GetDistance(location,staticpos) - if distance <= finddist and static then + if distance <= finddist and static and (weight <= maxloadable or _ignoreweight) then index = index + 1 table.insert(found, staticid, cargo) + maxloadable = maxloadable - weight end end end @@ -148158,6 +175715,7 @@ function CTLD:_LoadCratesNearby(Group, Unit) if self.Loaded_Cargo[unitname] then loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo numberonboard = loaded.Cratesloaded or 0 + massonboard = self:_GetUnitCargoMass(Unit) else loaded = {} -- #CTLD.LoadedCargo loaded.Troopsloaded = 0 @@ -148166,11 +175724,12 @@ function CTLD:_LoadCratesNearby(Group, Unit) end -- get nearby crates local finddist = self.CrateDistance or 35 - local nearcrates,number = self:_FindCratesNearby(Group,Unit,finddist) -- #table + local nearcrates,number = self:_FindCratesNearby(Group,Unit,finddist,false) -- #table + self:T(self.lid .. " Crates found: " .. number) if number == 0 and self.hoverautoloading then return self -- exit elseif number == 0 then - self:_SendMessage("Sorry no loadable crates nearby!", 10, false, Group) + self:_SendMessage("Sorry no loadable crates nearby or max cargo weight reached!", 10, false, Group) return self -- exit elseif numberonboard == cratelimit then self:_SendMessage("Sorry no fully loaded!", 10, false, Group) @@ -148214,30 +175773,46 @@ function CTLD:_LoadCratesNearby(Group, Unit) self.Loaded_Cargo[unitname] = loaded self:_UpdateUnitCargoMass(Unit) -- clean up real world crates - local existingcrates = self.Spawned_Cargo -- #table - local newexcrates = {} - for _,_crate in pairs(existingcrates) do - local excrate = _crate -- #CTLD_CARGO - local ID = excrate:GetID() - for _,_ID in pairs(crateidsloaded) do - if ID ~= _ID then - table.insert(newexcrates,_crate) - end - end - end - self.Spawned_Cargo = nil - self.Spawned_Cargo = newexcrates + self:_CleanupTrackedCrates(crateidsloaded) end end return self end +--- (Internal) Function to clean up tracked cargo crates +function CTLD:_CleanupTrackedCrates(crateIdsToRemove) + local existingcrates = self.Spawned_Cargo -- #table + local newexcrates = {} + for _,_crate in pairs(existingcrates) do + local excrate = _crate -- #CTLD_CARGO + local ID = excrate:GetID() + local keep = true + for _,_ID in pairs(crateIdsToRemove) do + if ID == _ID then + keep = false + end + end + -- remove destroyed crates here too + local static = _crate:GetPositionable() -- Wrapper.Static#STATIC -- crates + if not static or not static:IsAlive() then + keep = false + end + if keep then + table.insert(newexcrates,_crate) + end + end + self.Spawned_Cargo = nil + self.Spawned_Cargo = newexcrates + return self +end + --- (Internal) Function to get current loaded mass -- @param #CTLD self -- @param Wrapper.Unit#UNIT Unit -- @return #number mass in kgs function CTLD:_GetUnitCargoMass(Unit) self:T(self.lid .. " _GetUnitCargoMass") + if not Unit then return 0 end local unitname = Unit:GetName() local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo local loadedmass = 0 -- #number @@ -148477,7 +176052,7 @@ function CTLD:_UnloadTroops(Group, Unit) local name = cargo:GetName() or "none" local temptable = cargo:GetTemplates() or {} local position = Group:GetCoordinate() - local zoneradius = 100 -- drop zone radius + local zoneradius = self.troopdropzoneradius or 100 -- drop zone radius local factor = 1 if IsHerc then factor = cargo:GetCratesNeeded() or 1 -- spread a bit more if airdropping @@ -148646,7 +176221,7 @@ function CTLD:_BuildCrates(Group, Unit,Engineering) end -- get nearby crates local finddist = self.CrateDistance or 35 - local crates,number = self:_FindCratesNearby(Group,Unit, finddist) -- #table + local crates,number = self:_FindCratesNearby(Group,Unit, finddist,true) -- #table local buildables = {} local foundbuilds = false local canbuild = false @@ -148660,6 +176235,8 @@ function CTLD:_BuildCrates(Group, Unit,Engineering) local required = Crate:GetCratesNeeded() local template = Crate:GetTemplates() local ctype = Crate:GetType() + local ccoord = Crate:GetPositionable():GetCoordinate() -- Core.Point#COORDINATE + --local testmarker = ccoord:MarkToAll("Crate found",true,"Build Position") if not buildables[name] then local object = {} -- #CTLD.Buildable object.Name = name @@ -148668,6 +176245,7 @@ function CTLD:_BuildCrates(Group, Unit,Engineering) object.Template = template object.CanBuild = false object.Type = ctype -- #CTLD_CARGO.Enum + object.Coord = ccoord:GetVec2() buildables[name] = object foundbuilds = true else @@ -148730,7 +176308,7 @@ function CTLD:_RepairCrates(Group, Unit, Engineering) self:T(self.lid .. " _RepairCrates") -- get nearby crates local finddist = self.CrateDistance or 35 - local crates,number = self:_FindCratesNearby(Group,Unit,finddist) -- #table + local crates,number = self:_FindCratesNearby(Group,Unit,finddist,true) -- #table local buildables = {} local foundbuilds = false local canbuild = false @@ -148829,7 +176407,8 @@ function CTLD:_BuildObjectFromCrates(Group,Unit,Build,Repair,RepairLocation) temptable = {temptable} end local zone = ZONE_GROUP:New(string.format("Unload zone-%s",unitname),Group,100) - local randomcoord = zone:GetRandomCoordinate(35):GetVec2() + --local randomcoord = zone:GetRandomCoordinate(35):GetVec2() + local randomcoord = Build.Coord or zone:GetRandomCoordinate(35):GetVec2() if Repair then randomcoord = RepairLocation:GetVec2() end @@ -148838,7 +176417,7 @@ function CTLD:_BuildObjectFromCrates(Group,Unit,Build,Repair,RepairLocation) local alias = string.format("%s-%d", _template, math.random(1,100000)) if canmove then self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) - :InitRandomizeUnits(true,20,2) + --:InitRandomizeUnits(true,20,2) :InitDelayOff() :SpawnFromVec2(randomcoord) else -- don't random position of e.g. SAM units build as FOB @@ -148918,19 +176497,7 @@ function CTLD:_CleanUpCrates(Crates,Build,Number) if found == numberdest then break end -- got enough end -- loop and remove from real world representation - for _,_crate in pairs(existingcrates) do - local excrate = _crate -- #CTLD_CARGO - local ID = excrate:GetID() - for _,_ID in pairs(destIDs) do - if ID ~= _ID then - table.insert(newexcrates,_crate) - end - end - end - - -- reset Spawned_Cargo - self.Spawned_Cargo = nil - self.Spawned_Cargo = newexcrates + self:_CleanupTrackedCrates(destIDs) return self end @@ -148956,6 +176523,16 @@ function CTLD:_RefreshF10Menus() end -- end for self.CtldUnits = _UnitList + -- subcats? + if self.usesubcats then + for _id,_cargo in pairs(self.Cargo_Crates) do + local entry = _cargo -- #CTLD_CARGO + if not self.subcats[entry.Subcategory] then + self.subcats[entry.Subcategory] = entry.Subcategory + end + end + end + -- build unit menus local menucount = 0 local menus = {} @@ -148977,8 +176554,17 @@ function CTLD:_RefreshF10Menus() local listmenu = MENU_GROUP_COMMAND:New(_group,"List boarded cargo",topmenu, self._ListCargo, self, _group, _unit) local invtry = MENU_GROUP_COMMAND:New(_group,"Inventory",topmenu, self._ListInventory, self, _group, _unit) local rbcns = MENU_GROUP_COMMAND:New(_group,"List active zone beacons",topmenu, self._ListRadioBeacons, self, _group, _unit) - local smokemenu = MENU_GROUP_COMMAND:New(_group,"Smoke zones nearby",topmenu, self.SmokeZoneNearBy, self, _unit, false) - local smokemenu = MENU_GROUP_COMMAND:New(_group,"Flare zones nearby",topmenu, self.SmokeZoneNearBy, self, _unit, true):Refresh() + local smoketopmenu = MENU_GROUP:New(_group,"Smokes, Flares, Beacons",topmenu) + local smokemenu = MENU_GROUP_COMMAND:New(_group,"Smoke zones nearby",smoketopmenu, self.SmokeZoneNearBy, self, _unit, false) + local smokeself = MENU_GROUP:New(_group,"Drop smoke now",smoketopmenu) + local smokeselfred = MENU_GROUP_COMMAND:New(_group,"Red smoke",smokeself, self.SmokePositionNow, self, _unit, false,SMOKECOLOR.Red) + local smokeselfblue = MENU_GROUP_COMMAND:New(_group,"Blue smoke",smokeself, self.SmokePositionNow, self, _unit, false,SMOKECOLOR.Blue) + local smokeselfgreen = MENU_GROUP_COMMAND:New(_group,"Green smoke",smokeself, self.SmokePositionNow, self, _unit, false,SMOKECOLOR.Green) + local smokeselforange = MENU_GROUP_COMMAND:New(_group,"Orange smoke",smokeself, self.SmokePositionNow, self, _unit, false,SMOKECOLOR.Orange) + local smokeselfwhite = MENU_GROUP_COMMAND:New(_group,"White smoke",smokeself, self.SmokePositionNow, self, _unit, false,SMOKECOLOR.White) + local flaremenu = MENU_GROUP_COMMAND:New(_group,"Flare zones nearby",smoketopmenu, self.SmokeZoneNearBy, self, _unit, true) + local flareself = MENU_GROUP_COMMAND:New(_group,"Fire flare now",smoketopmenu, self.SmokePositionNow, self, _unit, true) + local beaconself = MENU_GROUP_COMMAND:New(_group,"Drop beacon now",smoketopmenu, self.DropBeaconNow, self, _unit):Refresh() -- sub menus -- sub menu troops management if cantroops then @@ -148995,11 +176581,26 @@ function CTLD:_RefreshF10Menus() if cancrates then local loadmenu = MENU_GROUP_COMMAND:New(_group,"Load crates",topcrates, self._LoadCratesNearby, self, _group, _unit) local cratesmenu = MENU_GROUP:New(_group,"Get Crates",topcrates) - for _,_entry in pairs(self.Cargo_Crates) do - local entry = _entry -- #CTLD_CARGO - menucount = menucount + 1 - local menutext = string.format("Crate %s (%dkg)",entry.Name,entry.PerCrateMass or 0) - menus[menucount] = MENU_GROUP_COMMAND:New(_group,menutext,cratesmenu,self._GetCrates, self, _group, _unit, entry) + + if self.usesubcats then + local subcatmenus = {} + for _name,_entry in pairs(self.subcats) do + subcatmenus[_name] = MENU_GROUP:New(_group,_name,cratesmenu) + end + for _,_entry in pairs(self.Cargo_Crates) do + local entry = _entry -- #CTLD_CARGO + local subcat = entry.Subcategory + menucount = menucount + 1 + local menutext = string.format("Crate %s (%dkg)",entry.Name,entry.PerCrateMass or 0) + menus[menucount] = MENU_GROUP_COMMAND:New(_group,menutext,subcatmenus[subcat],self._GetCrates, self, _group, _unit, entry) + end + else + for _,_entry in pairs(self.Cargo_Crates) do + local entry = _entry -- #CTLD_CARGO + menucount = menucount + 1 + local menutext = string.format("Crate %s (%dkg)",entry.Name,entry.PerCrateMass or 0) + menus[menucount] = MENU_GROUP_COMMAND:New(_group,menutext,cratesmenu,self._GetCrates, self, _group, _unit, entry) + end end for _,_entry in pairs(self.Cargo_Statics) do local entry = _entry -- #CTLD_CARGO @@ -149027,6 +176628,25 @@ function CTLD:_RefreshF10Menus() return self end +--- [Internal] Function to check if a template exists in the mission. +-- @param #CTLD self +-- @param #table temptable Table of string names +-- @return #boolen outcome +function CTLD:_CheckTemplates(temptable) + self:T(self.lid .. " _CheckTemplates") + local outcome = true + if type(temptable) ~= "table" then + temptable = {temptable} + end + for _,_name in pairs(temptable) do + if not _DATABASE.Templates.Groups[_name] then + outcome = false + self:E(self.lid .. "ERROR: Template name " .. _name .. " is missing!") + end + end + return outcome +end + --- User function - Add *generic* troop type loadable as cargo. This type will load directly into the heli without crates. -- @param #CTLD self -- @param #string Name Unique name of this type of troop. E.g. "Anti-Air Small". @@ -149038,6 +176658,10 @@ function CTLD:_RefreshF10Menus() function CTLD:AddTroopsCargo(Name,Templates,Type,NoTroops,PerTroopMass,Stock) self:T(self.lid .. " AddTroopsCargo") self:T({Name,Templates,Type,NoTroops,PerTroopMass,Stock}) + if not self:_CheckTemplates(Templates) then + self:E(self.lid .. "Troops Cargo for " .. Name .. " has missing template(s)!" ) + return self + end self.CargoCounter = self.CargoCounter + 1 -- Troops are directly loadable local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,true,NoTroops,nil,nil,PerTroopMass,Stock) @@ -149053,11 +176677,16 @@ end -- @param #number NoCrates Number of crates needed to build this cargo. -- @param #number PerCrateMass Mass in kg of each crate -- @param #number Stock Number of groups in stock. Nil for unlimited. -function CTLD:AddCratesCargo(Name,Templates,Type,NoCrates,PerCrateMass,Stock) +-- @param #string SubCategory Name of sub-category (optional). +function CTLD:AddCratesCargo(Name,Templates,Type,NoCrates,PerCrateMass,Stock,SubCategory) self:T(self.lid .. " AddCratesCargo") + if not self:_CheckTemplates(Templates) then + self:E(self.lid .. "Crates Cargo for " .. Name .. " has missing template(s)!" ) + return self + end self.CargoCounter = self.CargoCounter + 1 -- Crates are not directly loadable - local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,false,NoCrates,nil,nil,PerCrateMass,Stock) + local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,false,NoCrates,nil,nil,PerCrateMass,Stock,SubCategory) table.insert(self.Cargo_Crates,cargo) return self end @@ -149097,16 +176726,21 @@ end --- User function - Add *generic* repair crates loadable as cargo. This type will create crates that need to be loaded, moved, dropped and built. -- @param #CTLD self -- @param #string Name Unique name of this type of cargo. E.g. "Humvee". --- @param #string Template Template of VEHICLE or FOB cargo that this can repair. +-- @param #string Template Template of VEHICLE or FOB cargo that this can repair. MUST be the same as given in `AddCratesCargo(..)`! -- @param #CTLD_CARGO.Enum Type Type of cargo, here REPAIR. -- @param #number NoCrates Number of crates needed to build this cargo. -- @param #number PerCrateMass Mass in kg of each crate -- @param #number Stock Number of groups in stock. Nil for unlimited. -function CTLD:AddCratesRepair(Name,Template,Type,NoCrates, PerCrateMass,Stock) +-- @param #string SubCategory Name of the sub-category (optional). +function CTLD:AddCratesRepair(Name,Template,Type,NoCrates, PerCrateMass,Stock,SubCategory) self:T(self.lid .. " AddCratesRepair") + if not self:_CheckTemplates(Template) then + self:E(self.lid .. "Repair Cargo for " .. Name .. " has a missing template!" ) + return self + end self.CargoCounter = self.CargoCounter + 1 -- Crates are not directly loadable - local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Template,Type,false,false,NoCrates,nil,nil,PerCrateMass,Stock) + local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Template,Type,false,false,NoCrates,nil,nil,PerCrateMass,Stock,SubCategory) table.insert(self.Cargo_Crates,cargo) return self end @@ -149122,7 +176756,9 @@ function CTLD:AddZone(Zone) elseif zone.type == CTLD.CargoZoneType.DROP then table.insert(self.dropOffZones,zone) elseif zone.type == CTLD.CargoZoneType.SHIP then - table.insert(self.shipZones,zone) + table.insert(self.shipZones,zone) + elseif zone.type == CTLD.CargoZoneType.BEACON then + table.insert(self.droppedBeacons,zone) else table.insert(self.wpZones,zone) end @@ -149135,7 +176771,7 @@ end -- @param #CTLD.CargoZoneType ZoneType Type of zone this belongs to. -- @param #boolean NewState (Optional) Set to true to activate, false to switch off. function CTLD:ActivateZone(Name,ZoneType,NewState) - self:T(self.lid .. " AddZone") + self:T(self.lid .. " ActivateZone") local newstate = true -- set optional in case we\'re deactivating if NewState ~= nil then @@ -149170,7 +176806,7 @@ end -- @param #string Name Name of the zone to change in the ME. -- @param #CTLD.CargoZoneType ZoneType Type of zone this belongs to. function CTLD:DeactivateZone(Name,ZoneType) - self:T(self.lid .. " AddZone") + self:T(self.lid .. " DeactivateZone") self:ActivateZone(Name,ZoneType,false) return self end @@ -149237,7 +176873,7 @@ function CTLD:_GetVHFBeacon(Name) end ---- User function - Crates and adds a #CTLD.CargoZone zone for this CTLD instance. +--- User function - Creates and adds a #CTLD.CargoZone zone for this CTLD instance. -- Zones of type LOAD: Players load crates and troops here. -- Zones of type DROP: Players can drop crates here. Note that troops can be unloaded anywhere. -- Zone of type MOVE: Dropped troops and vehicles will start moving to the nearest zone of this type (also see options). @@ -149279,6 +176915,91 @@ function CTLD:AddCTLDZone(Name, Type, Color, Active, HasBeacon, Shiplength, Ship return self end +--- User function - Creates and adds a #CTLD.CargoZone zone for this CTLD instance from an Airbase or FARP name. +-- Zones of type LOAD: Players load crates and troops here. +-- Zones of type DROP: Players can drop crates here. Note that troops can be unloaded anywhere. +-- Zone of type MOVE: Dropped troops and vehicles will start moving to the nearest zone of this type (also see options). +-- @param #CTLD self +-- @param #string AirbaseName Name of the Airbase, can be e.g. AIRBASE.Caucasus.Beslan or "Beslan". For FARPs, this will be the UNIT name. +-- @param #string Type Type of this zone, #CTLD.CargoZoneType +-- @param #number Color Smoke/Flare color e.g. #SMOKECOLOR.Red +-- @param #string Active Is this zone currently active? +-- @param #string HasBeacon Does this zone have a beacon if it is active? +-- @return #CTLD self +function CTLD:AddCTLDZoneFromAirbase(AirbaseName, Type, Color, Active, HasBeacon) + self:T(self.lid .. " AddCTLDZoneFromAirbase") + local AFB = AIRBASE:FindByName(AirbaseName) + local name = AFB:GetZone():GetName() + self:T(self.lid .. "AFB " .. AirbaseName .. " ZoneName " .. name) + self:AddCTLDZone(name, Type, Color, Active, HasBeacon) + return self +end + +--- (Internal) Function to create a dropped beacon +-- @param #CTLD self +-- @param Wrapper.Unit#UNIT Unit +-- @return #CTLD self +function CTLD:DropBeaconNow(Unit) + self:T(self.lid .. " DropBeaconNow") + + local ctldzone = {} -- #CTLD.CargoZone + ctldzone.active = true + ctldzone.color = math.random(0,4) -- random color + ctldzone.name = "Beacon " .. math.random(1,10000) + ctldzone.type = CTLD.CargoZoneType.BEACON -- #CTLD.CargoZoneType + ctldzone.hasbeacon = true + + ctldzone.fmbeacon = self:_GetFMBeacon(ctldzone.name) + ctldzone.uhfbeacon = self:_GetUHFBeacon(ctldzone.name) + ctldzone.vhfbeacon = self:_GetVHFBeacon(ctldzone.name) + ctldzone.timestamp = timer.getTime() + + self.droppedbeaconref[ctldzone.name] = Unit:GetCoordinate() + + self:AddZone(ctldzone) + + local FMbeacon = ctldzone.fmbeacon -- #CTLD.ZoneBeacon + local VHFbeacon = ctldzone.vhfbeacon -- #CTLD.ZoneBeacon + local UHFbeacon = ctldzone.uhfbeacon -- #CTLD.ZoneBeacon + local Name = ctldzone.name + local FM = FMbeacon.frequency -- MHz + local VHF = VHFbeacon.frequency * 1000 -- KHz + local UHF = UHFbeacon.frequency -- MHz + local text = string.format("Dropped %s | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", Name, FM, VHF, UHF) + + self:_SendMessage(text,15,false,Unit:GetGroup()) + + return self +end + +--- (Internal) Housekeeping dropped beacons. +-- @param #CTLD self +-- @return #CTLD self +function CTLD:CheckDroppedBeacons() + self:T(self.lid .. " CheckDroppedBeacons") + + -- check for timeout + local timeout = self.droppedbeacontimeout or 600 + local livebeacontable = {} + + for _,_beacon in pairs (self.droppedBeacons) do + local beacon = _beacon -- #CTLD.CargoZone + local T0 = beacon.timestamp + if timer.getTime() - T0 > timeout then + local name = beacon.name + self.droppedbeaconref[name] = nil + _beacon = nil + else + table.insert(livebeacontable,beacon) + end + end + + self.droppedBeacons = nil + self.droppedBeacons = livebeacontable + + return self +end + --- (Internal) Function to show list of radio beacons -- @param #CTLD self -- @param Wrapper.Group#GROUP Group @@ -149287,8 +177008,8 @@ function CTLD:_ListRadioBeacons(Group, Unit) self:T(self.lid .. " _ListRadioBeacons") local report = REPORT:New("Active Zone Beacons") report:Add("------------------------------------------------------------") - local zones = {[1] = self.pickupZones, [2] = self.wpZones, [3] = self.dropOffZones, [4] = self.shipZones} - for i=1,4 do + local zones = {[1] = self.pickupZones, [2] = self.wpZones, [3] = self.dropOffZones, [4] = self.shipZones, [5] = self.droppedBeacons} + for i=1,5 do for index,cargozone in pairs(zones[i]) do -- Get Beacon object from zone local czone = cargozone -- #CTLD.CargoZone @@ -149319,16 +177040,28 @@ end -- @param #number Mhz Frequency in Mhz. -- @param #number Modulation Modulation AM or FM. -- @param #boolean IsShip If true zone is a ship. -function CTLD:_AddRadioBeacon(Name, Sound, Mhz, Modulation, IsShip) +-- @param #boolean IsDropped If true, this isn't a zone but a dropped beacon +function CTLD:_AddRadioBeacon(Name, Sound, Mhz, Modulation, IsShip, IsDropped) self:T(self.lid .. " _AddRadioBeacon") local Zone = nil if IsShip then Zone = UNIT:FindByName(Name) + elseif IsDropped then + Zone = self.droppedbeaconref[Name] else Zone = ZONE:FindByName(Name) + if not Zone then + Zone = AIRBASE:FindByName(Name):GetZone() + end end local Sound = Sound or "beacon.ogg" - if Zone then + if IsDropped and Zone then + local ZoneCoord = Zone + local ZoneVec3 = ZoneCoord:GetVec3() + local Frequency = Mhz * 1000000 -- Freq in Hertz + local Sound = "l10n/DEFAULT/"..Sound + trigger.action.radioTransmission(Sound, ZoneVec3, Modulation, false, Frequency, 1000) -- Beacon in MP only runs for 30secs straight + elseif Zone then local ZoneCoord = Zone:GetCoordinate() local ZoneVec3 = ZoneCoord:GetVec3() local Frequency = Mhz * 1000000 -- Freq in Hertz @@ -149343,10 +177076,12 @@ end function CTLD:_RefreshRadioBeacons() self:T(self.lid .. " _RefreshRadioBeacons") - local zones = {[1] = self.pickupZones, [2] = self.wpZones, [3] = self.dropOffZones, [4] = self.shipZones} - for i=1,4 do + local zones = {[1] = self.pickupZones, [2] = self.wpZones, [3] = self.dropOffZones, [4] = self.shipZones, [5] = self.droppedBeacons} + for i=1,5 do local IsShip = false if i == 4 then IsShip = true end + local IsDropped = false + if i == 5 then IsDropped = true end for index,cargozone in pairs(zones[i]) do -- Get Beacon object from zone local czone = cargozone -- #CTLD.CargoZone @@ -149359,9 +177094,9 @@ function CTLD:_RefreshRadioBeacons() local FM = FMbeacon.frequency -- MHz local VHF = VHFbeacon.frequency -- KHz local UHF = UHFbeacon.frequency -- MHz - self:_AddRadioBeacon(Name,Sound,FM,radio.modulation.FM, IsShip) - self:_AddRadioBeacon(Name,Sound,VHF,radio.modulation.FM, IsShip) - self:_AddRadioBeacon(Name,Sound,UHF,radio.modulation.AM, IsShip) + self:_AddRadioBeacon(Name,Sound,FM,radio.modulation.FM, IsShip, IsDropped) + self:_AddRadioBeacon(Name,Sound,VHF,radio.modulation.FM, IsShip, IsDropped) + self:_AddRadioBeacon(Name,Sound,UHF,radio.modulation.AM, IsShip, IsDropped) end end end @@ -149414,8 +177149,15 @@ function CTLD:IsUnitInZone(Unit,Zonetype) zonecoord = zone:GetCoordinate() zoneradius = czone.shiplength zonewidth = czone.shipwidth - else + elseif ZONE:FindByName(zonename) then zone = ZONE:FindByName(zonename) + self:T("Checking Zone: "..zonename) + zonecoord = zone:GetCoordinate() + zoneradius = zone:GetRadius() + zonewidth = zoneradius + elseif AIRBASE:FindByName(zonename) then + zone = AIRBASE:FindByName(zonename):GetZone() + self:T("Checking Zone: "..zonename) zonecoord = zone:GetCoordinate() zoneradius = zone:GetRadius() zonewidth = zoneradius @@ -149439,7 +177181,32 @@ function CTLD:IsUnitInZone(Unit,Zonetype) end end ---- User function - Start smoke in a zone close to the Unit. +--- User function - Drop a smoke or flare at current location. +-- @param #CTLD self +-- @param Wrapper.Unit#UNIT Unit The Unit. +-- @param #boolean Flare If true, flare instead. +-- @param #number SmokeColor Color enumerator for smoke, e.g. SMOKECOLOR.Red +function CTLD:SmokePositionNow(Unit, Flare, SmokeColor) + self:T(self.lid .. " SmokePositionNow") + local Smokecolor = self.SmokeColor or SMOKECOLOR.Red + if SmokeColor then + Smokecolor = SmokeColor + end + local FlareColor = self.FlareColor or FLARECOLOR.Red + -- table of #CTLD.CargoZone table + local unitcoord = Unit:GetCoordinate() -- Core.Point#COORDINATE + local Group = Unit:GetGroup() + if Flare then + unitcoord:Flare(FlareColor, 90) + else + local height = unitcoord:GetLandHeight() + 2 + unitcoord.y = height + unitcoord:Smoke(Smokecolor) + end + return self +end + +--- User function - Start smoke/flare in a zone close to the Unit. -- @param #CTLD self -- @param Wrapper.Unit#UNIT Unit The Unit. -- @param #boolean Flare If true, flare instead. @@ -149460,6 +177227,9 @@ function CTLD:SmokeZoneNearBy(Unit, Flare) zone = UNIT:FindByName(zonename) else zone = ZONE:FindByName(zonename) + if not zone then + zone = AIRBASE:FindByName(zonename):GetZone() + end end local zonecoord = zone:GetCoordinate() local active = CZone.active @@ -149494,8 +177264,9 @@ end -- @param #boolean Cantroops Unit can load troops. Default false. -- @param #number Cratelimit Unit can carry number of crates. Default 0. -- @param #number Trooplimit Unit can carry number of troops. Default 0. - -- @param #number Length Unit lenght (in mteres) for the load radius. Default 20. - function CTLD:UnitCapabilities(Unittype, Cancrates, Cantroops, Cratelimit, Trooplimit, Length) + -- @param #number Length Unit lenght (in metres) for the load radius. Default 20. + -- @param #number Maxcargoweight Maxmimum weight in kgs this helo can carry. Default 500. + function CTLD:UnitCapabilities(Unittype, Cancrates, Cantroops, Cratelimit, Trooplimit, Length, Maxcargoweight) self:T(self.lid .. " UnitCapabilities") local unittype = nil local unit = nil @@ -149507,6 +177278,13 @@ end else return self end + local length = 20 + local maxcargo = 500 + local existingcaps = self.UnitTypes[unittype] -- #CTLD.UnitCapabilities + if existingcaps then + length = existingcaps.length or 20 + maxcargo = existingcaps.cargoweightlimit or 500 + end -- set capabilities local capabilities = {} -- #CTLD.UnitCapabilities capabilities.type = unittype @@ -149514,7 +177292,8 @@ end capabilities.troops = Cantroops or false capabilities.cratelimit = Cratelimit or 0 capabilities.trooplimit = Trooplimit or 0 - capabilities.length = Length or 20 + capabilities.length = Length or length + capabilities.cargoweightlimit = Maxcargoweight or maxcargo self.UnitTypes[unittype] = capabilities return self end @@ -149560,8 +177339,8 @@ end local ucoord = Unit:GetCoordinate() local gheight = ucoord:GetLandHeight() local aheight = uheight - gheight -- height above ground - local maxh = self.HercMinAngels-- 1500m - local minh = self.HercMaxAngels -- 5000m + local minh = self.HercMinAngels-- 1500m + local maxh = self.HercMaxAngels -- 5000m local maxspeed = self.HercMaxSpeed -- 77 mps -- DONE: TEST - Speed test for Herc, should not be above 280kph/150kn local kmspeed = uspeed * 3.6 @@ -149589,7 +177368,7 @@ end else local minheight = UTILS.MetersToFeet(self.minimumHoverHeight) local maxheight = UTILS.MetersToFeet(self.maximumHoverHeight) - text = string.format("Hover parameters (autoload/drop):\n - Min height %dm \n - Max height %dm \n - Max speed 6fts \n - In parameter: %s", minheight, maxheight, htxt) + text = string.format("Hover parameters (autoload/drop):\n - Min height %dft \n - Max height %dft \n - Max speed 6ftps \n - In parameter: %s", minheight, maxheight, htxt) end self:_SendMessage(text, 10, false, Group) return self @@ -149806,7 +177585,7 @@ end self:T(_engineers.lid .. _engineers:GetStatus()) if wrenches and wrenches:IsAlive() then if engineers:IsStatus("Running") or engineers:IsStatus("Searching") then - local crates,number = self:_FindCratesNearby(wrenches,nil, self.EngineerSearch) -- #table + local crates,number = self:_FindCratesNearby(wrenches,nil, self.EngineerSearch,true) -- #table engineers:Search(crates,number) elseif engineers:IsStatus("Moving") then engineers:Move() @@ -150019,6 +177798,7 @@ end self:T({From, Event, To}) self:CleanDroppedTroops() self:_RefreshF10Menus() + self:CheckDroppedBeacons() self:_RefreshRadioBeacons() self:CheckAutoHoverload() self:_CheckEngineers() @@ -150254,7 +178034,7 @@ end local statics = nil local statics = {} - self:I(self.lid.."Bulding Statics Table for Saving") + self:T(self.lid.."Bulding Statics Table for Saving") for _,_cargo in pairs (stcstable) do local cargo = _cargo -- #CTLD_CARGO local object = cargo:GetPositionable() -- Wrapper.Static#STATIC @@ -150481,7 +178261,7 @@ end for _id,_entry in pairs (loadeddata) do local dataset = UTILS.Split(_entry,",") - -- 1=Group,2=x,3=y,4=z,5=CargoName,6=CargoTemplates,7=CargoType,8=CratesNeeded,9=CrateMass + -- 1=Group,2=x,3=y,4=z,5=CargoName,6=CargoTemplates,7=CargoType,8=CratesNeeded,9=CrateMass,10=SubCategory local groupname = dataset[1] local vec2 = {} vec2.x = tonumber(dataset[2]) @@ -150530,9 +178310,19107 @@ end return self end end -- end do + +do +--- **Hercules Cargo AIR Drop Events** by Anubis Yinepu +-- Moose CTLD OO refactoring by Applevangelist +-- +-- This script will only work for the Herculus mod by Anubis, and only for **Air Dropping** cargo from the Hercules. +-- Use the standard Moose CTLD if you want to unload on the ground. +-- Payloads carried by pylons 11, 12 and 13 need to be declared in the Herculus_Loadout.lua file +-- Except for Ammo pallets, this script will spawn whatever payload gets launched from pylons 11, 12 and 13 +-- Pylons 11, 12 and 13 are moveable within the Herculus cargobay area +-- Ammo pallets can only be jettisoned from these pylons with no benefit to DCS world +-- To benefit DCS world, Ammo pallets need to be off/on loaded using DCS arming and refueling window +-- Cargo_Container_Enclosed = true: Cargo enclosed in container with parachute, need to be dropped from 100m (300ft) or more, except when parked on ground +-- Cargo_Container_Enclosed = false: Open cargo with no parachute, need to be dropped from 10m (30ft) or less + +------------------------------------------------------ +--- **CTLD_HERCULES** class, extends Core.Base#BASE +-- @type CTLD_HERCULES +-- @field #string ClassName +-- @field #string lid +-- @field #string Name +-- @field #string Version +-- @extends Core.Base#BASE +CTLD_HERCULES = { + ClassName = "CTLD_HERCULES", + lid = "", + Name = "", + Version = "0.0.1", +} + +--- Define cargo types. +-- @type CTLD_HERCULES.Types +-- @field #table Type Name of cargo type, container (boolean) in container or not. +CTLD_HERCULES.Types = { + ["ATGM M1045 HMMWV TOW Air [7183lb]"] = {['name'] = "M1045 HMMWV TOW", ['container'] = true}, + ["ATGM M1045 HMMWV TOW Skid [7073lb]"] = {['name'] = "M1045 HMMWV TOW", ['container'] = false}, + ["APC M1043 HMMWV Armament Air [7023lb]"] = {['name'] = "M1043 HMMWV Armament", ['container'] = true}, + ["APC M1043 HMMWV Armament Skid [6912lb]"] = {['name'] = "M1043 HMMWV Armament", ['container'] = false}, + ["SAM Avenger M1097 Air [7200lb]"] = {['name'] = "M1097 Avenger", ['container'] = true}, + ["SAM Avenger M1097 Skid [7090lb]"] = {['name'] = "M1097 Avenger", ['container'] = false}, + ["APC Cobra Air [10912lb]"] = {['name'] = "Cobra", ['container'] = true}, + ["APC Cobra Skid [10802lb]"] = {['name'] = "Cobra", ['container'] = false}, + ["APC M113 Air [21624lb]"] = {['name'] = "M-113", ['container'] = true}, + ["APC M113 Skid [21494lb]"] = {['name'] = "M-113", ['container'] = false}, + ["Tanker M978 HEMTT [34000lb]"] = {['name'] = "M978 HEMTT Tanker", ['container'] = false}, + ["HEMTT TFFT [34400lb]"] = {['name'] = "HEMTT TFFT", ['container'] = false}, + ["SPG M1128 Stryker MGS [33036lb]"] = {['name'] = "M1128 Stryker MGS", ['container'] = false}, + ["AAA Vulcan M163 Air [21666lb]"] = {['name'] = "Vulcan", ['container'] = true}, + ["AAA Vulcan M163 Skid [21577lb]"] = {['name'] = "Vulcan", ['container'] = false}, + ["APC M1126 Stryker ICV [29542lb]"] = {['name'] = "M1126 Stryker ICV", ['container'] = false}, + ["ATGM M1134 Stryker [30337lb]"] = {['name'] = "M1134 Stryker ATGM", ['container'] = false}, + ["APC LAV-25 Air [22520lb]"] = {['name'] = "LAV-25", ['container'] = true}, + ["APC LAV-25 Skid [22514lb]"] = {['name'] = "LAV-25", ['container'] = false}, + ["M1025 HMMWV Air [6160lb]"] = {['name'] = "Hummer", ['container'] = true}, + ["M1025 HMMWV Skid [6050lb]"] = {['name'] = "Hummer", ['container'] = false}, + ["IFV M2A2 Bradley [34720lb]"] = {['name'] = "M-2 Bradley", ['container'] = false}, + ["IFV MCV-80 [34720lb]"] = {['name'] = "MCV-80", ['container'] = false}, + ["IFV BMP-1 [23232lb]"] = {['name'] = "BMP-1", ['container'] = false}, + ["IFV BMP-2 [25168lb]"] = {['name'] = "BMP-2", ['container'] = false}, + ["IFV BMP-3 [32912lb]"] = {['name'] = "BMP-3", ['container'] = false}, + ["ARV BRDM-2 Air [12320lb]"] = {['name'] = "BRDM-2", ['container'] = true}, + ["ARV BRDM-2 Skid [12210lb]"] = {['name'] = "BRDM-2", ['container'] = false}, + ["APC BTR-80 Air [23936lb]"] = {['name'] = "BTR-80", ['container'] = true}, + ["APC BTR-80 Skid [23826lb]"] = {['name'] = "BTR-80", ['container'] = false}, + ["APC BTR-82A Air [24998lb]"] = {['name'] = "BTR-82A", ['container'] = true}, + ["APC BTR-82A Skid [24888lb]"] = {['name'] = "BTR-82A", ['container'] = false}, + ["SAM ROLAND ADS [34720lb]"] = {['name'] = "Roland Radar", ['container'] = false}, + ["SAM ROLAND LN [34720b]"] = {['name'] = "Roland ADS", ['container'] = false}, + ["SAM SA-13 STRELA [21624lb]"] = {['name'] = "Strela-10M3", ['container'] = false}, + ["AAA ZSU-23-4 Shilka [32912lb]"] = {['name'] = "ZSU-23-4 Shilka", ['container'] = false}, + ["SAM SA-19 Tunguska 2S6 [34720lb]"] = {['name'] = "2S6 Tunguska", ['container'] = false}, + ["Transport UAZ-469 Air [3747lb]"] = {['name'] = "UAZ-469", ['container'] = true}, + ["Transport UAZ-469 Skid [3630lb]"] = {['name'] = "UAZ-469", ['container'] = false}, + ["AAA GEPARD [34720lb]"] = {['name'] = "Gepard", ['container'] = false}, + ["SAM CHAPARRAL Air [21624lb]"] = {['name'] = "M48 Chaparral", ['container'] = true}, + ["SAM CHAPARRAL Skid [21516lb]"] = {['name'] = "M48 Chaparral", ['container'] = false}, + ["SAM LINEBACKER [34720lb]"] = {['name'] = "M6 Linebacker", ['container'] = false}, + ["Transport URAL-375 [14815lb]"] = {['name'] = "Ural-375", ['container'] = false}, + ["Transport M818 [16000lb]"] = {['name'] = "M 818", ['container'] = false}, + ["IFV MARDER [34720lb]"] = {['name'] = "Marder", ['container'] = false}, + ["Transport Tigr Air [15900lb]"] = {['name'] = "Tigr_233036", ['container'] = true}, + ["Transport Tigr Skid [15730lb]"] = {['name'] = "Tigr_233036", ['container'] = false}, + ["IFV TPZ FUCH [33440lb]"] = {['name'] = "TPZ", ['container'] = false}, + ["IFV BMD-1 Air [18040lb]"] = {['name'] = "BMD-1", ['container'] = true}, + ["IFV BMD-1 Skid [17930lb]"] = {['name'] = "BMD-1", ['container'] = false}, + ["IFV BTR-D Air [18040lb]"] = {['name'] = "BTR_D", ['container'] = true}, + ["IFV BTR-D Skid [17930lb]"] = {['name'] = "BTR_D", ['container'] = false}, + ["EWR SBORKA Air [21624lb]"] = {['name'] = "Dog Ear radar", ['container'] = true}, + ["EWR SBORKA Skid [21624lb]"] = {['name'] = "Dog Ear radar", ['container'] = false}, + ["ART 2S9 NONA Air [19140lb]"] = {['name'] = "SAU 2-C9", ['container'] = true}, + ["ART 2S9 NONA Skid [19030lb]"] = {['name'] = "SAU 2-C9", ['container'] = false}, + ["ART GVOZDIKA [34720lb]"] = {['name'] = "SAU Gvozdika", ['container'] = false}, + ["APC MTLB Air [26400lb]"] = {['name'] = "MTLB", ['container'] = true}, + ["APC MTLB Skid [26290lb]"] = {['name'] = "MTLB", ['container'] = false}, + --["Generic Crate [20000lb]"] = {['name'] = "Hercules_Container_Parachute", ['container'] = true} --nothing generic in Moose CTLD +} + +--- Cargo Object +-- @type CTLD_HERCULES.CargoObject +-- @field #number Cargo_Drop_Direction +-- @field #table Cargo_Contents +-- @field #string Cargo_Type_name +-- @field #boolean Container_Enclosed +-- @field #boolean ParatrooperGroupSpawn +-- @field #number Cargo_Country +-- @field #boolean offload_cargo +-- @field #boolean all_cargo_survive_to_the_ground +-- @field #boolean all_cargo_gets_destroyed +-- @field #boolean destroy_cargo_dropped_without_parachute +-- @field Core.Timer#TIMER scheduleFunctionID + +--- [User] Instantiate a new object +-- @param #CTLD_HERCULES self +-- @param #string Coalition Coalition side, "red", "blue" or "neutral" +-- @param #string Alias Name of this instance +-- @param Ops.CTLD#CTLD CtldObject CTLD instance to link into +-- @return #CTLD_HERCULES self +-- @usage +-- Integrate to your CTLD instance like so, where `my_ctld` is a previously created CTLD instance: +-- +-- my_ctld.enableHercules = false -- avoid dual loading via CTLD F10 and F8 ground crew +-- local herccargo = CTLD_HERCULES:New("blue", "Hercules Test", my_ctld) +-- +-- You also need: +-- * A template called "Infantry" for 10 Paratroopers (as set via herccargo.infantrytemplate). +-- * Depending on what you are loading with the help of the ground crew, there are 42 more templates for the various vehicles that are loadable. +-- There's a **quick check output in the `dcs.log`** which tells you what's there and what not. +-- E.g.: +-- ...Checking template for APC BTR-82A Air [24998lb] (BTR-82A) ... MISSING) +-- ...Checking template for ART 2S9 NONA Skid [19030lb] (SAU 2-C9) ... MISSING) +-- ...Checking template for EWR SBORKA Air [21624lb] (Dog Ear radar) ... MISSING) +-- ...Checking template for Transport Tigr Air [15900lb] (Tigr_233036) ... OK) +-- +-- Expected template names are the ones in the rounded brackets. +-- +-- HINTS +-- +-- The script works on the EVENTS.Shot trigger, which is used by the mod when you **drop cargo from the Hercules while flying**. Unloading on the ground does +-- not achieve anything here. If you just want to unload on the ground, use the normal Moose CTLD. +-- There are two ways of airdropping: +-- 1) Very low and very slow (>5m and <10m AGL) - here you can drop stuff which has "Skid" at the end of the cargo name (loaded via F8 Ground Crew menu) +-- 2) Higher up and slow (>100m AGL) - here you can drop paratroopers and cargo which has "Air" at the end of the cargo name (loaded via F8 Ground Crew menu) +function CTLD_HERCULES:New(Coalition, Alias, CtldObject) + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #CTLD_HERCULES + + --set Coalition + if Coalition and type(Coalition)=="string" then + if Coalition=="blue" then + self.coalition=coalition.side.BLUE + self.coalitiontxt = Coalition + elseif Coalition=="red" then + self.coalition=coalition.side.RED + self.coalitiontxt = Coalition + elseif Coalition=="neutral" then + self.coalition=coalition.side.NEUTRAL + self.coalitiontxt = Coalition + else + self:E("ERROR: Unknown coalition in CTLD!") + end + else + self.coalition = Coalition + self.coalitiontxt = string.lower(UTILS.GetCoalitionName(self.coalition)) + end + + -- Set alias. + if Alias then + self.alias=tostring(Alias) + else + self.alias="UNHCR" + if self.coalition then + if self.coalition==coalition.side.RED then + self.alias="Red CTLD Hercules" + elseif self.coalition==coalition.side.BLUE then + self.alias="Blue CTLD Hercules" + end + end + end + + -- Set some string id for output to DCS.log file. + self.lid=string.format("%s (%s) | ", self.alias, self.coalitiontxt) + + self.infantrytemplate = "Infantry" -- template for a group of 10 paratroopers + self.CTLD = CtldObject -- Ops.CTLD#CTLD + + self.verbose = true + + self.j = 0 + self.carrierGroups = {} + self.Cargo = {} + self.ParatrooperCount = {} + + self.ObjectTracker = {} + + -- Set some string id for output to DCS.log file. + self.lid=string.format("%s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown") + + --self:HandleEvent(EVENTS.Birth,self._HandleBirth) + self:HandleEvent(EVENTS.Shot, self._HandleShot) + + self:I(self.lid .. "Started") + + self:CheckTemplates() + + return self +end + +--- [Internal] Function to check availability of templates +-- @param #CTLD_HERCULES self +-- @return #CTLD_HERCULES self +function CTLD_HERCULES:CheckTemplates() + self:T(self.lid .. 'CheckTemplates') + -- inject Paratroopers + self.Types["Paratroopers 10"] = { + name = self.infantrytemplate, + container = false, + available = false, + } + local missing = {} + local nomissing = 0 + local found = {} + local nofound = 0 + + -- list of groundcrew loadables + for _index,_tab in pairs (self.Types) do + local outcometxt = "MISSING" + if _DATABASE.Templates.Groups[_tab.name] then + outcometxt = "OK" + self.Types[_index].available= true + found[_tab.name] = true + else + self.Types[_index].available = false + missing[_tab.name] = true + end + if self.verbose then + self:I(string.format(self.lid .. "Checking template for %s (%s) ... %s", _index,_tab.name,outcometxt)) + end + end + for _,_name in pairs(found) do + nofound = nofound + 1 + end + for _,_name in pairs(missing) do + nomissing = nomissing + 1 + end + self:I(string.format(self.lid .. "Template Check Summary: Found %d, Missing %d, Total %d",nofound,nomissing,nofound+nomissing)) + return self +end + +--- [Internal] Function to spawn a soldier group of 10 units +-- @param #CTLD_HERCULES self +-- @param Wrapper.Group#GROUP Cargo_Drop_initiator +-- @param Core.Point#POINT_VEC3 Cargo_Drop_Position +-- @param #string Cargo_Type_name +-- @param #number CargoHeading +-- @param #number Cargo_Country +-- @param #number GroupSpacing +-- @return #CTLD_HERCULES self +function CTLD_HERCULES:Soldier_SpawnGroup(Cargo_Drop_initiator,Cargo_Drop_Position, Cargo_Type_name, CargoHeading, Cargo_Country, GroupSpacing) + --- TODO: Rework into Moose Spawns + self:T(self.lid .. 'Soldier_SpawnGroup') + self:T(Cargo_Drop_Position) + -- create a matching #CTLD_CARGO type + local InjectTroopsType = CTLD_CARGO:New(nil,self.infantrytemplate,{self.infantrytemplate},CTLD_CARGO.Enum.TROOPS,true,true,10,nil,false,80) + -- get a #ZONE object + local position = Cargo_Drop_Position:GetVec2() + local dropzone = ZONE_RADIUS:New("Infantry " .. math.random(1,10000),position,100) + -- and go: + self.CTLD:InjectTroops(dropzone,InjectTroopsType) + return self +end + +--- [Internal] Function to spawn a group +-- @param #CTLD_HERCULES self +-- @param Wrapper.Group#GROUP Cargo_Drop_initiator +-- @param Core.Point#POINT_VEC3 Cargo_Drop_Position +-- @param #string Cargo_Type_name +-- @param #number CargoHeading +-- @param #number Cargo_Country +-- @return #CTLD_HERCULES self +function CTLD_HERCULES:Cargo_SpawnGroup(Cargo_Drop_initiator,Cargo_Drop_Position, Cargo_Type_name, CargoHeading, Cargo_Country) + --- TODO: Rework into Moose Spawns + self:T(self.lid .. "Cargo_SpawnGroup") + self:T(Cargo_Type_name) + if Cargo_Type_name ~= 'Container red 1' then + -- create a matching #CTLD_CARGO type + local InjectVehicleType = CTLD_CARGO:New(nil,Cargo_Type_name,{Cargo_Type_name},CTLD_CARGO.Enum.VEHICLE,true,true,1,nil,false,1000) + -- get a #ZONE object + local position = Cargo_Drop_Position:GetVec2() + local dropzone = ZONE_RADIUS:New("Vehicle " .. math.random(1,10000),position,100) + -- and go: + self.CTLD:InjectVehicles(dropzone,InjectVehicleType) + end + return self +end + +--- [Internal] Function to spawn static cargo +-- @param #CTLD_HERCULES self +-- @param Wrapper.Group#GROUP Cargo_Drop_initiator +-- @param Core.Point#POINT_VEC3 Cargo_Drop_Position +-- @param #string Cargo_Type_name +-- @param #number CargoHeading +-- @param #boolean dead +-- @param #number Cargo_Country +-- @return #CTLD_HERCULES self +function CTLD_HERCULES:Cargo_SpawnStatic(Cargo_Drop_initiator,Cargo_Drop_Position, Cargo_Type_name, CargoHeading, dead, Cargo_Country) + --- TODO: Rework into Moose Static Spawns + self:T(self.lid .. "Cargo_SpawnStatic") + self:T("Static " .. Cargo_Type_name .. " Dead " .. tostring(dead)) + local position = Cargo_Drop_Position:GetVec2() + local Zone = ZONE_RADIUS:New("Cargo Static " .. math.random(1,10000),position,100) + if not dead then + -- CTLD_CARGO:New(ID, Name, Templates, Sorte, HasBeenMoved, LoadDirectly, CratesNeeded, Positionable, Dropped, PerCrateMass, Stock) + local injectstatic = CTLD_CARGO:New(nil,"Cargo Static Group "..math.random(1,10000),"iso_container",CTLD_CARGO.Enum.STATIC,true,false,1,nil,true,4500,1) + self.CTLD:InjectStatics(Zone,injectstatic,true) + else + --local static = SPAWNSTATIC:NewFromType("iso_container","Cargos",Cargo_Country) + --static.InitDead = true + --static:SpawnFromZone(Zone,CargoHeading) + end + return self +end + +--- [Internal] Spawn cargo objects +-- @param #CTLD_HERCULES self +-- @param Wrapper.Group#GROUP Cargo_Drop_initiator +-- @param #number Cargo_Drop_Direction +-- @param Core.Point#COORDINATE Cargo_Content_position +-- @param #string Cargo_Type_name +-- @param #boolean Cargo_over_water +-- @param #boolean Container_Enclosed +-- @param #boolean ParatrooperGroupSpawn +-- @param #boolean offload_cargo +-- @param #boolean all_cargo_survive_to_the_ground +-- @param #boolean all_cargo_gets_destroyed +-- @param #boolean destroy_cargo_dropped_without_parachute +-- @param #number Cargo_Country +-- @return #CTLD_HERCULES self +function CTLD_HERCULES:Cargo_SpawnObjects(Cargo_Drop_initiator,Cargo_Drop_Direction, Cargo_Content_position, Cargo_Type_name, Cargo_over_water, Container_Enclosed, ParatrooperGroupSpawn, offload_cargo, all_cargo_survive_to_the_ground, all_cargo_gets_destroyed, destroy_cargo_dropped_without_parachute, Cargo_Country) + self:T(self.lid .. 'Cargo_SpawnObjects') + + local CargoHeading = self.CargoHeading + --local Cargo_Drop_Position = {} + + if offload_cargo == true or ParatrooperGroupSpawn == true then + if ParatrooperGroupSpawn == true then + self:Soldier_SpawnGroup(Cargo_Drop_initiator,Cargo_Content_position, Cargo_Type_name, CargoHeading, Cargo_Country, 0) + self:Soldier_SpawnGroup(Cargo_Drop_initiator,Cargo_Content_position, Cargo_Type_name, CargoHeading, Cargo_Country, 5) + self:Soldier_SpawnGroup(Cargo_Drop_initiator,Cargo_Content_position, Cargo_Type_name, CargoHeading, Cargo_Country, 10) + else + self:Cargo_SpawnGroup(Cargo_Drop_initiator,Cargo_Content_position, Cargo_Type_name, CargoHeading, Cargo_Country) + end + else + if all_cargo_gets_destroyed == true or Cargo_over_water == true then + if Container_Enclosed == true then + --self:Cargo_SpawnStatic(Cargo_Drop_initiator,Cargo_Content_position, Cargo_Type_name, CargoHeading, true, Cargo_Country) + if ParatrooperGroupSpawn == false then + --self:Cargo_SpawnStatic(Cargo_Drop_initiator,Cargo_Content_position, "Hercules_Container_Parachute_Static", CargoHeading, true, Cargo_Country) + end + else + --self:Cargo_SpawnStatic(Cargo_Drop_initiator,Cargo_Content_position, Cargo_Type_name, CargoHeading, true, Cargo_Country) + end + else + if all_cargo_survive_to_the_ground == true then + if ParatrooperGroupSpawn == true then + self:Cargo_SpawnStatic(Cargo_Drop_initiator,Cargo_Content_position, Cargo_Type_name, CargoHeading, true, Cargo_Country) + else + self:Cargo_SpawnGroup(Cargo_Drop_initiator,Cargo_Content_position, Cargo_Type_name, CargoHeading, Cargo_Country) + end + if Container_Enclosed == true then + if ParatrooperGroupSpawn == false then + self:Cargo_SpawnStatic(Cargo_Drop_initiator,Cargo_Content_position, "Hercules_Container_Parachute_Static", CargoHeading, false, Cargo_Country) + end + end + end + if destroy_cargo_dropped_without_parachute == true then + if Container_Enclosed == true then + if ParatrooperGroupSpawn == true then + self:Soldier_SpawnGroup(Cargo_Drop_initiator,Cargo_Content_position, Cargo_Type_name, CargoHeading, Cargo_Country, 0) + else + self:Cargo_SpawnGroup(Cargo_Drop_initiator,Cargo_Content_position, Cargo_Type_name, CargoHeading, Cargo_Country) + self:Cargo_SpawnStatic(Cargo_Drop_initiator,Cargo_Content_position, "Hercules_Container_Parachute_Static", CargoHeading, false, Cargo_Country) + end + else + self:Cargo_SpawnStatic(Cargo_Drop_initiator,Cargo_Content_position, Cargo_Type_name, CargoHeading, true, Cargo_Country) + end + end + end + end + return self +end + +--- [Internal] Function to calculate object height +-- @param #CTLD_HERCULES self +-- @param Wrapper.Group#GROUP group The group for which to calculate the height +-- @return #number height over ground +function CTLD_HERCULES:Calculate_Object_Height_AGL(group) + self:T(self.lid .. "Calculate_Object_Height_AGL") + if group.ClassName and group.ClassName == "GROUP" then + local gcoord = group:GetCoordinate() + local height = group:GetHeight() + local lheight = gcoord:GetLandHeight() + self:T(self.lid .. "Height " .. height - lheight) + return height - lheight + else + -- DCS object + --self:T({group}) + if group:isExist() then + local dcsposition = group:getPosition().p + local dcsvec2 = {x = dcsposition.x, y = dcsposition.z} -- Vec2 + local height = math.floor(group:getPosition().p.y - land.getHeight(dcsvec2)) + self.ObjectTracker[group.id_] = dcsposition -- Vec3 + self:T(self.lid .. "Height " .. height) + --self:T({group.id_,self.ObjectTracker[group.id_]}) + return height + else + return 0 + end + end +end + +--- [Internal] Function to check surface type +-- @param #CTLD_HERCULES self +-- @param Wrapper.Group#GROUP group The group for which to calculate the height +-- @return #number height over ground +function CTLD_HERCULES:Check_SurfaceType(object) + self:T(self.lid .. "Check_SurfaceType") + -- LAND,--1 SHALLOW_WATER,--2 WATER,--3 ROAD,--4 RUNWAY--5 + if object:isExist() then + return land.getSurfaceType({x = object:getPosition().p.x, y = object:getPosition().p.z}) + else + return 1 + end +end + +--- [Internal] Function to track cargo objects +-- @param #CTLD_HERCULES self +-- @param #CTLD_HERCULES.CargoObject cargo +-- @param Wrapper.Group#GROUP initiator +-- @return #number height over ground +function CTLD_HERCULES:Cargo_Track(cargo, initiator) + self:T(self.lid .. "Cargo_Track") + local Cargo_Drop_initiator = initiator + if cargo.Cargo_Contents ~= nil then + if self:Calculate_Object_Height_AGL(cargo.Cargo_Contents) < 10 then --pallet less than 5m above ground before spawning + if self:Check_SurfaceType(cargo.Cargo_Contents) == 2 or self:Check_SurfaceType(cargo.Cargo_Contents) == 3 then + cargo.Cargo_over_water = true--pallets gets destroyed in water + end + local dcsvec3 = self.ObjectTracker[cargo.Cargo_Contents.id_] -- last known position + self:T("SPAWNPOSITION: ") + self:T({dcsvec3}) + local Vec2 = { + x=dcsvec3.x, + y=dcsvec3.z, + } + local vec3 = COORDINATE:NewFromVec2(Vec2) + self.ObjectTracker[cargo.Cargo_Contents.id_] = nil + self:Cargo_SpawnObjects(Cargo_Drop_initiator,cargo.Cargo_Drop_Direction, vec3, cargo.Cargo_Type_name, cargo.Cargo_over_water, cargo.Container_Enclosed, cargo.ParatrooperGroupSpawn, cargo.offload_cargo, cargo.all_cargo_survive_to_the_ground, cargo.all_cargo_gets_destroyed, cargo.destroy_cargo_dropped_without_parachute, cargo.Cargo_Country) + if cargo.Cargo_Contents:isExist() then + cargo.Cargo_Contents:destroy()--remove pallet+parachute before hitting ground and replace with Cargo_SpawnContents + end + --timer.removeFunction(cargo.scheduleFunctionID) + cargo.scheduleFunctionID:Stop() + cargo = {} + end + end + return self +end + +--- [Internal] Function to calc north correction +-- @param #CTLD_HERCULES self +-- @param Core.Point#POINT_Vec3 point Position Vec3 +-- @return #number north correction +function CTLD_HERCULES:Calculate_Cargo_Drop_initiator_NorthCorrection(point) + self:T(self.lid .. "Calculate_Cargo_Drop_initiator_NorthCorrection") + if not point.z then --Vec2; convert to Vec3 + point.z = point.y + point.y = 0 + end + local lat, lon = coord.LOtoLL(point) + local north_posit = coord.LLtoLO(lat + 1, lon) + return math.atan2(north_posit.z - point.z, north_posit.x - point.x) +end + +--- [Internal] Function to calc initiator heading +-- @param #CTLD_HERCULES self +-- @param Wrapper.Group#GROUP Cargo_Drop_initiator +-- @return #number north corrected heading +function CTLD_HERCULES:Calculate_Cargo_Drop_initiator_Heading(Cargo_Drop_initiator) + self:T(self.lid .. "Calculate_Cargo_Drop_initiator_Heading") + local Heading = Cargo_Drop_initiator:GetHeading() + Heading = Heading + self:Calculate_Cargo_Drop_initiator_NorthCorrection(Cargo_Drop_initiator:GetVec3()) + if Heading < 0 then + Heading = Heading + (2 * math.pi)-- put heading in range of 0 to 2*pi + end + return Heading + 0.06 -- rad +end + +--- [Internal] Function to initialize dropped cargo +-- @param #CTLD_HERCULES self +-- @param Wrapper.Group#GROUP Initiator +-- @param #table Cargo_Contents Table 'weapon' from event data +-- @param #string Cargo_Type_name Name of this cargo +-- @param #boolean Container_Enclosed Is container? +-- @param #boolean SoldierGroup Is soldier group? +-- @param #boolean ParatrooperGroupSpawnInit Is paratroopers? +-- @return #CTLD_HERCULES self +function CTLD_HERCULES:Cargo_Initialize(Initiator, Cargo_Contents, Cargo_Type_name, Container_Enclosed, SoldierGroup, ParatrooperGroupSpawnInit) + self:T(self.lid .. "Cargo_Initialize") + local Cargo_Drop_initiator = Initiator:GetName() + if Cargo_Drop_initiator ~= nil then + if ParatrooperGroupSpawnInit == true then + self:T("Paratrooper Drop") + -- Paratroopers + if not self.ParatrooperCount[Cargo_Drop_initiator] then + self.ParatrooperCount[Cargo_Drop_initiator] = 1 + else + self.ParatrooperCount[Cargo_Drop_initiator] = self.ParatrooperCount[Cargo_Drop_initiator] + 1 + end + + local Paratroopers = self.ParatrooperCount[Cargo_Drop_initiator] + + self:T("Paratrooper Drop Number " .. self.ParatrooperCount[Cargo_Drop_initiator]) + + local SpawnParas = false + + if math.fmod(Paratroopers,10) == 0 then + SpawnParas = true + end + + self.j = self.j + 1 + self.Cargo[self.j] = {} + self.Cargo[self.j].Cargo_Drop_Direction = self:Calculate_Cargo_Drop_initiator_Heading(Initiator) + self.Cargo[self.j].Cargo_Contents = Cargo_Contents + self.Cargo[self.j].Cargo_Type_name = Cargo_Type_name + self.Cargo[self.j].Container_Enclosed = Container_Enclosed + self.Cargo[self.j].ParatrooperGroupSpawn = SpawnParas + self.Cargo[self.j].Cargo_Country = Initiator:GetCountry() + + if self:Calculate_Object_Height_AGL(Initiator) < 5.0 then --aircraft on ground + self.Cargo[self.j].offload_cargo = true + elseif self:Calculate_Object_Height_AGL(Initiator) < 10.0 then --aircraft less than 10m above ground + self.Cargo[self.j].all_cargo_survive_to_the_ground = true + elseif self:Calculate_Object_Height_AGL(Initiator) < 100.0 then --aircraft more than 10m but less than 100m above ground + self.Cargo[self.j].all_cargo_gets_destroyed = true + else + self.Cargo[self.j].all_cargo_gets_destroyed = false + end + + local timer = TIMER:New(self.Cargo_Track,self,self.Cargo[self.j],Initiator) + self.Cargo[self.j].scheduleFunctionID = timer + timer:Start(5,2,600) + + else + -- no paras + self.j = self.j + 1 + self.Cargo[self.j] = {} + self.Cargo[self.j].Cargo_Drop_Direction = self:Calculate_Cargo_Drop_initiator_Heading(Initiator) + self.Cargo[self.j].Cargo_Contents = Cargo_Contents + self.Cargo[self.j].Cargo_Type_name = Cargo_Type_name + self.Cargo[self.j].Container_Enclosed = Container_Enclosed + self.Cargo[self.j].ParatrooperGroupSpawn = false + self.Cargo[self.j].Cargo_Country = Initiator:GetCountry() + + if self:Calculate_Object_Height_AGL(Initiator) < 5.0 then--aircraft on ground + self.Cargo[self.j].offload_cargo = true + elseif self:Calculate_Object_Height_AGL(Initiator) < 10.0 then--aircraft less than 10m above ground + self.Cargo[self.j].all_cargo_survive_to_the_ground = true + elseif self:Calculate_Object_Height_AGL(Initiator) < 100.0 then--aircraft more than 10m but less than 100m above ground + self.Cargo[self.j].all_cargo_gets_destroyed = true + else + self.Cargo[self.j].destroy_cargo_dropped_without_parachute = true --aircraft more than 100m above ground + end + + local timer = TIMER:New(self.Cargo_Track,self,self.Cargo[self.j],Initiator) + self.Cargo[self.j].scheduleFunctionID = timer + timer:Start(5,2,600) + end + end + return self +end + +--- [Internal] Function to change cargotype per group (Wrench) +-- @param #CTLD_HERCULES self +-- @param #number key Carrier key id +-- @param #string cargoType Type of cargo +-- @param #number cargoNum Number of cargo objects +-- @return #CTLD_HERCULES self +function CTLD_HERCULES:SetType(key,cargoType,cargoNum) + self:T(self.lid .. "SetType") + self.carrierGroups[key]['cargoType'] = cargoType + self.carrierGroups[key]['cargoNum'] = cargoNum + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +-- EventHandlers +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + +--- [Internal] Function to capture SHOT event +-- @param #CTLD_HERCULES self +-- @param Core.Event#EVENTDATA Cargo_Drop_Event The event data +-- @return #CTLD_HERCULES self +function CTLD_HERCULES:_HandleShot(Cargo_Drop_Event) + self:T(self.lid .. "Shot Event ID:" .. Cargo_Drop_Event.id) + if Cargo_Drop_Event.id == EVENTS.Shot then + + local GT_Name = "" + local SoldierGroup = false + local ParatrooperGroupSpawnInit = false + + local GT_DisplayName = Weapon.getDesc(Cargo_Drop_Event.weapon).typeName:sub(15, -1)--Remove "weapons.bombs." from string + self:T(string.format("%sCargo_Drop_Event: %s", self.lid, Weapon.getDesc(Cargo_Drop_Event.weapon).typeName)) + + if (GT_DisplayName == "Squad 30 x Soldier [7950lb]") then + self:Cargo_Initialize(Cargo_Drop_Event.IniGroup, Cargo_Drop_Event.weapon, "Soldier M4 GRG", false, true, true) + end + + if self.Types[GT_DisplayName] then + local GT_Name = self.Types[GT_DisplayName]['name'] + local Cargo_Container_Enclosed = self.Types[GT_DisplayName]['container'] + self:Cargo_Initialize(Cargo_Drop_Event.IniGroup, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) + end + end + return self +end + +--- [Internal] Function to capture BIRTH event +-- @param #CTLD_HERCULES self +-- @param Core.Event#EVENTDATA event The event data +-- @return #CTLD_HERCULES self +function CTLD_HERCULES:_HandleBirth(event) + -- not sure what this is needed for? I think this for setting generic crates "content" setting. + self:T(self.lid .. "Birth Event ID:" .. event.id) + --[[ + if event.id == EVENTS.Birth then + local desc = event.initiator:getDesc() + if desc["displayName"] == "Hercules" then + local grpTab = {} + grpTab['object'] = event.IniGroup + grpTab['name'] = event.IniGroupName + grpTab['cargoType'] = 'Container red 1' + grpTab['cargoNum'] = 1 + grpTab['key'] = #self.carrierGroups + 1 + + table.insert(self.carrierGroups,grpTab) + + local hercCargoMenu = MENU_GROUP:New(event.IniGroup,"CargoTypes",nil) + local mlrs = MENU_GROUP_COMMAND:New(event.IniGroup,"MLRS",hercCargoMenu,self.SetType,self,grpTab['key'],'MLRS',1) + local mlrs = MENU_GROUP_COMMAND:New(event.IniGroup,"Mortar",hercCargoMenu,self.SetType,self,grpTab['key'],'2B11 mortar',8) + local mlrs = MENU_GROUP_COMMAND:New(event.IniGroup,"M-109",hercCargoMenu,self.SetType,self,grpTab['key'],'M-109',1) + local mlrs = MENU_GROUP_COMMAND:New(event.IniGroup,"FOB Crate",hercCargoMenu,self.SetType,self,grpTab['key'],'Container red 1',1) + end + end + --]] + return self +end + +end + ------------------------------------------------------------------- -- End Ops.CTLD.lua ------------------------------------------------------------------- +--- **Ops** - Strategic Zone. +-- +-- **Main Features:** +-- +-- * Monitor if a zone is captured +-- * Monitor if an airbase is captured +-- * Define conditions under which zones are captured/held +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- @module Ops.OpsZone +-- @image OPS_OpsZone.png + + +--- OPSZONE class. +-- @type OPSZONE +-- @field #string ClassName Name of the class. +-- @field #string lid DCS log ID string. +-- @field #number verbose Verbosity of output. +-- @field Core.Zone#ZONE zone The zone. +-- @field Wrapper.Airbase#AIRBASE airbase The airbase that is monitored. +-- @field #string airbaseName Name of the airbase that is monitored. +-- @field #string zoneName Name of the zone. +-- @field #number zoneRadius Radius of the zone in meters. +-- @field #number ownerCurrent Coalition of the current owner of the zone. +-- @field #number ownerPrevious Coalition of the previous owner of the zone. +-- @field Core.Timer#TIMER timerStatus Timer for calling the status update. +-- @field #number Nred Number of red units in the zone. +-- @field #number Nblu Number of blue units in the zone. +-- @field #number Nnut Number of neutral units in the zone. +-- @field #table ObjectCategories Object categories for the scan. +-- @field #table UnitCategories Unit categories for the scan. +-- @field #number Tattacked Abs. mission time stamp when an attack was started. +-- @field #number dTCapture Time interval in seconds until a zone is captured. +-- @field #boolean neutralCanCapture Neutral units can capture. Default `false`. +-- @field #boolean drawZone If `true`, draw the zone on the F10 map. +-- @field #boolean markZone If `true`, mark the zone on the F10 map. +-- @field Wrapper.Marker#MARKER marker Marker on the F10 map. +-- @field #string markerText Text shown in the maker. +-- @field #table chiefs Chiefs that monitor this zone. +-- @field #table Missions Missions that are attached to this OpsZone. +-- @extends Core.Fsm#FSM + +--- *Gentlemen, when the enemy is committed to a mistake we must not interrupt him too soon.* --- Horation Nelson +-- +-- === +-- +-- # The OPSZONE Concept +-- +-- An OPSZONE is a strategically important area. +-- +-- **Restrictions** +-- +-- * Since we are using a DCS routine that scans a zone for units or other objects present in the zone and this DCS routine is limited to cicular zones, only those can be used. +-- +-- @field #OPSZONE +OPSZONE = { + ClassName = "OPSZONE", + verbose = 0, + Nred = 0, + Nblu = 0, + Nnut = 0, + chiefs = {}, + Missions = {}, +} + +--- OPSZONE.MISSION +-- @type OPSZONE.MISSION +-- @field #number Coalition Coalition +-- @field #string Type Type of mission +-- @field Ops.Auftrag#AUFTRAG Mission The actual attached mission + +--- OPSZONE class version. +-- @field #string version +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. +-- DONE: Can neutrals capture? No, since they are _neutral_! +-- DONE: Capture airbases. +-- DONE: Can statics capture or hold a zone? No, unless explicitly requested by mission designer. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new OPSZONE class object. +-- @param #OPSZONE self +-- @param Core.Zone#ZONE Zone The zone. Needs to be a ZONE\_RADIUS (round) zone. Can be passed as ZONE\_AIRBASE or simply as the name of the airbase. +-- @param #number CoalitionOwner Initial owner of the coaliton. Default `coalition.side.NEUTRAL`. +-- @return #OPSZONE self +-- @usage +-- myopszone = OPSZONE:New(ZONE:FindByName("OpsZoneOne"),coalition.side.RED) -- base zone from the mission editor +-- myopszone = OPSZONE:New(ZONE_RADIUS:New("OpsZoneTwo",mycoordinate:GetVec2(),5000),coalition.side.BLUE) -- radius zone of 5km at a coordinate +-- myopszone = OPSZONE:New(ZONE_RADIUS:New("Batumi")) -- airbase zone from Batumi Airbase, ca 2500m radius +-- myopszone = OPSZONE:New(ZONE_AIRBASE:New("Batumi",6000),coalition.side.BLUE) -- airbase zone from Batumi Airbase, but with a specific radius of 6km +-- +function OPSZONE:New(Zone, CoalitionOwner) + + -- Inherit everything from LEGION class. + local self=BASE:Inherit(self, FSM:New()) -- #OPSZONE + + -- Check if zone name instead of ZONE object was passed. + if Zone then + if type(Zone)=="string" then + -- Convert string into a ZONE or ZONE_AIRBASE + local Name=Zone + Zone=ZONE:New(Name) + if not Zone then + local airbase=AIRBASE:FindByName(Name) + if airbase then + Zone=ZONE_AIRBASE:New(Name, 2000) + end + end + if not Zone then + self:E(string.format("ERROR: No ZONE or ZONE_AIRBASE found for name: %s", Name)) + return nil + end + end + else + self:E("ERROR: First parameter Zone is nil in OPSZONE:New(Zone) call!") + return nil + end + + -- Basic checks. + if Zone:IsInstanceOf("ZONE_AIRBASE") then + self.airbase=Zone._.ZoneAirbase + self.airbaseName=self.airbase:GetName() + elseif Zone:IsInstanceOf("ZONE_RADIUS") then + -- Nothing to do. + else + self:E("ERROR: OPSZONE must be a SPHERICAL zone due to DCS restrictions!") + return nil + end + + -- Set some string id for output to DCS.log file. + self.lid=string.format("OPSZONE %s | ", Zone:GetName()) + + -- Set some values. + self.zone=Zone + self.zoneName=Zone:GetName() + self.zoneRadius=Zone:GetRadius() + self.Missions = {} + + -- Current and previous owners. + self.ownerCurrent=CoalitionOwner or coalition.side.NEUTRAL + self.ownerPrevious=CoalitionOwner or coalition.side.NEUTRAL + + -- Contested. + self.isContested=false + + -- We take the airbase coalition. + if self.airbase then + self.ownerCurrent=self.airbase:GetCoalition() + self.ownerPrevious=self.airbase:GetCoalition() + end + + -- Set object categories. + self:SetObjectCategories() + self:SetUnitCategories() + + -- Draw zone. Default is on. + self:SetDrawZone() + self:SetMarkZone(true) + + -- Status timer. + self.timerStatus=TIMER:New(OPSZONE.Status, self) + + -- FMS start state is STOPPED. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Empty") -- Start FSM. + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + self:AddTransition("*", "Captured", "Guarded") -- Zone was captured. + + self:AddTransition("Empty", "Guarded", "Guarded") -- Owning coalition left the zone and returned. + + self:AddTransition("*", "Empty", "Empty") -- No red or blue units inside the zone. + + self:AddTransition("*", "Attacked", "Attacked") -- A guarded zone is under attack. + self:AddTransition("*", "Defeated", "Guarded") -- The owning coalition defeated an attack. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". + -- @function [parent=#OPSZONE] Start + -- @param #OPSZONE self + + --- Triggers the FSM event "Start" after a delay. + -- @function [parent=#OPSZONE] __Start + -- @param #OPSZONE self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Stop". + -- @function [parent=#OPSZONE] Stop + -- @param #OPSZONE self + + --- Triggers the FSM event "Stop" after a delay. + -- @function [parent=#OPSZONE] __Stop + -- @param #OPSZONE self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Captured". + -- @function [parent=#OPSZONE] Captured + -- @param #OPSZONE self + -- @param #number Coalition Coalition side that captured the zone. + + --- Triggers the FSM event "Captured" after a delay. + -- @function [parent=#OPSZONE] __Captured + -- @param #OPSZONE self + -- @param #number delay Delay in seconds. + -- @param #number Coalition Coalition side that captured the zone. + + --- On after "Captured" event. + -- @function [parent=#OPSZONE] OnAfterCaptured + -- @param #OPSZONE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number Coalition Coalition side that captured the zone. + + + --- Triggers the FSM event "Guarded". + -- @function [parent=#OPSZONE] Guarded + -- @param #OPSZONE self + + --- Triggers the FSM event "Guarded" after a delay. + -- @function [parent=#OPSZONE] __Guarded + -- @param #OPSZONE self + -- @param #number delay Delay in seconds. + + --- On after "Guarded" event. + -- @function [parent=#OPSZONE] OnAfterGuarded + -- @param #OPSZONE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Empty". + -- @function [parent=#OPSZONE] Empty + -- @param #OPSZONE self + + --- Triggers the FSM event "Empty" after a delay. + -- @function [parent=#OPSZONE] __Empty + -- @param #OPSZONE self + -- @param #number delay Delay in seconds. + + --- On after "Empty" event. + -- @function [parent=#OPSZONE] OnAfterEmpty + -- @param #OPSZONE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Attacked". + -- @function [parent=#OPSZONE] Attacked + -- @param #OPSZONE self + -- @param #number AttackerCoalition Coalition side that is attacking the zone. + + --- Triggers the FSM event "Attacked" after a delay. + -- @function [parent=#OPSZONE] __Attacked + -- @param #OPSZONE self + -- @param #number delay Delay in seconds. + -- @param #number AttackerCoalition Coalition side that is attacking the zone. + + --- On after "Attacked" event. + -- @function [parent=#OPSZONE] OnAfterAttacked + -- @param #OPSZONE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number AttackerCoalition Coalition side that is attacking the zone. + + + --- Triggers the FSM event "Defeated". + -- @function [parent=#OPSZONE] Defeated + -- @param #OPSZONE self + -- @param #number DefeatedCoalition Coalition side that was defeated. + + --- Triggers the FSM event "Defeated" after a delay. + -- @function [parent=#OPSZONE] __Defeated + -- @param #OPSZONE self + -- @param #number delay Delay in seconds. + -- @param #number DefeatedCoalition Coalition side that was defeated. + + --- On after "Defeated" event. + -- @function [parent=#OPSZONE] OnAfterDefeated + -- @param #OPSZONE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number DefeatedCoalition Coalition side that was defeated. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set verbosity level. +-- @param #OPSZONE self +-- @param #number VerbosityLevel Level of output (higher=more). Default 0. +-- @return #OPSZONE self +function OPSZONE:SetVerbosity(VerbosityLevel) + self.verbose=VerbosityLevel or 0 + return self +end + +--- Set categories of objects that can capture or hold the zone. +-- +-- * Default is {Object.Category.UNIT, Object.Category.STATIC} so units and statics can capture and hold zones. +-- * Set to `{Object.Category.UNIT}` if only units should be able to capture and hold zones +-- +-- Which units can capture zones can be further refined by `:SetUnitCategories()`. +-- +-- @param #OPSZONE self +-- @param #table Categories Object categories. Default is `{Object.Category.UNIT, Object.Category.STATIC}`. +-- @return #OPSZONE self +function OPSZONE:SetObjectCategories(Categories) + + -- Ensure table if something was passed. + if Categories and type(Categories)~="table" then + Categories={Categories} + end + + -- Set categories. + self.ObjectCategories=Categories or {Object.Category.UNIT, Object.Category.STATIC} + + return self +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 self +function OPSZONE:SetUnitCategories(Categories) + + -- Ensure table. + if Categories and type(Categories)~="table" then + Categories={Categories} + end + + -- Set categories. + self.UnitCategories=Categories or {Unit.Category.GROUND_UNIT} + + 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. +-- @return #OPSZONE self +function OPSZONE:SetNeutralCanCapture(CanCapture) + self.neutralCanCapture=CanCapture + return self +end + +--- Set if zone is drawn on the F10 map. Color will change depending on current owning coalition. +-- @param #OPSZONE self +-- @param #boolean Switch If `true` or `nil`, draw zone. If `false`, zone is not drawn. +-- @return #OPSZONE self +function OPSZONE:SetDrawZone(Switch) + if Switch==false then + self.drawZone=false + else + self.drawZone=true + end + return self +end + +--- Set if a marker on the F10 map shows the current zone status. +-- @param #OPSZONE self +-- @param #boolean Switch If `true`, zone is marked. If `false` or `nil`, zone is not marked. +-- @param #boolean ReadOnly If `true` or `nil` then mark is read only. +-- @return #OPSZONE self +function OPSZONE:SetMarkZone(Switch, ReadOnly) + if Switch then + self.markZone=true + local Coordinate=self:GetCoordinate() + self.markerText=self:_GetMarkerText() + self.marker=self.marker or MARKER:New(Coordinate, self.markerText) + if ReadOnly==false then + self.marker.readonly=false + else + self.marker.readonly=true + end + self.marker:ToAll() + else + if self.marker then + self.marker:Remove() + end + self.marker=nil + --self.marker=false + end + return self +end + + +--- Get current owner of the zone. +-- @param #OPSZONE self +-- @return #number Owner coalition. +function OPSZONE:GetOwner() + return self.ownerCurrent +end + +--- Get coalition name of current owner of the zone. +-- @param #OPSZONE self +-- @return #string Owner coalition. +function OPSZONE:GetOwnerName() + return UTILS.GetCoalitionName(self.ownerCurrent) +end + +--- Get coordinate of zone. +-- @param #OPSZONE self +-- @return Core.Point#COORDINATE Coordinate of the zone. +function OPSZONE:GetCoordinate() + local coordinate=self.zone:GetCoordinate() + return coordinate +end + +--- Returns a random coordinate in the zone. +-- @param #OPSZONE self +-- @param #number inner (Optional) Minimal distance from the center of the zone in meters. Default is 0 m. +-- @param #number outer (Optional) Maximal distance from the outer edge of the zone in meters. Default is the radius of the zone. +-- @param #table surfacetypes (Optional) Table of surface types. Can also be a single surface type. We will try max 1000 times to find the right type! +-- @return Core.Point#COORDINATE The random coordinate. +function OPSZONE:GetRandomCoordinate(inner, outer, surfacetypes) + + local zone=self:GetZone() + + local coord=zone:GetRandomCoordinate(inner, outer, surfacetypes) + + return coord +end + +--- Get zone name. +-- @param #OPSZONE self +-- @return #string Name of the zone. +function OPSZONE:GetName() + return self.zoneName +end + +--- Get the zone object. +-- @param #OPSZONE self +-- @return Core.Zone#ZONE The zone. +function OPSZONE:GetZone() + return self.zone +end + +--- Get previous owner of the zone. +-- @param #OPSZONE self +-- @return #number Previous owner coalition. +function OPSZONE:GetPreviousOwner() + return self.ownerPrevious +end + +--- Get duration of the current attack. +-- @param #OPSZONE self +-- @return #number Duration in seconds since when the last attack began. Is `nil` if the zone is not under attack currently. +function OPSZONE:GetAttackDuration() + if self:IsAttacked() and self.Tattacked then + + local dT=timer.getAbsTime()-self.Tattacked + return dT + end + + return nil +end + + +--- Check if the red coalition is currently owning the zone. +-- @param #OPSZONE self +-- @return #boolean If `true`, zone is red. +function OPSZONE:IsRed() + local is=self.ownerCurrent==coalition.side.RED + return is +end + +--- Check if the blue coalition is currently owning the zone. +-- @param #OPSZONE self +-- @return #boolean If `true`, zone is blue. +function OPSZONE:IsBlue() + local is=self.ownerCurrent==coalition.side.BLUE + return is +end + +--- Check if the neutral coalition is currently owning the zone. +-- @param #OPSZONE self +-- @return #boolean If `true`, zone is neutral. +function OPSZONE:IsNeutral() + local is=self.ownerCurrent==coalition.side.NEUTRAL + return is +end + +--- Check if a certain coalition is currently owning the zone. +-- @param #OPSZONE self +-- @param #number Coalition The Coalition that is supposed to own the zone. +-- @return #boolean If `true`, zone is owned by the given coalition. +function OPSZONE:IsCoalition(Coalition) + local is=self.ownerCurrent==Coalition + return is +end + +--- Check if zone is guarded. +-- @param #OPSZONE self +-- @return #boolean If `true`, zone is guarded. +function OPSZONE:IsGuarded() + local is=self:is("Guarded") + return is +end + +--- Check if zone is empty. +-- @param #OPSZONE self +-- @return #boolean If `true`, zone is empty. +function OPSZONE:IsEmpty() + local is=self:is("Empty") + return is +end + +--- Check if zone is being attacked by the opposite coalition. +-- @param #OPSZONE self +-- @return #boolean If `true`, zone is being attacked. +function OPSZONE:IsAttacked() + local is=self:is("Attacked") + return is +end + +--- Check if zone is contested. Contested here means red *and* blue units are present in the zone. +-- @param #OPSZONE self +-- @return #boolean If `true`, zone is contested. +function OPSZONE:IsContested() + return self.isContested +end + +--- Check if FMS is stopped. +-- @param #OPSZONE self +-- @return #boolean If `true`, FSM is stopped +function OPSZONE:IsStopped() + local is=self:is("Stopped") + return is +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Start/Stop Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Start OPSZONE FSM. +-- @param #OPSZONE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSZONE:onafterStart(From, Event, To) + + -- Info. + self:I(self.lid..string.format("Starting OPSZONE v%s", OPSZONE.version)) + + -- Reinit the timer. + self.timerStatus=self.timerStatus or TIMER:New(OPSZONE.Status, self) + + -- Status update. + self.timerStatus:Start(1, 120) + + -- Handle base captured event. + if self.airbase then + self:HandleEvent(EVENTS.BaseCaptured) + end + +end + +--- Stop OPSZONE FSM. +-- @param #OPSZONE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSZONE:onafterStop(From, Event, To) + + -- Info. + self:I(self.lid..string.format("Stopping OPSZONE")) + + -- Reinit the timer. + self.timerStatus:Stop() + + -- Draw zone. + if self.drawZone then + self.zone:UndrawZone() + end + + -- Remove marker. + if self.markZone then + self.marker:Remove() + end + + -- Unhandle events. + self:UnHandleEvent(EVENTS.BaseCaptured) + + -- Stop FSM scheduler. + self.CallScheduler:Clear() + if self.Scheduler then + self.Scheduler:Clear() + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Status Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Update status. +-- @param #OPSZONE self +function OPSZONE:Status() + + -- Current FSM state. + local fsmstate=self:GetState() + + -- Get contested. + local contested=tostring(self:IsContested()) + + -- Info message. + if self.verbose>=1 then + local text=string.format("State %s: Owner %d (previous %d), contested=%s, Nunits: red=%d, blue=%d, neutral=%d", fsmstate, self.ownerCurrent, self.ownerPrevious, contested, self.Nred, self.Nblu, self.Nnut) + self:I(self.lid..text) + end + + -- Scanning zone. + self:Scan() + + -- Evaluate the scan result. + self:EvaluateZone() + + -- Update F10 marker (only if enabled). + self:_UpdateMarker() + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "Captured" event. +-- @param #OPSZONE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number NewOwnerCoalition Coalition of the new owner. +function OPSZONE:onafterCaptured(From, Event, To, NewOwnerCoalition) + + -- Debug info. + self:T(self.lid..string.format("Zone captured by coalition=%d", NewOwnerCoalition)) + + -- Set owners. + self.ownerPrevious=self.ownerCurrent + self.ownerCurrent=NewOwnerCoalition + + for _,_chief in pairs(self.chiefs) do + local chief=_chief --Ops.Chief#CHIEF + if chief.coalition==self.ownerCurrent then + chief:ZoneCaptured(self) + else + chief:ZoneLost(self) + end + end + +end + +--- On after "Empty" event. +-- @param #OPSZONE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSZONE:onafterEmpty(From, Event, To) + + -- Debug info. + self:T(self.lid..string.format("Zone is empty EVENT")) + + -- Inform chief. + for _,_chief in pairs(self.chiefs) do + local chief=_chief --Ops.Chief#CHIEF + chief:ZoneEmpty(self) + end + +end + +--- On after "Attacked" event. +-- @param #OPSZONE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number AttackerCoalition Coalition of the attacking ground troops. +function OPSZONE:onafterAttacked(From, Event, To, AttackerCoalition) + + -- Debug info. + self:T(self.lid..string.format("Zone is being attacked by coalition=%s!", tostring(AttackerCoalition))) + + -- Inform chief. + if AttackerCoalition then + for _,_chief in pairs(self.chiefs) do + local chief=_chief --Ops.Chief#CHIEF + if chief.coalition~=AttackerCoalition then + chief:ZoneAttacked(self) + end + end + end + +end + +--- On after "Defeated" event. +-- @param #OPSZONE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number DefeatedCoalition Coalition side that was defeated. +function OPSZONE:onafterDefeated(From, Event, To, DefeatedCoalition) + + -- Debug info. + self:T(self.lid..string.format("Defeated attack on zone by coalition=%d", DefeatedCoalition)) + + -- Not attacked any more. + self.Tattacked=nil + +end + +--- On enter "Guarded" state. +-- @param #OPSZONE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSZONE:onenterGuarded(From, Event, To) + + -- Debug info. + self:T(self.lid..string.format("Zone is guarded")) + + -- Not attacked any more. + self.Tattacked=nil + + if self.drawZone then + self.zone:UndrawZone() + + local color=self:_GetZoneColor() + + self.zone:DrawZone(nil, color, 1.0, color, 0.5) + end + +end + +--- On enter "Attacked" state. +-- @param #OPSZONE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSZONE:onenterAttacked(From, Event, To) + + -- Debug info. + self:T(self.lid..string.format("Zone is Attacked")) + + -- Time stamp when the attack started. + self.Tattacked=timer.getAbsTime() + + -- Draw zone? + if self.drawZone then + self.zone:UndrawZone() + + -- Color. + local color={1, 204/255, 204/255} + + -- Draw zone. + self.zone:DrawZone(nil, color, 1.0, color, 0.5) + end + + self:_CleanMissionTable() +end + +--- On enter "Empty" event. +-- @param #OPSZONE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSZONE:onenterEmpty(From, Event, To) + + -- Debug info. + self:T(self.lid..string.format("Zone is empty now")) + + if self.drawZone then + self.zone:UndrawZone() + + local color=self:_GetZoneColor() + + self.zone:DrawZone(nil, color, 1.0, color, 0.2) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Scan Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Scan zone. +-- @param #OPSZONE self +-- @return #OPSZONE self +function OPSZONE:Scan() + + -- Debug info. + if self.verbose>=3 then + local text=string.format("Scanning zone %s R=%.1f m", self.zoneName, self.zoneRadius) + self:I(self.lid..text) + end + + -- Search. + local SphereSearch={id=world.VolumeType.SPHERE, params={point=self.zone:GetVec3(), radius=self.zoneRadius}} + + -- Init number of red, blue and neutral units. + local Nred=0 + local Nblu=0 + local Nnut=0 + + --- Function to evaluate the world search + local function EvaluateZone(_ZoneObject) + + local ZoneObject=_ZoneObject --DCS#Object + + if ZoneObject then + + -- Object category. + local ObjectCategory=ZoneObject:getCategory() + + if ObjectCategory==Object.Category.UNIT and ZoneObject:isExist() and ZoneObject:isActive() then + + --- + -- UNIT + --- + + -- This is a DCS unit object. + local DCSUnit=ZoneObject --DCS#Unit + + --- Function to check if unit category is included. + local function Included() + + if not self.UnitCategories then + -- Any unit is included. + return true + else + -- Check if found object is in specified categories. + local CategoryDCSUnit = ZoneObject:getDesc().category + + for _,UnitCategory in pairs(self.UnitCategories) do + if UnitCategory==CategoryDCSUnit then + return true + end + end + + end + + return false + end + + + if Included() then + + -- Get Coalition. + local Coalition=DCSUnit:getCoalition() + + -- Increase counter. + if Coalition==coalition.side.RED then + Nred=Nred+1 + elseif Coalition==coalition.side.BLUE then + Nblu=Nblu+1 + elseif Coalition==coalition.side.NEUTRAL then + Nnut=Nnut+1 + end + + -- Debug info. + if self.verbose>=4 then + self:I(self.lid..string.format("Found unit %s (coalition=%d)", DCSUnit:getName(), Coalition)) + end + end + + elseif ObjectCategory==Object.Category.STATIC and ZoneObject:isExist() then + + --- + -- STATIC + --- + + -- This is a DCS static object. + local DCSStatic=ZoneObject --DCS#Static + + -- Get coalition. + local Coalition=DCSStatic:getCoalition() + + -- CAREFUL! Downed pilots break routine here without any error thrown. + --local unit=STATIC:Find(DCSStatic) + + -- Increase counter. + if Coalition==coalition.side.RED then + Nred=Nred+1 + elseif Coalition==coalition.side.BLUE then + Nblu=Nblu+1 + elseif Coalition==coalition.side.NEUTRAL then + Nnut=Nnut+1 + end + + -- Debug info + if self.verbose>=4 then + self:I(self.lid..string.format("Found static %s (coalition=%d)", DCSStatic:getName(), Coalition)) + end + + elseif ObjectCategory==Object.Category.SCENERY then + + --- + -- SCENERY + --- + + local SceneryType = ZoneObject:getTypeName() + local SceneryName = ZoneObject:getName() + + -- Debug info. + self:T2(self.lid..string.format("Found scenery type=%s, name=%s", SceneryType, SceneryName)) + end + + end + + return true + end + + -- Search objects. + world.searchObjects(self.ObjectCategories, SphereSearch, EvaluateZone) + + -- Debug info. + if self.verbose>=3 then + local text=string.format("Scan result Nred=%d, Nblue=%d, Nneutral=%d", Nred, Nblu, Nnut) + self:I(self.lid..text) + end + + -- Set values. + self.Nred=Nred + self.Nblu=Nblu + self.Nnut=Nnut + + return self +end + +--- Evaluate zone. +-- @param #OPSZONE self +-- @return #OPSZONE self +function OPSZONE:EvaluateZone() + + -- Set values. + local Nred=self.Nred + local Nblu=self.Nblu + local Nnut=self.Nnut + + if self:IsRed() then + + --- + -- RED zone + --- + + if Nred==0 then + + -- No red units in red zone any more. + + if Nblu>0 then + -- Blue captured red zone. + if not self.airbase then + self:Captured(coalition.side.BLUE) + end + elseif Nnut>0 and self.neutralCanCapture then + -- Neutral captured red zone. + if not self.airbase then + self:Captured(coalition.side.NEUTRAL) + end + else + -- Red zone is now empty (but will remain red). + if not self:IsEmpty() then + self:Empty() + end + end + + else + + -- Red units in red zone. + + if Nblu>0 then + + if not self:IsAttacked() then + self:Attacked(coalition.side.BLUE) + end + + elseif Nblu==0 then + + if self:IsAttacked() and self:IsContested() then + self:Defeated(coalition.side.BLUE) + elseif self:IsEmpty() then + -- Red units left zone and returned (or from initial Empty state). + self:Guarded() + end + + end + + end + + -- Contested by blue? + if Nblu==0 then + self.isContested=false + else + self.isContested=true + end + + elseif self:IsBlue() then + + --- + -- BLUE zone + --- + + if Nblu==0 then + + -- No blue units in blue zone any more. + + if Nred>0 then + -- Red captured blue zone. + if not self.airbase then + self:Captured(coalition.side.RED) + end + elseif Nnut>0 and self.neutralCanCapture then + -- Neutral captured blue zone. + if not self.airbase then + self:Captured(coalition.side.NEUTRAL) + end + else + -- Blue zone is empty now. + if not self:IsEmpty() then + self:Empty() + end + end + + else + + -- Still blue units in blue zone. + + if Nred>0 then + + if not self:IsAttacked() then + -- Red is attacking blue zone. + self:Attacked(coalition.side.RED) + end + + elseif Nred==0 then + + if self:IsAttacked() and self:IsContested() then + -- Blue defeated read attack. + self:Defeated(coalition.side.RED) + elseif self:IsEmpty() then + -- Blue units left zone and returned (or from initial Empty state). + self:Guarded() + end + + end + + end + + -- Contested by red? + if Nred==0 then + self.isContested=false + else + self.isContested=true + end + + elseif self:IsNeutral() then + + --- + -- NEUTRAL zone + --- + + -- Not checked as neutrals cant capture (for now). + --if Nnut==0 then + + -- No neutral units in neutral zone any more. + + if Nred>0 and Nblu>0 then + self:T(self.lid.."FF neutrals left neutral zone and red and blue are present! What to do?") + if not self:IsAttacked() then + self:Attacked() + end + self.isContested=true + elseif Nred>0 then + -- Red captured neutral zone. + if not self.airbase then + self:Captured(coalition.side.RED) + end + elseif Nblu>0 then + -- Blue captured neutral zone. + if not self.airbase then + self:Captured(coalition.side.BLUE) + end + else + -- Neutral zone is empty now. + if not self:IsEmpty() then + self:Empty() + end + end + + --end + + else + self:E(self.lid.."ERROR: Unknown coaliton!") + end + + + -- Finally, check airbase coalition + if self.airbase then + + -- Current coalition. + local airbasecoalition=self.airbase:GetCoalition() + + if airbasecoalition~=self.ownerCurrent then + self:T(self.lid..string.format("Captured airbase %s: Coaltion %d-->%d", self.airbaseName, self.ownerCurrent, airbasecoalition)) + self:Captured(airbasecoalition) + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- DCS Event Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Monitor hit events. +-- @param #OPSZONE self +-- @param Core.Event#EVENTDATA EventData The event data. +function OPSZONE:OnEventHit(EventData) + + if self.HitsOn then + + local UnitHit = EventData.TgtUnit + + -- Check if unit is inside the capture zone and that it is of the defending coalition. + if UnitHit and UnitHit:IsInZone(self) and UnitHit:GetCoalition()==self.ownerCurrent then + + -- Update last hit time. + self.HitTimeLast=timer.getTime() + + -- Only trigger attacked event if not already in state "Attacked". + if not self:IsAttacked() then + self:T3(self.lid.."Hit ==> Attack") + self:Attacked() + end + + end + + end + +end + +--- Monitor base captured events. +-- @param #OPSZONE self +-- @param Core.Event#EVENTDATA EventData The event data. +function OPSZONE:OnEventBaseCaptured(EventData) + + if EventData and EventData.Place and self.airbase and self.airbaseName then + + -- Place is the airbase that was captured. + local airbase=EventData.Place --Wrapper.Airbase#AIRBASE + + -- Check that this airbase belongs or did belong to this warehouse. + if EventData.PlaceName==self.airbaseName then + + -- New coalition of the airbase + local CoalitionNew=airbase:GetCoalition() + + -- Debug info. + self:I(self.lid..string.format("EVENT BASE CAPTURED: New coalition of airbase %s: %d [previous=%d]", self.airbaseName, CoalitionNew, self.ownerCurrent)) + + -- Check that coalition actually changed. + if CoalitionNew~=self.ownerCurrent then + self:Captured(CoalitionNew) + end + + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get RGB color of zone depending on current owner. +-- @param #OPSZONE self +-- @return #table RGB color. +function OPSZONE:_GetZoneColor() + + local color={0,0,0} + + if self.ownerCurrent==coalition.side.NEUTRAL then + color={1, 1, 1} + elseif self.ownerCurrent==coalition.side.BLUE then + color={0, 0, 1} + elseif self.ownerCurrent==coalition.side.RED then + color={1, 0, 0} + else + + end + + return color +end + +--- Update marker on the F10 map. +-- @param #OPSZONE self +function OPSZONE:_UpdateMarker() + + if self.markZone then + + -- Get marker text. + local text=self:_GetMarkerText() + + -- Chck if marker text changed and if so, update the marker. + if text~=self.markerText then + self.markerText=text + self.marker:UpdateText(self.markerText) + end + + --TODO: Update position if changed. + + end + +end + +--- Get marker text +-- @param #OPSZONE self +-- @return #string Marker text. +function OPSZONE:_GetMarkerText() + + local owner=UTILS.GetCoalitionName(self.ownerCurrent) + local prevowner=UTILS.GetCoalitionName(self.ownerPrevious) + + -- Get marker text. + local text=string.format("%s: Owner=%s [%s]\nState=%s [Contested=%s]\nBlue=%d, Red=%d, Neutral=%d", + self.zoneName, owner, prevowner, self:GetState(), tostring(self:IsContested()), self.Nblu, self.Nred, self.Nnut) + + return text +end + +--- Add a chief that monitors this zone. Chief will be informed about capturing etc. +-- @param #OPSZONE self +-- @param Ops.Chief#CHIEF Chief The chief. +-- @return #table RGB color. +function OPSZONE:_AddChief(Chief) + + -- Add chief. + table.insert(self.chiefs, Chief) + +end + +--- Add an entry to the OpsZone mission table +-- @param #OPSZONE self +-- @param #number Coalition Coalition of type e.g. coalition.side.NEUTRAL +-- @param #string Type Type of mission, e.g. AUFTRAG.Type.CAS +-- @param Ops.Auftrag#AUFTRAG Auftrag The Auftrag itself +-- @return #OPSZONE self +function OPSZONE:_AddMission(Coalition,Type,Auftrag) + + -- Add a mission + local entry = {} -- #OPSZONE.MISSION + entry.Coalition = Coalition or coalition.side.NEUTRAL + entry.Type = Type or "" + entry.Mission = Auftrag or nil + + table.insert(self.Missions,entry) + + return self +end + +--- Get the OpsZone mission table. #table of #OPSZONE.MISSION entries +-- @param #OPSZONE self +-- @return #table Missions +function OPSZONE:_GetMissions() + return self.Missions +end + +--- Add an entry to the OpsZone mission table. +-- @param #OPSZONE self +-- @param #number Coalition Coalition of type e.g. `coalition.side.NEUTRAL`. +-- @param #string Type Type of mission, e.g. `AUFTRAG.Type.CAS`. +-- @return #boolean found True if we have that kind of mission, else false. +-- @return #table Missions Table of `Ops.Auftrag#AUFTRAG` entries. +function OPSZONE:_FindMissions(Coalition, Type) + -- search the table + local foundmissions = {} + local found = false + for _,_entry in pairs(self.Missions) do + local entry = _entry -- #OPSZONE.MISSION + if entry.Coalition == Coalition and entry.Type == Type and entry.Mission and entry.Mission:IsNotOver() then + table.insert(foundmissions,entry.Mission) + found = true + end + end + return found, foundmissions +end + +--- Clean mission table from missions that are over. +-- @param #OPSZONE self +-- @return #OPSZONE self +function OPSZONE:_CleanMissionTable() + local missions = {} + for _,_entry in pairs(self.Missions) do + local entry = _entry -- #OPSZONE.MISSION + if entry.Mission and entry.Mission:IsNotOver() then + table.insert(missions,entry) + end + end + self.Missions = missions + return self +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- **Ops** - Chief of Staff. +-- +-- **Main Features:** +-- +-- * Automatic target engagement based on detection network +-- * Define multiple border, conflict and attack zones +-- * Define strategic "capture" zones +-- * Set strategy of chief from passive to agressive +-- * Manual target engagement via AUFTRAG and TARGET classes +-- * Add AIRWINGS, BRIGADES and FLEETS as resources +-- * Seamless air-to-air, air-to-ground, ground-to-ground dispatching +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Ops.Chief +-- @image OPS_Chief.png + + +--- CHIEF class. +-- @type CHIEF +-- @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 #table targetqueue Target queue. +-- @field #table zonequeue Strategic zone queue. +-- @field Core.Set#SET_ZONE borderzoneset Set of zones defining the border of our territory. +-- @field Core.Set#SET_ZONE yellowzoneset Set of zones defining the extended border. Defcon is set to YELLOW if enemy activity is detected. +-- @field Core.Set#SET_ZONE engagezoneset Set of zones where enemies are actively engaged. +-- @field #number threatLevelMin Lowest threat level of targets to attack. +-- @field #number threatLevelMax Highest threat level of targets to attack. +-- @field #string Defcon Defence condition. +-- @field #string strategy Strategy of the CHIEF. +-- @field Ops.Commander#COMMANDER commander Commander of assigned legions. +-- @field #number Nsuccess Number of successful missions. +-- @field #number Nfailure Number of failed mission. +-- @extends Ops.Intelligence#INTEL + +--- *In preparing for battle I have always found that plans are useless, but planning is indispensable* -- Dwight D Eisenhower +-- +-- === +-- +-- # The CHIEF Concept +-- +-- The Chief of staff gathers INTEL and assigns missions (AUFTRAG) the airforce, army and/or navy. +-- +-- # Territory +-- +-- The chief class allows you to define boarder zones, conflict zones and attack zones. +-- +-- ## Border Zones +-- +-- Border zones define your own territory. +-- They can be set via the @{#CHIEF.SetBorderZones}() function as a set or added zone by zone via the @{#CHIEF.AddBorderZone}() function. +-- +-- ## Conflict Zones +-- +-- Conflict zones define areas, which usually are under dispute of different coalitions. +-- They can be set via the @{#CHIEF.SetConflictZones}() function as a set or added zone by zone via the @{#CHIEF.AddConflictZone}() function. +-- +-- ## Attack Zones +-- +-- Attack zones are zones that usually lie within the enemy territory. They are only enganged with an agressive strategy. +-- They can be set via the @{#CHIEF.SetAttackZones}() function as a set or added zone by zone via the @{#CHIEF.AddAttackZone}() function. +-- +-- # Defense Condition +-- +-- The defence condition (DEFCON) depends on enemy activity detected in the different zone types and is set automatically. +-- +-- * `CHIEF.Defcon.GREEN`: No enemy activities detected. +-- * `CHIEF.Defcon.YELLOW`: Enemy activity detected in conflict zones. +-- * `CHIEF.Defcon.RED`: Enemy activity detected in border zones. +-- +-- The current DEFCON can be retrieved with the @(#CHIEF.GetDefcon)() function. +-- +-- When the DEFCON changed, an FSM event @{#CHIEF.DefconChange} is triggered. Mission designers can hook into this event via the @{#CHIEF.OnAfterDefconChange}() function: +-- +-- --- Function called when the DEFCON changes. +-- function myChief:OnAfterDefconChange(From, Event, To, Defcon) +-- local text=string.format("Changed DEFCON to %s", Defcon) +-- MESSAGE:New(text, 120):ToAll() +-- end +-- +-- # Strategy +-- +-- The strategy of the chief determines, in which areas targets are engaged automatically. +-- +-- * `CHIEF.Strategy.PASSIVE`: Chief is completely passive. No targets at all are engaged automatically. +-- * `CHIEF.Strategy.DEFENSIVE`: Chief acts defensively. Only targets in his own territory are engaged. +-- * `CHIEF.Strategy.OFFENSIVE`: Chief behaves offensively. Targets in his own territory and in conflict zones are enganged. +-- * `CHIEF.Strategy.AGGRESSIVE`: Chief is aggressive. Targets in his own territory, in conflict zones and in attack zones are enganged. +-- * `CHIEF.Strategy.TOTALWAR`: Anything anywhere is enganged. +-- +-- The strategy can be set by the @(#CHIEF.SetStrategy)() and retrieved with the @(#CHIEF.GetStrategy)() function. +-- +-- When the strategy is changed, the FSM event @{#CHIEF.StrategyChange} is triggered and customized code can be added to the @{#CHIEF.OnAfterStrategyChange}() function: +-- +-- --- Function called when the STRATEGY changes. +-- function myChief:OnAfterStrategyChange(From, Event, To, Strategy) +-- local text=string.format("Strategy changd to %s", Strategy) +-- MESSAGE:New(text, 120):ToAll() +-- end +-- +-- # Resources +-- +-- A chief needs resources such as air, ground and naval assets. These can be added in form of AIRWINGs, BRIGADEs and FLEETs. +-- +-- Whenever the chief detects a target or receives a mission, he will select the best available assets and assign them to the mission. +-- The best assets are determined by their mission performance, payload performance (in case of air), distance to the target, skill level, etc. +-- +-- ## Adding Airwings +-- +-- Airwings can be added via the @{#CHIEF.AddAirwing}() function. +-- +-- ## Adding Brigades +-- +-- Brigades can be added via the @{#CHIEF.AddBrigade}() function. +-- +-- ## Adding Fleets +-- +-- 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 +-- +-- Strategically important zones, which should be captured can be added via the @{#CHIEF.AddStrategicZone}(*OpsZone, Prio, Importance*) function. +-- The first parameter *OpsZone* is an @{Ops.OpsZone#OPSZONE} specifying the zone. This has to be a **circular zone** due to DCS API restrictions. +-- The second parameter *Prio* is the priority. The zone queue is sorted wrt to lower prio values. By default this is set to 50. +-- The third parameter *Importance* is the importance of the zone. By default this is `nil`. If you specify one zone with importance 2 and a second zone with +-- importance 3, then the zone of importance 2 is attacked first and only if that zone has been captured, zones that have importances with higher values are attacked. +-- +-- For example: +-- +-- local myStratZone=myChief:AddStrategicZone(myOpsZone, nil , 2) +-- +-- Will at a strategic zone with importance 2. +-- +-- If the zone is currently owned by another coalition and enemy ground troops are present in the zone, a CAS and an ARTY mission are lauchned: +-- +-- * A mission of type `AUFTRAG.Type.CASENHANCED` is started if assets are available that can carry out this mission type. +-- * A mission of type `AUFTRAG.Type.ARTY` is started provided assets are available. +-- +-- The CAS flight(s) will patrol the zone randomly and take out enemy ground units they detect. It can always be possible that the enemies cannot be detected however. +-- The assets will shell the zone. However, it is unlikely that they hit anything as they do not have any information about the location of the enemies. +-- +-- Once the zone is cleaned of enemy forces, ground troops are send there. By default, two missions are launched: +-- +-- * First mission is of type `AUFTRAG.Type.ONGUARD` and will send infantry groups. These are transported by helicopters. Therefore, helo assets with `AUFTRAG.Type.OPSTRANSPORT` need to be available. +-- * The second mission is also of type `AUFTRAG.Type.ONGUARD` but will send tanks if these are available. +-- +-- ## Customized Reaction +-- +-- The default mission types and number of assets can be customized for the two scenarious (zone empty or zone occupied by the enemy). +-- +-- In order to do this, you need to create resource lists (one for each scenario) via the @{#CHIEF.CreateResource}() function. +-- These lists can than passed as additional parameters to the @{#CHIEF.AddStrategicZone} function. +-- +-- For example: +-- +-- --- Create a resource list of mission types and required assets for the case that the zone is OCCUPIED. +-- -- +-- -- Here, we create an enhanced CAS mission and employ at least on and at most two asset groups. +-- local ResourceOccupied=myChief:CreateResource(AUFTRAG.Type.CASENHANCED, 1, 2) +-- -- We also add ARTY missions with at least one and at most two assets. We additionally require these to be MLRS groups (and not howitzers). +-- myChief:AddToResource(ResourceOccupied, AUFTRAG.Type.ARTY, 1, 2, nil, "MLRS") +-- -- Add at least one RECON mission that uses UAV type assets. +-- myChief:AddToResource(ResourceOccupied, AUFTRAG.Type.RECON, 1, nil, GROUP.Attribute.AIR_UAV) +-- -- Add at least one but at most two BOMBCARPET missions. +-- myChief:AddToResource(ResourceOccupied, AUFTRAG.Type.BOMBCARPET, 1, 2) +-- +-- --- Create a resource list of mission types and required assets for the case that the zone is EMPTY. +-- -- +-- -- Here, we create an ONGUARD mission and employ at least on and at most five infantry assets. +-- local ResourceEmpty=myChief:CreateResource(AUFTRAG.Type.ONGUARD, 1, 5, GROUP.Attribute.GROUND_INFANTRY) +-- -- Additionally, we send up to three tank groups. +-- myChief:AddToResource(ResourceEmpty, AUFTRAG.Type.ONGUARD, 1, 3, GROUP.Attribute.GROUND_TANK) +-- -- Finally, we send two groups that patrol the zone. +-- myChief:AddToResource(ResourceEmpty, AUFTRAG.Type.PATROLZONE, 2) +-- +-- -- Add stratetic zone with customized reaction. +-- myChief:AddStrategicZone(myOpsZone, nil , 2, ResourceOccupied, ResourceEmpty) +-- +-- As the location of the enemies is not known, only mission types that don't require an explicit target group are possible. These are +-- +-- * `AUFTRAG.Type.CASENHANCED` +-- * `AUFTRAG.Type.ARTY` +-- * `AUFTRAG.Type.PATROLZONE` +-- * `AUFTRAG.Type.ONGUARD` +-- * `AUFTRAG.Type.RECON` +-- * `AUFTRAG.Type.AMMOSUPPLY` +-- * `AUFTRAG.Type.BOMBING` +-- * `AUFTRAG.Type.BOMBCARPET` +-- * `AUFTRAG.Type.BARRAGE` +-- +-- ## Events +-- +-- Whenever a strategic zone is captured by us the FSM event @{#CHIEF.ZoneCaptured} is triggered and customized further actions can be executed +-- with the @{#CHIEF.OnAfterZoneCaptured}() function. +-- +-- Whenever a strategic zone is lost (captured by the enemy), the FSM event @{#CHIEF.ZoneLost} is triggered and customized further actions can be executed +-- with the @{#CHIEF.OnAfterZoneLost}() function. +-- +-- Further events are +-- +-- * @{#CHIEF.ZoneEmpty}, once the zone is completely empty of ground troops. Code can be added to the @{#CHIEF.OnAfterZoneEmpty}() function. +-- * @{#CHIEF.ZoneAttacked}, once the zone is under attack. Code can be added to the @{#CHIEF.OnAfterZoneAttacked}() function. +-- +-- Note that the ownership of a zone is determined via zone scans, i.e. not via the detection network. In other words, there is an all knowing eye. +-- Think of it as the local population providing the intel. It's not totally realistic but the best compromise within the limits of DCS. +-- +-- +-- +-- @field #CHIEF +CHIEF = { + ClassName = "CHIEF", + verbose = 0, + lid = nil, + targetqueue = {}, + zonequeue = {}, + borderzoneset = nil, + yellowzoneset = nil, + engagezoneset = nil, + tacview = false, + Nsuccess = 0, + Nfailure = 0, +} + +--- Defence condition. +-- @type CHIEF.DEFCON +-- @field #string GREEN No enemy activities detected in our terretory or conflict zones. +-- @field #string YELLOW Enemy in conflict zones. +-- @field #string RED Enemy within our border. +CHIEF.DEFCON = { + GREEN="Green", + YELLOW="Yellow", + RED="Red", +} + +--- Strategy. +-- @type CHIEF.Strategy +-- @field #string PASSIVE No targets at all are engaged. +-- @field #string DEFENSIVE Only target in our own terretory are engaged. +-- @field #string OFFENSIVE Targets in own terretory and yellow zones are engaged. +-- @field #string AGGRESSIVE Targets in own terretory, conflict zones and attack zones are engaged. +-- @field #string TOTALWAR Anything is engaged anywhere. +CHIEF.Strategy = { + PASSIVE="Passive", + DEFENSIVE="Defensive", + OFFENSIVE="Offensive", + AGGRESSIVE="Aggressive", + TOTALWAR="Total War" +} + +--- Mission performance. +-- @type CHIEF.MissionPerformance +-- @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. +-- @field #number prio Priority. +-- @field #number importance Importance. +-- @field #table resourceEmpty Resource list. +-- @field #table resourceOccup Resource list. +-- @field #table missions Mission. + +--- Resource. +-- @type CHIEF.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. + +--- CHIEF class version. +-- @field #string version +CHIEF.version="0.4.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Let user specify amount of resources. +-- DONE: Tactical overview. +-- DONE: Add event for opsgroups on mission. +-- DONE: Add event for zone captured. +-- DONE: Limits of missions? +-- DONE: Create a good mission, which can be passed on to the COMMANDER. +-- DONE: Capture OPSZONEs. +-- DONE: Get list of own assets and capabilities. +-- DONE: Get list/overview of enemy assets etc. +-- DONE: Put all contacts into target list. Then make missions from them. +-- DONE: Set of interesting zones. +-- DONE: Add/remove spawned flightgroups to detection set. +-- DONE: Borderzones. +-- NOGO: Maybe it's possible to preselect the assets for the mission. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new CHIEF object and start the FSM. +-- @param #CHIEF self +-- @param #number Coalition Coalition side, e.g. `coaliton.side.BLUE`. Can also be passed as a string "red", "blue" or "neutral". +-- @param Core.Set#SET_GROUP AgentSet Set of agents (groups) providing intel. Default is an empty set. +-- @param #string Alias An *optional* alias how this object is called in the logs etc. +-- @return #CHIEF self +function CHIEF:New(Coalition, AgentSet, Alias) + + -- Set alias. + Alias=Alias or "CHIEF" + + -- coalition + if type(Coalition) == "string" then + if string.lower(Coalition) == "blue" then + Coalition = coalition.side.BLUE + elseif string.lower(Coalition) == "red" then + Coalition = coalition.side.RED + else + Coalition = coalition.side.NEUTRAL + end + end + + -- Inherit everything from INTEL class. + local self=BASE:Inherit(self, INTEL:New(AgentSet, Coalition, Alias)) --#CHIEF + + -- Defaults. + self:SetBorderZones() + self:SetConflictZones() + self:SetAttackZones() + self:SetThreatLevelRange() + + -- Init stuff. + self.Defcon=CHIEF.DEFCON.GREEN + self.strategy=CHIEF.Strategy.DEFENSIVE + self.TransportCategories = {Group.Category.HELICOPTER} + + -- Create a new COMMANDER. + self.commander=COMMANDER:New(Coalition) + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("*", "MissionAssign", "*") -- Assign mission to a COMMANDER. + self:AddTransition("*", "MissionCancel", "*") -- Cancel mission. + + self:AddTransition("*", "TransportCancel", "*") -- Cancel transport. + + self:AddTransition("*", "OpsOnMission", "*") -- An OPSGROUP was send on a Mission (AUFTRAG). + + self:AddTransition("*", "ZoneCaptured", "*") -- + self:AddTransition("*", "ZoneLost", "*") -- + self:AddTransition("*", "ZoneEmpty", "*") -- + self:AddTransition("*", "ZoneAttacked", "*") -- + + self:AddTransition("*", "DefconChange", "*") -- Change defence condition. + self:AddTransition("*", "StrategyChange", "*") -- Change strategy condition. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". + -- @function [parent=#CHIEF] Start + -- @param #CHIEF self + + --- Triggers the FSM event "Start" after a delay. + -- @function [parent=#CHIEF] __Start + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Stop". + -- @param #CHIEF self + + --- Triggers the FSM event "Stop" after a delay. + -- @function [parent=#CHIEF] __Stop + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Status". + -- @function [parent=#CHIEF] Status + -- @param #CHIEF self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#CHIEF] __Status + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "DefconChange". + -- @function [parent=#CHIEF] DefconChange + -- @param #CHIEF self + -- @param #string Defcon New Defence Condition. + + --- Triggers the FSM event "DefconChange" after a delay. + -- @function [parent=#CHIEF] __DefconChange + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param #string Defcon New Defence Condition. + + --- On after "DefconChange" event. + -- @function [parent=#CHIEF] OnAfterDefconChange + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string Defcon New Defence Condition. + + + --- Triggers the FSM event "StrategyChange". + -- @function [parent=#CHIEF] StrategyChange + -- @param #CHIEF self + -- @param #string Strategy New strategy. + + --- Triggers the FSM event "StrategyChange" after a delay. + -- @function [parent=#CHIEF] __StrategyChange + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param #string Strategy New strategy. + + --- On after "StrategyChange" event. + -- @function [parent=#CHIEF] OnAfterStrategyChange + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string Strategy New strategy. + + + --- Triggers the FSM event "MissionAssign". + -- @function [parent=#CHIEF] MissionAssign + -- @param #CHIEF self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Legions The Legion(s) to which the mission is assigned. + + --- Triggers the FSM event "MissionAssign" after a delay. + -- @function [parent=#CHIEF] __MissionAssign + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Legions The Legion(s) to which the mission is assigned. + + --- On after "MissionAssign" event. + -- @function [parent=#CHIEF] OnAfterMissionAssign + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Legions The Legion(s) to which the mission is assigned. + + --- Triggers the FSM event "MissionCancel". + -- @function [parent=#CHIEF] MissionCancel + -- @param #CHIEF self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "MissionCancel" after a delay. + -- @function [parent=#CHIEF] __MissionCancel + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "MissionCancel" event. + -- @function [parent=#CHIEF] OnAfterMissionCancel + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "TransportCancel". + -- @function [parent=#CHIEF] TransportCancel + -- @param #CHIEF self + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- Triggers the FSM event "TransportCancel" after a delay. + -- @function [parent=#CHIEF] __TransportCancel + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- On after "TransportCancel" event. + -- @function [parent=#CHIEF] OnAfterTransportCancel + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + + --- Triggers the FSM event "OpsOnMission". + -- @function [parent=#CHIEF] OpsOnMission + -- @param #CHIEF self + -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "OpsOnMission" after a delay. + -- @function [parent=#CHIEF] __OpsOnMission + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "OpsOnMission" event. + -- @function [parent=#CHIEF] OnAfterOpsOnMission + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "ZoneCaptured". + -- @function [parent=#CHIEF] ZoneCaptured + -- @param #CHIEF self + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that was captured. + + --- Triggers the FSM event "ZoneCaptured" after a delay. + -- @function [parent=#CHIEF] __ZoneCaptured + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that was captured. + + --- On after "ZoneCaptured" event. + -- @function [parent=#CHIEF] OnAfterZoneCaptured + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that was captured. + + --- Triggers the FSM event "ZoneLost". + -- @function [parent=#CHIEF] ZoneLost + -- @param #CHIEF self + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that was lost. + + --- Triggers the FSM event "ZoneLost" after a delay. + -- @function [parent=#CHIEF] __ZoneLost + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that was lost. + + --- On after "ZoneLost" event. + -- @function [parent=#CHIEF] OnAfterZoneLost + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that was lost. + + --- Triggers the FSM event "ZoneEmpty". + -- @function [parent=#CHIEF] ZoneEmpty + -- @param #CHIEF self + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that is empty now. + + --- Triggers the FSM event "ZoneEmpty" after a delay. + -- @function [parent=#CHIEF] __ZoneEmpty + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that is empty now. + + --- On after "ZoneEmpty" event. + -- @function [parent=#CHIEF] OnAfterZoneEmpty + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that is empty now. + + --- Triggers the FSM event "ZoneAttacked". + -- @function [parent=#CHIEF] ZoneAttacked + -- @param #CHIEF self + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that is being attacked. + + --- Triggers the FSM event "ZoneAttacked" after a delay. + -- @function [parent=#CHIEF] __ZoneAttacked + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that is being attacked. + + --- On after "ZoneAttacked" event. + -- @function [parent=#CHIEF] OnAfterZoneAttacked + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that is being attacked. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set this to be an air-to-any dispatcher, i.e. engaging air, ground and naval targets. This is the default anyway. +-- @param #CHIEF self +-- @return #CHIEF self +function CHIEF:SetAirToAny() + + self:SetFilterCategory({}) + + return self +end + +--- Set this to be an air-to-air dispatcher. +-- @param #CHIEF self +-- @return #CHIEF self +function CHIEF:SetAirToAir() + + self:SetFilterCategory({Unit.Category.AIRPLANE, Unit.Category.HELICOPTER}) + + return self +end + +--- Set this to be an air-to-ground dispatcher, i.e. engage only ground units +-- @param #CHIEF self +-- @return #CHIEF self +function CHIEF:SetAirToGround() + + self:SetFilterCategory({Unit.Category.GROUND_UNIT}) + + return self +end + +--- Set this to be an air-to-sea dispatcher, i.e. engage only naval units. +-- @param #CHIEF self +-- @return #CHIEF self +function CHIEF:SetAirToSea() + + self:SetFilterCategory({Unit.Category.SHIP}) + + return self +end + +--- Set this to be an air-to-surface dispatcher, i.e. engaging ground and naval groups. +-- @param #CHIEF self +-- @return #CHIEF self +function CHIEF:SetAirToSurface() + + self:SetFilterCategory({Unit.Category.GROUND_UNIT, Unit.Category.SHIP}) + + return self +end + +--- Set a threat level range that will be engaged. Threat level is a number between 0 and 10, where 10 is a very dangerous threat. +-- Targets with threat level 0 are usually harmless. +-- @param #CHIEF self +-- @param #number ThreatLevelMin Min threat level. Default 1. +-- @param #number ThreatLevelMax Max threat level. Default 10. +-- @return #CHIEF self +function CHIEF:SetThreatLevelRange(ThreatLevelMin, ThreatLevelMax) + + self.threatLevelMin=ThreatLevelMin or 1 + self.threatLevelMax=ThreatLevelMax or 10 + + return self +end + +--- Set defence condition. +-- @param #CHIEF self +-- @param #string Defcon Defence condition. See @{#CHIEF.DEFCON}, e.g. `CHIEF.DEFCON.RED`. +-- @return #CHIEF self +function CHIEF:SetDefcon(Defcon) + + -- Check if valid string was passed. + local gotit=false + for _,defcon in pairs(CHIEF.DEFCON) do + if defcon==Defcon then + gotit=true + end + end + if not gotit then + self:E(self.lid..string.format("ERROR: Unknown DEFCON specified! Dont know defcon=%s", tostring(Defcon))) + return self + end + + -- Trigger event if defcon changed. + if Defcon~=self.Defcon then + self:DefconChange(Defcon) + end + + -- Set new DEFCON. + self.Defcon=Defcon + + return self +end + +--- Create a new resource list of required assets. +-- @param #CHIEF self +-- @param #string MissionType The mission type. +-- @param #number Nmin Min number of required assets. Default 1. +-- @param #number Nmax Max number of requried assets. Default 1. +-- @param #table Attributes Generalized attribute(s). Default `nil`. +-- @param #table Properties DCS attribute(s). Default `nil`. +-- @return #table The resource object. +function CHIEF:CreateResource(MissionType, Nmin, Nmax, Attributes, Properties) + + local resource={} + + self:AddToResource(resource, MissionType, Nmin, Nmax, Attributes, Properties) + + return resource +end + +--- Add mission type and number of required assets to resource. +-- @param #CHIEF self +-- @param #table Resource Resource table. +-- @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 #CHIEF self +function CHIEF:AddToResource(Resource, 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={} --#CHIEF.Resource + resource.MissionType=MissionType + resource.Nmin=Nmin or 1 + resource.Nmax=Nmax or 1 + resource.Attributes=Attributes or {} + resource.Properties=Properties or {} + + -- Add to table. + table.insert(Resource, resource) + + -- Debug output. + if self.verbose>10 then + local text="Resource:" + for _,_r in pairs(Resource) do + local r=_r --#CHIEF.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 self +end + +--- Delete mission type from resource list. All running missions are cancelled. +-- @param #CHIEF self +-- @param #table Resource Resource table. +-- @param #string MissionType Mission Type. +-- @return #CHIEF self +function CHIEF:DeleteFromResource(Resource, MissionType) + + for i=#Resource,1,-1 do + local resource=Resource[i] --#CHIEF.Resource + if resource.MissionType==MissionType then + if resource.mission and resource.mission:IsNotOver() then + resource.mission:Cancel() + end + table.remove(Resource, i) + end + end + + 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`. +function CHIEF:GetDefcon(Defcon) + return self.Defcon +end + +--- Set limit for number of total or specific missions to be executed simultaniously. +-- @param #CHIEF self +-- @param #number Limit Number of max. mission of this type. Default 10. +-- @param #string MissionType Type of mission, e.g. `AUFTRAG.Type.BAI`. Default `"Total"` for total number of missions. +-- @return #CHIEF self +function CHIEF:SetLimitMission(Limit, MissionType) + self.commander:SetLimitMission(Limit, MissionType) + return self +end + +--- Set tactical overview on. +-- @param #CHIEF self +-- @return #CHIEF self +function CHIEF:SetTacticalOverviewOn() + self.tacview=true + return self +end + +--- Set tactical overview off. +-- @param #CHIEF self +-- @return #CHIEF self +function CHIEF:SetTacticalOverviewOff() + self.tacview=false + return self +end + + +--- Set strategy. +-- @param #CHIEF self +-- @param #string Strategy Strategy. See @{#CHIEF.strategy}, e.g. `CHIEF.Strategy.DEFENSIVE` (default). +-- @return #CHIEF self +function CHIEF:SetStrategy(Strategy) + + -- Trigger event if Strategy changed. + if Strategy~=self.strategy then + self:StrategyChange(Strategy) + end + + -- Set new Strategy. + self.strategy=Strategy + + return self +end + +--- Get defence condition. +-- @param #CHIEF self +-- @param #string Current Defence condition. See @{#CHIEF.DEFCON}, e.g. `CHIEF.DEFCON.RED`. +function CHIEF:GetDefcon(Defcon) + return self.Defcon +end + + +--- Get the commander. +-- @param #CHIEF self +-- @return Ops.Commander#COMMANDER The commander. +function CHIEF:GetCommander() + return self.commander +end + + +--- Add an AIRWING to the chief's commander. +-- @param #CHIEF self +-- @param Ops.AirWing#AIRWING Airwing The airwing to add. +-- @return #CHIEF self +function CHIEF:AddAirwing(Airwing) + + -- Add airwing to the commander. + self:AddLegion(Airwing) + + return self +end + +--- Add a BRIGADE to the chief's commander. +-- @param #CHIEF self +-- @param Ops.Brigade#BRIGADE Brigade The brigade to add. +-- @return #CHIEF self +function CHIEF:AddBrigade(Brigade) + + -- Add brigade to the commander. + self:AddLegion(Brigade) + + return self +end + +--- Add a FLEET to the chief's commander. +-- @param #CHIEF self +-- @param Ops.Fleet#FLEET Fleet The fleet to add. +-- @return #CHIEF self +function CHIEF:AddFleet(Fleet) + + -- Add fleet to the commander. + self:AddLegion(Fleet) + + return self +end + +--- Add a LEGION to the chief's commander. +-- @param #CHIEF self +-- @param Ops.Legion#LEGION Legion The legion to add. +-- @return #CHIEF self +function CHIEF:AddLegion(Legion) + + -- Set chief of the legion. + Legion.chief=self + + -- Add legion to the commander. + self.commander:AddLegion(Legion) + + return self +end + + +--- Add mission to mission queue of the COMMANDER. +-- @param #CHIEF self +-- @param Ops.Auftrag#AUFTRAG Mission Mission to be added. +-- @return #CHIEF self +function CHIEF:AddMission(Mission) + + Mission.chief=self + + Mission.statusChief=AUFTRAG.Status.PLANNED + + self:I(self.lid..string.format("Adding mission #%d", Mission.auftragsnummer)) + + self.commander:AddMission(Mission) + + return self +end + +--- Remove mission from queue. +-- @param #CHIEF self +-- @param Ops.Auftrag#AUFTRAG Mission Mission to be removed. +-- @return #CHIEF self +function CHIEF:RemoveMission(Mission) + + Mission.chief=nil + + self.commander:RemoveMission(Mission) + + return self +end + +--- Add transport to transport queue of the COMMANDER. +-- @param #CHIEF self +-- @param Ops.OpsTransport#OPSTRANSPORT Transport Transport to be added. +-- @return #CHIEF self +function CHIEF:AddOpsTransport(Transport) + + Transport.chief=self + + self.commander:AddOpsTransport(Transport) + + return self +end + +--- Remove transport from queue. +-- @param #CHIEF self +-- @param Ops.OpsTransport#OPSTRANSPORT Transport Transport to be removed. +-- @return #CHIEF self +function CHIEF:RemoveTransport(Transport) + + Transport.chief=nil + + self.commander:RemoveTransport(Transport) + + return self +end + +--- Add target. +-- @param #CHIEF self +-- @param Ops.Target#TARGET Target Target object to be added. +-- @return #CHIEF self +function CHIEF:AddTarget(Target) + + if not self:IsTarget(Target) then + Target.chief=self + table.insert(self.targetqueue, Target) + end + + return self +end + +--- Check if a TARGET is already in the queue. +-- @param #CHIEF self +-- @param Ops.Target#TARGET Target Target object to be added. +-- @return #boolean If `true`, target exists in the target queue. +function CHIEF: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 #CHIEF self +-- @param Ops.Target#TARGET Target The target. +-- @return #CHIEF self +function CHIEF: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 strategically important zone. +-- By default two resource lists are created. One for the case that the zone is empty and the other for the case that the zone is occupied. +-- +-- Occupied: +-- +-- * `AUFTRAG.Type.ARTY` with Nmin=1, Nmax=2 +-- * `AUFTRAG.Type.CASENHANCED` with Nmin=1, Nmax=2 +-- +-- Empty: +-- +-- * `AUFTRAG.Type.ONGUARD` with Nmin=1 and Nmax=3 assets, Attribute=`GROUP.Attribute.GROUND_INFANTRY`. +-- * `AUFTRAG.Type.ONGURAD` with Nmin=1 and Nmax=1 assets, Attribute=`GROUP.Attribute.GROUND_TANK`. +-- +-- Resources can be created with the @{#CHIEF.CreateResource} and @{#CHIEF.AddToResource} functions. +-- +-- @param #CHIEF self +-- @param Ops.OpsZone#OPSZONE OpsZone OPS zone object. +-- @param #number Priority Priority. Default 50. +-- @param #number Importance Importance. Default nil. +-- @param #CHIEF.Resource ResourceOccupied (Optional) Resources used then zone is occupied by the enemy. +-- @param #CHIEF.Resource ResourceEmpty (Optional) Resources used then zone is empty. +-- @return #CHIEF.StrategicZone The strategic zone. +function CHIEF:AddStrategicZone(OpsZone, Priority, Importance, ResourceOccupied, ResourceEmpty) + + local stratzone={} --#CHIEF.StrategicZone + + stratzone.opszone=OpsZone + stratzone.prio=Priority or 50 + stratzone.importance=Importance + + stratzone.missions={} + + -- Start ops zone. + if OpsZone:IsStopped() then + OpsZone:Start() + end + + -- Add resources if zone is occupied. + if ResourceOccupied then + stratzone.resourceOccup=UTILS.DeepCopy(ResourceOccupied) + else + stratzone.resourceOccup=self:CreateResource(AUFTRAG.Type.ARTY, 1, 2) + self:AddToResource(stratzone.resourceOccup, AUFTRAG.Type.CASENHANCED, 1, 2) + end + + -- Add resources if zone is empty + if ResourceEmpty then + stratzone.resourceEmpty=UTILS.DeepCopy(ResourceEmpty) + else + stratzone.resourceEmpty=self:CreateResource(AUFTRAG.Type.ONGUARD, 1, 3, GROUP.Attribute.GROUND_INFANTRY) + self:AddToResource(stratzone.resourceEmpty, AUFTRAG.Type.ONGUARD, 1, 1, GROUP.Attribute.GROUND_TANK) + end + + -- Add to table. + table.insert(self.zonequeue, stratzone) + + -- Add chief so we get informed when something happens. + OpsZone:_AddChief(self) + + return stratzone +end + +--- Set the resource list of missions and assets employed when the zone is empty. +-- @param #CHIEF self +-- @param #CHIEF.StrategicZone StrategicZone The strategic zone. +-- @param #CHIEF.Resource Resource Resource list of missions and assets. +-- @param #boolean NoCopy If `true`, do **not** create a deep copy of the resource. +-- @return #CHIEF self +function CHIEF:SetStrategicZoneResourceEmpty(StrategicZone, Resource, NoCopy) + if NoCopy then + StrategicZone.resourceEmpty=Resource + else + StrategicZone.resourceEmpty=UTILS.DeepCopy(Resource) + end + return self +end + +--- Set the resource list of missions and assets employed when the zone is occupied by the enemy. +-- @param #CHIEF self +-- @param #CHIEF.StrategicZone StrategicZone The strategic zone. +-- @param #CHIEF.Resource Resource Resource list of missions and assets. +-- @param #boolean NoCopy If `true`, do **not** create a deep copy of the resource. +-- @return #CHIEF self +function CHIEF:SetStrategicZoneResourceOccupied(StrategicZone, Resource, NoCopy) + if NoCopy then + StrategicZone.resourceOccup=Resource + else + StrategicZone.resourceOccup=UTILS.DeepCopy(Resource) + end + return self +end + +--- Get the resource list of missions and assets employed when the zone is empty. +-- @param #CHIEF self +-- @param #CHIEF.StrategicZone StrategicZone The strategic zone. +-- @return #CHIEF.Resource Resource list of missions and assets. +function CHIEF:GetStrategicZoneResourceEmpty(StrategicZone) + return StrategicZone.resourceEmpty +end + +--- Get the resource list of missions and assets employed when the zone is occupied by the enemy. +-- @param #CHIEF self +-- @param #CHIEF.StrategicZone StrategicZone The strategic zone. +-- @return #CHIEF.Resource Resource list of missions and assets. +function CHIEF:GetStrategicZoneResourceOccupied(StrategicZone) + return StrategicZone.resourceOccup +end + + +--- Remove strategically important zone. All runing missions are cancelled. +-- @param #CHIEF self +-- @param Ops.OpsZone#OPSZONE OpsZone OPS zone object. +-- @param #number Delay Delay in seconds before the zone is removed. Default immidiately. +-- @return #CHIEF self +function CHIEF:RemoveStrategicZone(OpsZone, Delay) + + if Delay and Delay>0 then + -- Delayed call. + self:ScheduleOnce(Delay, CHIEF.RemoveStrategicZone, self, OpsZone) + else + + -- Loop over all zones in the queue. + for i=#self.zonequeue,1,-1 do + local stratzone=self.zonequeue[i] --#CHIEF.StrategicZone + + if OpsZone.zoneName==stratzone.opszone.zoneName then + + -- Debug info. + self:T(self.lid..string.format("Removing OPS zone \"%s\" from queue! All running missions will be cancelled", OpsZone.zoneName)) + + -- Cancel running missions. + for _,_resource in pairs(stratzone.resourceEmpty) do + local resource=_resource --#CHIEF.Resource + if resource.mission and resource.mission:IsNotOver() then + resource.mission:Cancel() + end + end + + -- Cancel running missions. + for _,_resource in pairs(stratzone.resourceOccup) do + local resource=_resource --#CHIEF.Resource + if resource.mission and resource.mission:IsNotOver() then + resource.mission:Cancel() + end + end + + -- Remove from table. + table.remove(self.zonequeue, i) + + -- Done! + return self + end + end + + end + + return self +end + +--- Add a rearming zone. +-- @param #CHIEF self +-- @param Core.Zone#ZONE RearmingZone Rearming zone. +-- @return Ops.Brigade#BRIGADE.SupplyZone The rearming zone data. +function CHIEF:AddRearmingZone(RearmingZone) + + -- Hand over to commander. + local supplyzone=self.commander:AddRearmingZone(RearmingZone) + + return supplyzone +end + +--- Add a refuelling zone. +-- @param #CHIEF self +-- @param Core.Zone#ZONE RefuellingZone Refuelling zone. +-- @return Ops.Brigade#BRIGADE.SupplyZone The refuelling zone data. +function CHIEF:AddRefuellingZone(RefuellingZone) + + -- Hand over to commander. + local supplyzone=self.commander:AddRefuellingZone(RefuellingZone) + + return supplyzone +end + +--- Add a CAP zone. Flights will engage detected targets inside this zone. +-- @param #CHIEF self +-- @param Core.Zone#ZONE Zone CAP Zone. Has to be a circular zone. +-- @param #number Altitude Orbit altitude in feet. Default is 12,0000 feet. +-- @param #number Speed Orbit speed in KIAS. Default 350 kts. +-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West). +-- @param #number Leg Length of race-track in NM. Default 30 NM. +-- @return Ops.AirWing#AIRWING.PatrolZone The CAP zone data. +function CHIEF:AddCapZone(Zone, Altitude, Speed, Heading, Leg) + + -- Hand over to commander. + local zone=self.commander:AddCapZone(Zone, Altitude, Speed, Heading, Leg) + + return zone +end + +--- Add a GCI CAP. +-- @param #CHIEF self +-- @param Core.Zone#ZONE Zone Zone, where the flight orbits. +-- @param #number Altitude Orbit altitude in feet. Default is 12,0000 feet. +-- @param #number Speed Orbit speed in KIAS. Default 350 kts. +-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West). +-- @param #number Leg Length of race-track in NM. Default 30 NM. +-- @return Ops.AirWing#AIRWING.PatrolZone The CAP zone data. +function CHIEF:AddGciCapZone(Zone, Altitude, Speed, Heading, Leg) + + -- Hand over to commander. + local zone=self.commander:AddGciCapZone(Zone, Altitude, Speed, Heading, Leg) + + return zone +end + +--- Add an AWACS zone. +-- @param #CHIEF self +-- @param Core.Zone#ZONE Zone Zone. +-- @param #number Altitude Orbit altitude in feet. Default is 12,0000 feet. +-- @param #number Speed Orbit speed in KIAS. Default 350 kts. +-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West). +-- @param #number Leg Length of race-track in NM. Default 30 NM. +-- @return Ops.AirWing#AIRWING.PatrolZone The AWACS zone data. +function CHIEF:AddAwacsZone(Zone, Altitude, Speed, Heading, Leg) + + -- Hand over to commander. + local zone=self.commander:AddAwacsZone(Zone, Altitude, Speed, Heading, Leg) + + return zone +end + +--- Add a refuelling tanker zone. +-- @param #CHIEF self +-- @param Core.Zone#ZONE Zone Zone. +-- @param #number Altitude Orbit altitude in feet. Default is 12,0000 feet. +-- @param #number Speed Orbit speed in KIAS. Default 350 kts. +-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West). +-- @param #number Leg Length of race-track in NM. Default 30 NM. +-- @param #number RefuelSystem Refuelling system. +-- @return Ops.AirWing#AIRWING.TankerZone The tanker zone data. +function CHIEF:AddTankerZone(Zone, Altitude, Speed, Heading, Leg, RefuelSystem) + + -- Hand over to commander. + local zone=self.commander:AddTankerZone(Zone, Altitude, Speed, Heading, Leg, RefuelSystem) + + return zone +end + + +--- Set border zone set, defining your territory. +-- +-- * Detected enemy troops in these zones will trigger defence condition `RED`. +-- * Enemies in these zones will only be engaged if strategy is at least `CHIEF.STRATEGY.DEFENSIVE`. +-- +-- @param #CHIEF self +-- @param Core.Set#SET_ZONE BorderZoneSet Set of zones, defining our borders. +-- @return #CHIEF self +function CHIEF:SetBorderZones(BorderZoneSet) + + -- Border zones. + self.borderzoneset=BorderZoneSet or SET_ZONE:New() + + return self +end + +--- Add a zone defining your territory. +-- +-- * Detected enemy troops in these zones will trigger defence condition `RED`. +-- * Enemies in these zones will only be engaged if strategy is at least `CHIEF.STRATEGY.DEFENSIVE`. +-- +-- @param #CHIEF self +-- @param Core.Zone#ZONE Zone The zone. +-- @return #CHIEF self +function CHIEF:AddBorderZone(Zone) + + -- Add a border zone. + self.borderzoneset:AddZone(Zone) + + return self +end + +--- Set conflict zone set. +-- +-- * Detected enemy troops in these zones will trigger defence condition `YELLOW`. +-- * Enemies in these zones will only be engaged if strategy is at least `CHIEF.STRATEGY.OFFENSIVE`. +-- +-- @param #CHIEF self +-- @param Core.Set#SET_ZONE ZoneSet Set of zones. +-- @return #CHIEF self +function CHIEF:SetConflictZones(ZoneSet) + + -- Conflict zones. + self.yellowzoneset=ZoneSet or SET_ZONE:New() + + return self +end + +--- Add a conflict zone. +-- +-- * Detected enemy troops in these zones will trigger defence condition `YELLOW`. +-- * Enemies in these zones will only be engaged if strategy is at least `CHIEF.STRATEGY.OFFENSIVE`. +-- +-- @param #CHIEF self +-- @param Core.Zone#ZONE Zone The zone to add. +-- @return #CHIEF self +function CHIEF:AddConflictZone(Zone) + + -- Add a conflict zone. + self.yellowzoneset:AddZone(Zone) + + return self +end + +--- Set attack zone set. +-- +-- * Enemies in these zones will only be engaged if strategy is at least `CHIEF.STRATEGY.AGGRESSIVE`. +-- +-- @param #CHIEF self +-- @param Core.Set#SET_ZONE ZoneSet Set of zones. +-- @return #CHIEF self +function CHIEF:SetAttackZones(ZoneSet) + + -- Attacak zones. + self.engagezoneset=ZoneSet or SET_ZONE:New() + + return self +end + +--- Add an attack zone. +-- +-- * Enemies in these zones will only be engaged if strategy is at least `CHIEF.STRATEGY.AGGRESSIVE`. +-- +-- @param #CHIEF self +-- @param Core.Zone#ZONE Zone The zone to add. +-- @return #CHIEF self +function CHIEF:AddAttackZone(Zone) + + -- Add an attack zone. + self.engagezoneset:AddZone(Zone) + + return self +end + +--- Allow chief to use GROUND units for transport tasks. Helicopters are still preferred, and be aware there's no check as of now +-- if a destination can be reached on land. +-- @param #CHIEF self +-- @return #CHIEF self +function CHIEF:AllowGroundTransport() + self.TransportCategories = {Group.Category.GROUND, Group.Category.HELICOPTER} + return self +end + +--- Forbid chief to use GROUND units for transport tasks. Restrict to Helicopters. This is the default +-- @param #CHIEF self +-- @return #CHIEF self +function CHIEF:ForbidGroundTransport() + self.TransportCategories = {Group.Category.HELICOPTER} + return self +end + +--- Check if current strategy is passive. +-- @param #CHIEF self +-- @return #boolean If `true`, strategy is passive. +function CHIEF:IsPassive() + return self.strategy==CHIEF.Strategy.PASSIVE +end + +--- Check if current strategy is defensive. +-- @param #CHIEF self +-- @return #boolean If `true`, strategy is defensive. +function CHIEF:IsDefensive() + return self.strategy==CHIEF.Strategy.DEFENSIVE +end + +--- Check if current strategy is offensive. +-- @param #CHIEF self +-- @return #boolean If `true`, strategy is offensive. +function CHIEF:IsOffensive() + return self.strategy==CHIEF.Strategy.OFFENSIVE +end + +--- Check if current strategy is aggressive. +-- @param #CHIEF self +-- @return #boolean If `true`, strategy is agressive. +function CHIEF:IsAgressive() + return self.strategy==CHIEF.Strategy.AGGRESSIVE +end + +--- Check if current strategy is total war. +-- @param #CHIEF self +-- @return #boolean If `true`, strategy is total war. +function CHIEF:IsTotalWar() + return self.strategy==CHIEF.Strategy.TOTALWAR +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Start & Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. +-- @param #CHIEF self +-- @param Wrapper.Group#GROUP Group Flight group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function CHIEF:onafterStart(From, Event, To) + + -- Short info. + local text=string.format("Starting Chief of Staff") + self:I(self.lid..text) + + -- Start parent INTEL. + self:GetParent(self).onafterStart(self, From, Event, To) + + -- Start commander. + if self.commander then + if self.commander:GetState()=="NotReadyYet" then + self.commander:Start() + end + end + +end + +--- On after "Status" event. +-- @param #CHIEF self +-- @param Wrapper.Group#GROUP Group Flight group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function CHIEF:onafterStatus(From, Event, To) + + -- Start parent INTEL. + self:GetParent(self).onafterStatus(self, From, Event, To) + + -- FSM state. + local fsmstate=self:GetState() + + --- + -- CONTACTS: Mission Cleanup + --- + + -- Clean up missions where the contact was lost. + for _,_contact in pairs(self.ContactsLost) do + local contact=_contact --Ops.Intelligence#INTEL.Contact + + if contact.mission and contact.mission:IsNotOver() then + + -- Debug info. + local text=string.format("Lost contact to target %s! %s mission %s will be cancelled.", contact.groupname, contact.mission.type:upper(), contact.mission.name) + MESSAGE:New(text, 120, "CHIEF"):ToAll() + self:T(self.lid..text) + + -- Cancel this mission. + contact.mission:Cancel() + + end + + -- Remove a target from the queue. + if contact.target then + self:RemoveTarget(contact.target) + end + + end + + --- + -- CONTACTS: Create new TARGETS + --- + + -- Create TARGETs for all new contacts. + self.Nborder=0 ; self.Nconflict=0 ; self.Nattack=0 + for _,_contact in pairs(self.Contacts) do + local contact=_contact --Ops.Intelligence#INTEL.Contact + local group=contact.group --Wrapper.Group#GROUP + + -- Check if contact inside of our borders. + local inred=self:CheckGroupInBorder(group) + if inred then + self.Nborder=self.Nborder+1 + end + + -- Check if contact is in the conflict zones. + local inyellow=self:CheckGroupInConflict(group) + if inyellow then + self.Nconflict=self.Nconflict+1 + end + + -- Check if contact is in the attack zones. + local inattack=self:CheckGroupInAttack(group) + if inattack then + self.Nattack=self.Nattack+1 + end + + + -- Check if this is not already a target. + if not contact.target then + + -- Create a new TARGET of the contact group. + local Target=TARGET:New(contact.group) + + -- Set to contact. + contact.target=Target + + -- Set contact to target. Might be handy. + Target.contact=contact + + -- Add target to queue. + self:AddTarget(Target) + + end + + end + + + + --- + -- Defcon + --- + + -- TODO: Need to introduce time check to avoid fast oscillation between different defcon states in case groups move in and out of the zones. + if self.Nborder>0 then + self:SetDefcon(CHIEF.DEFCON.RED) + elseif self.Nconflict>0 then + self:SetDefcon(CHIEF.DEFCON.YELLOW) + else + self:SetDefcon(CHIEF.DEFCON.GREEN) + end + + --- + -- Check Target Queue + --- + + -- Check target queue and assign missions to new targets. + self:CheckTargetQueue() + + -- Loop over targets. + for _,_target in pairs(self.targetqueue) do + local target=_target --Ops.Target#TARGET + + if target and target:IsAlive() and target.chief and target.mission and target.mission:IsNotOver() then + + local inborder=self:CheckTargetInZones(target, self.borderzoneset) + + local inyellow=self:CheckTargetInZones(target, self.yellowzoneset) + + local inattack=self:CheckTargetInZones(target, self.engagezoneset) + + if self.strategy==CHIEF.Strategy.PASSIVE then + + -- Passive: No targets are engaged at all. + self:T(self.lid..string.format("Cancelling mission for target %s as strategy is PASSIVE", target:GetName())) + target.mission:Cancel() + + elseif self.strategy==CHIEF.Strategy.DEFENSIVE then + + -- Defensive: Cancel if not in border. + if not inborder then + self:T(self.lid..string.format("Cancelling mission for target %s as strategy is DEFENSIVE and not inside border", target:GetName())) + target.mission:Cancel() + end + + elseif self.strategy==CHIEF.Strategy.OFFENSIVE then + + -- Offensive: Cancel if not in border or conflict. + if not (inborder or inyellow) then + self:T(self.lid..string.format("Cancelling mission for target %s as strategy is OFFENSIVE and not inside border or conflict", target:GetName())) + target.mission:Cancel() + end + + elseif self.strategy==CHIEF.Strategy.AGGRESSIVE then + + -- Aggessive: Cancel if not in border, conflict or attack. + if not (inborder or inyellow or inattack) then + self:T(self.lid..string.format("Cancelling mission for target %s as strategy is AGGRESSIVE and not inside border, conflict or attack", target:GetName())) + target.mission:Cancel() + end + + elseif self.strategy==CHIEF.Strategy.TOTALWAR then + + -- Total War: No missions are cancelled. + + end + + end + + end + + --- + -- Check Strategic Zone Queue + --- + + -- Check target queue and assign missions to new targets. + self:CheckOpsZoneQueue() + + + -- Display tactival overview. + self:_TacticalOverview() + + --- + -- Info General + --- + + if self.verbose>=1 then + local Nassets=self.commander:CountAssets() + local Ncontacts=#self.Contacts + local Nmissions=#self.commander.missionqueue + local Ntargets=#self.targetqueue + + -- Info message + local text=string.format("Defcon=%s Strategy=%s: Assets=%d, Contacts=%d [Border=%d, Conflict=%d, Attack=%d], Targets=%d, Missions=%d", + self.Defcon, self.strategy, Nassets, Ncontacts, self.Nborder, self.Nconflict, self.Nattack, Ntargets, Nmissions) + self:I(self.lid..text) + + end + + --- + -- Info Contacts + --- + + -- Info about contacts. + if self.verbose>=2 and #self.Contacts>0 then + local text="Contacts:" + for i,_contact in pairs(self.Contacts) do + local contact=_contact --Ops.Intelligence#INTEL.Contact + + local mtext="N/A" + if contact.mission then + mtext=string.format("\"%s\" [%s] %s", contact.mission:GetName(), contact.mission:GetType(), contact.mission.status:upper()) + end + text=text..string.format("\n[%d] %s Type=%s (%s): Threat=%d Mission=%s", i, contact.groupname, contact.categoryname, contact.typename, contact.threatlevel, mtext) + end + self:I(self.lid..text) + end + + --- + -- Info Targets + --- + + if self.verbose>=3 and #self.targetqueue>0 then + local text="Targets:" + for i,_target in pairs(self.targetqueue) do + local target=_target --Ops.Target#TARGET + + local mtext="N/A" + if target.mission then + mtext=string.format("\"%s\" [%s] %s", target.mission:GetName(), target.mission:GetType(), target.mission.status:upper()) + end + text=text..string.format("\n[%d] %s: Category=%s, prio=%d, importance=%d, alive=%s [%.1f/%.1f], Mission=%s", + i, target:GetName(), target.category, target.prio, target.importance or -1, tostring(target:IsAlive()), target:GetLife(), target:GetLife0(), mtext) + end + self:I(self.lid..text) + end + + --- + -- Info Missions + --- + + -- Mission queue. + if self.verbose>=4 and #self.commander.missionqueue>0 then + local text="Mission queue:" + for i,_mission in pairs(self.commander.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + local target=mission:GetTargetName() or "unknown" + + text=text..string.format("\n[%d] %s (%s): status=%s, target=%s", i, mission.name, mission.type, mission.status, target) + end + self:I(self.lid..text) + end + + --- + -- Info Strategic Zones + --- + + -- Loop over targets. + if self.verbose>=4 and #self.zonequeue>0 then + local text="Zone queue:" + for i,_stratzone in pairs(self.zonequeue) do + local stratzone=_stratzone --#CHIEF.StrategicZone + + -- OPS zone object. + local opszone=stratzone.opszone + + local owner=UTILS.GetCoalitionName(opszone.ownerCurrent) + local prevowner=UTILS.GetCoalitionName(opszone.ownerPrevious) + + text=text..string.format("\n[%d] %s [%s]: owner=%s [%s] (prio=%d, importance=%s): Blue=%d, Red=%d, Neutral=%d", + i, opszone.zone:GetName(), opszone:GetState(), owner, prevowner, stratzone.prio, tostring(stratzone.importance), opszone.Nblu, opszone.Nred, opszone.Nnut) + + end + self:I(self.lid..text) + end + + + --- + -- Info Assets + --- + + if self.verbose>=5 then + local text="Assets:" + for _,missiontype in pairs(AUFTRAG.Type) do + local N=self.commander:CountAssets(nil, missiontype) + if N>0 then + text=text..string.format("\n- %s: %d", missiontype, N) + end + end + self:I(self.lid..text) + + local text="Assets:" + for _,attribute in pairs(WAREHOUSE.Attribute) do + local N=self.commander:CountAssets(nil, nil, attribute) + if N>0 or self.verbose>=10 then + text=text..string.format("\n- %s: %d", attribute, N) + end + end + self:T(self.lid..text) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Events +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "MissionAssignToAny" event. +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Legions The Legion(s) to which the mission is assigned. +function CHIEF:onafterMissionAssign(From, Event, To, Mission, Legions) + + if self.commander then + self:T(self.lid..string.format("Assigning mission %s (%s) to COMMANDER", Mission.name, Mission.type)) + Mission.chief=self + Mission.statusChief=AUFTRAG.Status.QUEUED + self.commander:MissionAssign(Mission, Legions) + else + self:E(self.lid..string.format("Mission cannot be assigned as no COMMANDER is defined!")) + end + +end + +--- On after "MissionCancel" event. +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +function CHIEF:onafterMissionCancel(From, Event, To, Mission) + + -- Debug info. + self:T(self.lid..string.format("Cancelling mission %s (%s) in status %s", Mission.name, Mission.type, Mission.status)) + + -- Set status to CANCELLED. + Mission.statusChief=AUFTRAG.Status.CANCELLED + + if Mission:IsPlanned() then + + -- Mission is still in planning stage. Should not have any LEGIONS assigned ==> Just remove it form the COMMANDER queue. + self:RemoveMission(Mission) + + else + + -- COMMANDER will cancel mission. + if Mission.commander then + Mission.commander:MissionCancel(Mission) + end + + end + +end + +--- On after "TransportCancel" event. +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. +function CHIEF:onafterTransportCancel(From, Event, To, Transport) + + -- Debug info. + self:T(self.lid..string.format("Cancelling transport UID=%d in status %s", Transport.uid, Transport:GetState())) + + if Transport:IsPlanned() then + + -- Mission is still in planning stage. Should not have any LEGIONS assigned ==> Just remove it form the COMMANDER queue. + self:RemoveTransport(Transport) + + else + + -- COMMANDER will cancel mission. + if Transport.commander then + Transport.commander:TransportCancel(Transport) + end + + end + +end + +--- On after "DefconChange" event. +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string Defcon New defence condition. +function CHIEF:onafterDefconChange(From, Event, To, Defcon) + self:T(self.lid..string.format("Changing Defcon from %s --> %s", self.Defcon, Defcon)) +end + +--- On after "StrategyChange" event. +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string Strategy +function CHIEF:onafterStrategyChange(From, Event, To, Strategy) + self:T(self.lid..string.format("Changing Strategy from %s --> %s", self.strategy, Strategy)) +end + +--- On after "OpsOnMission". +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP OpsGroup Ops group on mission +-- @param Ops.Auftrag#AUFTRAG Mission The requested mission. +function CHIEF:onafterOpsOnMission(From, Event, To, OpsGroup, Mission) + -- Debug info. + self:T(self.lid..string.format("Group %s on mission %s [%s]", OpsGroup:GetName(), Mission:GetName(), Mission:GetType())) +end + + +--- On after "ZoneCaptured". +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsZone#OPSZONE OpsZone The zone that was captured by us. +function CHIEF:onafterZoneCaptured(From, Event, To, OpsZone) + -- Debug info. + self:T(self.lid..string.format("Zone %s captured!", OpsZone:GetName())) +end + + +--- On after "ZoneLost". +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsZone#OPSZONE OpsZone The zone that was lost. +function CHIEF:onafterZoneLost(From, Event, To, OpsZone) + -- Debug info. + self:T(self.lid..string.format("Zone %s lost!", OpsZone:GetName())) +end + +--- On after "ZoneEmpty". +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsZone#OPSZONE OpsZone The zone that is empty now. +function CHIEF:onafterZoneEmpty(From, Event, To, OpsZone) + -- Debug info. + self:T(self.lid..string.format("Zone %s empty!", OpsZone:GetName())) +end + +--- On after "ZoneAttacked". +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsZone#OPSZONE OpsZone The zone that being attacked. +function CHIEF:onafterZoneAttacked(From, Event, To, OpsZone) + -- Debug info. + self:T(self.lid..string.format("Zone %s attacked!", OpsZone:GetName())) +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Target Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Display tactical overview. +-- @param #CHIEF self +function CHIEF:_TacticalOverview() + + if self.tacview then + + local NassetsTotal=self.commander:CountAssets() + local NassetsStock=self.commander:CountAssets(true) + local Ncontacts=#self.Contacts + local NmissionsTotal=#self.commander.missionqueue + local NmissionsRunni=self.commander:CountMissions(AUFTRAG.Type, true) + local Ntargets=#self.targetqueue + local Nzones=#self.zonequeue + local Nagents=self.detectionset:CountAlive() + + -- Info message + local text=string.format("Tactical Overview\n") + text=text..string.format("=================\n") + + -- Strategy and defcon info. + text=text..string.format("Strategy: %s - Defcon: %s - Agents=%s\n", self.strategy, self.Defcon, Nagents) + + -- Contact info. + text=text..string.format("Contacts: %d [Border=%d, Conflict=%d, Attack=%d]\n", Ncontacts, self.Nborder, self.Nconflict, self.Nattack) + + -- Asset info. + text=text..string.format("Assets: %d [Active=%d, Stock=%d]\n", NassetsTotal, NassetsTotal-NassetsStock, NassetsStock) + + -- Target info. + text=text..string.format("Targets: %d\n", Ntargets) + + -- Mission info. + text=text..string.format("Missions: %d [Running=%d/%d - Success=%d, Failure=%d]\n", NmissionsTotal, NmissionsRunni, self:GetMissionLimit("Total"), self.Nsuccess, self.Nfailure) + for _,mtype in pairs(AUFTRAG.Type) do + local n=self.commander:CountMissions(mtype) + if n>0 then + local N=self.commander:CountMissions(mtype, true) + local limit=self:GetMissionLimit(mtype) + text=text..string.format(" - %s: %d [Running=%d/%d]\n", mtype, n, N, limit) + end + end + + -- Strategic zone info. + text=text..string.format("Strategic Zones: %d\n", Nzones) + for _,_stratzone in pairs(self.zonequeue) do + local stratzone=_stratzone --#CHIEF.StrategicZone + local owner=stratzone.opszone:GetOwnerName() + text=text..string.format(" - %s: %s - %s [I=%d, P=%d]\n", stratzone.opszone:GetName(), owner, stratzone.opszone:GetState(), stratzone.importance, stratzone.prio) + end + + -- Message to coalition. + MESSAGE:New(text, 60, nil, true):ToCoalition(self.coalition) + + -- Output to log. + if self.verbose>=4 then + self:I(self.lid..text) + end + + end + +end + + +--- Check target queue and assign ONE valid target by adding it to the mission queue of the COMMANDER. +-- @param #CHIEF self +function CHIEF:CheckTargetQueue() + + -- Number of missions. + local Ntargets=#self.targetqueue + + -- Treat special cases. + if Ntargets==0 then + return nil + end + + -- Check if total number of missions is reached. + local NoLimit=self:_CheckMissionLimit("Total") + --env.info("FF chief total nolimit="..tostring(NoLimit)) + 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=self.threatLevelMin and threatlevel<=self.threatLevelMax + + -- Airbases, Zones and Coordinates have threat level 0. We consider them threads independent of min/max threat level set. + if target.category==TARGET.Category.AIRBASE or target.category==TARGET.Category.ZONE or target.Category==TARGET.Category.COORDINATE then + isThreat=true + end + + -- Debug message. + local text=string.format("Target %s: Alive=%s, Threat=%s, Important=%s", target:GetName(), tostring(isAlive), tostring(isThreat), tostring(isImportant)) + + -- Check if mission is done. + if target.mission then + text=text..string.format(", Mission \"%s\" (%s) [%s]", target.mission:GetName(), target.mission:GetState(), target.mission:GetType()) + if target.mission:IsOver() then + text=text..string.format(" - DONE ==> removing mission") + target.mission=nil + end + else + text=text..string.format(", NO mission yet") + end + self:T2(self.lid..text) + + -- Check that target is alive and not already a mission has been assigned. + if isAlive and isThreat and isImportant and not target.mission then + + -- Check if this target is "valid", i.e. fits with the current strategy. + local valid=false + if self.strategy==CHIEF.Strategy.PASSIVE then + + --- + -- PASSIVE: No targets at all are attacked. + --- + + valid=false + + elseif self.strategy==CHIEF.Strategy.DEFENSIVE then + + --- + -- DEFENSIVE: Attack inside borders only. + --- + + if self:CheckTargetInZones(target, self.borderzoneset) then + valid=true + end + + elseif self.strategy==CHIEF.Strategy.OFFENSIVE then + + --- + -- OFFENSIVE: Attack inside borders and in yellow zones. + --- + + if self:CheckTargetInZones(target, self.borderzoneset) or self:CheckTargetInZones(target, self.yellowzoneset) then + valid=true + end + + elseif self.strategy==CHIEF.Strategy.AGGRESSIVE then + + --- + -- AGGRESSIVE: Attack in all zone sets. + --- + + if self:CheckTargetInZones(target, self.borderzoneset) or self:CheckTargetInZones(target, self.yellowzoneset) or self:CheckTargetInZones(target, self.engagezoneset) then + valid=true + end + + elseif self.strategy==CHIEF.Strategy.TOTALWAR then + + --- + -- TOTAL WAR: We attack anything we find. + --- + + valid=true + end + + -- Valid target? + if valid then + + -- Debug info. + self:T(self.lid..string.format("Got valid target %s: category=%s, threatlevel=%d", target:GetName(), target.category, threatlevel)) + + -- Get mission performances for the given target. + local MissionPerformances=self:_GetMissionPerformanceFromTarget(target) + + -- Mission. + local mission=nil --Ops.Auftrag#AUFTRAG + local Legions=nil + + if #MissionPerformances>0 then + + for _,_mp in pairs(MissionPerformances) do + local mp=_mp --#CHIEF.MissionPerformance + + -- Check mission type limit. + local notlimited=self:_CheckMissionLimit(mp.MissionType) + + --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.commander:RecruitAssetsForTarget(target, mp.MissionType, NassetsMin, NassetsMax) + + if recruited then + + self:T(self.lid..string.format("Recruited %d assets for mission type %s [performance=%d] of target %s", #assets, mp.MissionType, mp.Performance, target:GetName())) + + -- Create a mission. + mission=AUFTRAG:NewFromTarget(target, mp.MissionType) + + -- Add asset to mission. + if mission then + + mission:_AddAssets(assets) + Legions=legions + + -- We got what we wanted ==> leave loop. + break + end + else + self:T(self.lid..string.format("Could NOT recruit assets for mission type %s [performance=%d] of target %s", mp.MissionType, mp.Performance, target:GetName())) + end + end + end + end + + -- Check if mission could be defined. + if mission and Legions then + + -- Set target mission entry. + target.mission=mission + + -- Mission parameters. + mission.prio=target.prio + mission.importance=target.importance + + -- Assign mission to legions. + self:MissionAssign(mission, Legions) + + -- Only ONE target is assigned per check. + return + end + + end + + end + end + +end + +--- Check if limit of missions has been reached. +-- @param #CHIEF self +-- @param #string MissionType Type of mission. +-- @return #boolean If `true`, mission limit has **not** been reached. If `false`, limit has been reached. +function CHIEF:_CheckMissionLimit(MissionType) + return self.commander:_CheckMissionLimit(MissionType) +end + +--- Get mission limit. +-- @param #CHIEF self +-- @param #string MissionType Type of mission. +-- @return #number Limit. Unlimited mission types are returned as 999. +function CHIEF:GetMissionLimit(MissionType) + local l=self.commander.limitMission[MissionType] + if not l then + l=999 + end + return l +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Strategic Zone Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check strategic zone queue. +-- @param #CHIEF self +function CHIEF:CheckOpsZoneQueue() + + -- Number of zones. + local Nzones=#self.zonequeue + + -- Treat special cases. + if Nzones==0 then + return nil + end + + -- Loop over strategic zone and remove stopped zones. + for i=Nzones, 1, -1 do + local stratzone=self.zonequeue[i] --#CHIEF.StrategicZone + if stratzone.opszone:IsStopped() then + self:RemoveStrategicZone(stratzone.opszone) + end + end + + -- Loop over strategic zones and cancel missions for occupied zones if zone is not occupied any more. + for _,_startzone in pairs(self.zonequeue) do + local stratzone=_startzone --#CHIEF.StrategicZone + + -- Current owner of the zone. + local ownercoalition=stratzone.opszone:GetOwner() + + -- Check if we own the zone or it is empty. + if ownercoalition==self.coalition or stratzone.opszone:IsEmpty() then + + -- Loop over resources. + for _,_resource in pairs(stratzone.resourceOccup or {}) do + local resource=_resource --#CHIEF.Resource + + -- Cancel running missions. + if resource.mission then + resource.mission:Cancel() + end + + end + end + end + + -- Passive strategy ==> Do not act. + if self:IsPassive() then + return + 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. + local function _sort(a, b) + local taskA=a --#CHIEF.StrategicZone + local taskB=b --#CHIEF.StrategicZone + return (taskA.prio Recruiting for mission type %s: Nmin=%d, Nmax=%d", zoneName, missionType, resource.Nmin, resource.Nmax)) + + -- Recruit assets. + local recruited=self:RecruitAssetsForZone(stratzone, resource) + + if recruited then + self:T(self.lid..string.format("Successfully recruited assets for empty zone \"%s\" [mission type=%s]", zoneName, missionType)) + else + self:T(self.lid..string.format("Could not recruited assets for empty zone \"%s\" [mission type=%s]", zoneName, missionType)) + end + + end + + end + + else + + --- + -- Zone is NOT EMPTY + -- + -- We first send a CAS flight to eliminate enemy activity. + --- + + for _,_resource in pairs(stratzone.resourceOccup or {}) do + local resource=_resource --#CHIEF.Resource + + -- Mission type. + local missionType=resource.MissionType + + if (not resource.mission) or resource.mission:IsOver() then + + -- Debug info. + self:T2(self.lid..string.format("Zone %s is NOT empty ==> Recruiting for mission type %s: Nmin=%d, Nmax=%d", zoneName, missionType, resource.Nmin, resource.Nmax)) + + -- Recruit assets. + local recruited=self:RecruitAssetsForZone(stratzone, resource) + + if recruited then + self:T(self.lid..string.format("Successfully recruited assets for occupied zone %s, mission type=%s", zoneName, missionType)) + else + self:T(self.lid..string.format("Could not recruited assets for occupied zone %s, mission type=%s", zoneName, missionType)) + end + + end + + end + + end + + end + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Zone Check Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check if group is inside our border. +-- @param #CHIEF self +-- @param Wrapper.Group#GROUP group The group. +-- @return #boolean If true, group is in any border zone. +function CHIEF:CheckGroupInBorder(group) + + local inside=self:CheckGroupInZones(group, self.borderzoneset) + + return inside +end + +--- Check if group is in a conflict zone. +-- @param #CHIEF self +-- @param Wrapper.Group#GROUP group The group. +-- @return #boolean If true, group is in any conflict zone. +function CHIEF:CheckGroupInConflict(group) + + -- Check inside yellow but not inside our border. + local inside=self:CheckGroupInZones(group, self.yellowzoneset) --and not self:CheckGroupInZones(group, self.borderzoneset) + + return inside +end + +--- Check if group is in a attack zone. +-- @param #CHIEF self +-- @param Wrapper.Group#GROUP group The group. +-- @return #boolean If true, group is in any attack zone. +function CHIEF:CheckGroupInAttack(group) + + -- Check inside yellow but not inside our border. + local inside=self:CheckGroupInZones(group, self.engagezoneset) --and not self:CheckGroupInZones(group, self.borderzoneset) + + return inside +end + +--- Check if group is inside a zone. +-- @param #CHIEF self +-- @param Wrapper.Group#GROUP group The group. +-- @param Core.Set#SET_ZONE zoneset Set of zones. +-- @return #boolean If true, group is in any zone. +function CHIEF:CheckGroupInZones(group, zoneset) + + for _,_zone in pairs(zoneset.Set or {}) do + local zone=_zone --Core.Zone#ZONE + + if group:IsInZone(zone) then + return true + end + end + + return false +end + +--- Check if group is inside a zone. +-- @param #CHIEF self +-- @param Ops.Target#TARGET target The target. +-- @param Core.Set#SET_ZONE zoneset Set of zones. +-- @return #boolean If true, group is in any zone. +function CHIEF:CheckTargetInZones(target, zoneset) + + for _,_zone in pairs(zoneset.Set or {}) do + local zone=_zone --Core.Zone#ZONE + + if zone:IsCoordinateInZone(target:GetCoordinate()) then + return true + end + end + + return false +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Resources +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a mission performance table. +-- @param #CHIEF self +-- @param #string MissionType Mission type. +-- @param #number Performance Performance. +-- @return #CHIEF.MissionPerformance Mission performance. +function CHIEF:_CreateMissionPerformance(MissionType, Performance) + local mp={} --#CHIEF.MissionPerformance + mp.MissionType=MissionType + mp.Performance=Performance + return mp +end + +--- Get mission performance for a given TARGET. +-- @param #CHIEF self +-- @param Ops.Target#TARGET Target The target. +-- @return #table Mission performances of type `#CHIEF.MissionPerformance`. +function CHIEF:_GetMissionPerformanceFromTarget(Target) + + -- Possible target objects. + local group=nil --Wrapper.Group#GROUP + local airbase=nil --Wrapper.Airbase#AIRBASE + local scenery=nil --Wrapper.Scenery#SCENERY + local static=nil --Wrapper.Static#STATIC + local coordinate=nil --Core.Point#COORDINATE + + -- Get target objective. + local target=Target:GetObject() + + if target:IsInstanceOf("GROUP") then + group=target --Target is already a group. + elseif target:IsInstanceOf("UNIT") then + group=target:GetGroup() + elseif target:IsInstanceOf("AIRBASE") then + airbase=target + elseif target:IsInstanceOf("STATIC") then + static=target + elseif target:IsInstanceOf("SCENERY") then + scenery=target + end + + -- Target category. + local TargetCategory=Target:GetCategory() + + -- Mission performances. + local missionperf={} --#CHIEF.MissionPerformance + + if group then + + local category=group:GetCategory() + local attribute=group:GetAttribute() + + if category==Group.Category.AIRPLANE or category==Group.Category.HELICOPTER then + + --- + -- A2A: Intercept + --- + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.INTERCEPT, 100)) + + elseif category==Group.Category.GROUND or category==Group.Category.TRAIN then + + --- + -- GROUND + --- + + if attribute==GROUP.Attribute.GROUND_SAM then + + -- SEAD/DEAD + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.SEAD, 100)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.GROUNDATTACK, 50)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30)) + + elseif attribute==GROUP.Attribute.GROUND_EWR then + + -- EWR + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI, 100)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.GROUNDATTACK, 50)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30)) + + elseif attribute==GROUP.Attribute.GROUND_AAA then + + -- AAA + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI, 100)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.GROUNDATTACK, 50)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARMORATTACK, 40)) + + elseif attribute==GROUP.Attribute.GROUND_ARTILLERY then + + -- ARTY + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI, 100)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.GROUNDATTACK, 75)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARMORATTACK, 70)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBING, 70)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30)) + + elseif attribute==GROUP.Attribute.GROUND_INFANTRY then + + -- Infantry + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI, 100)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.GROUNDATTACK, 50)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARMORATTACK, 40)) + + elseif attribute==GROUP.Attribute.GROUND_TANK then + + -- Tanks + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.CAS, 100)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.GROUNDATTACK, 50)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARMORATTACK, 40)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30)) + + else + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI, 100)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.GROUNDATTACK, 50)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30)) + + end + + + elseif category==Group.Category.SHIP then + + --- + -- NAVAL + --- + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ANTISHIP, 100)) + + else + self:E(self.lid.."ERROR: Unknown Group category!") + end + + elseif airbase then + + --- + -- AIRBASE + --- + + -- Bomb runway. + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBRUNWAY, 100)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30)) + + elseif static then + + --- + -- STATIC + --- + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI, 100)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBING, 70)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBCARPET, 50)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30)) + + elseif scenery then + + --- + -- SCENERY + --- + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.STRIKE, 100)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBING, 70)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBCARPET, 50)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30)) + + elseif coordinate then + + --- + -- COORDINATE + --- + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBING, 100)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBCARPET, 50)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30)) + + end + + return missionperf +end + +--- Get mission performances for a given Group Attribute. +-- @param #CHIEF self +-- @param #string Attribute Group attibute. +-- @return #table Mission performances of type `#CHIEF.MissionPerformance`. +function CHIEF:_GetMissionTypeForGroupAttribute(Attribute) + + local missionperf={} --#CHIEF.MissionPerformance + + if Attribute==GROUP.Attribute.AIR_ATTACKHELO then + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.INTERCEPT), 100) + + elseif Attribute==GROUP.Attribute.GROUND_AAA then + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI), 100) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBING), 80) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBCARPET), 70) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY), 30) + + elseif Attribute==GROUP.Attribute.GROUND_SAM then + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.SEAD), 100) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI), 90) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY), 50) + + elseif Attribute==GROUP.Attribute.GROUND_EWR then + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.SEAD), 100) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI), 100) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY), 50) + + end + + return missionperf +end + + +--- Recruit assets for a given OPS zone. +-- @param #CHIEF self +-- @param #CHIEF.StrategicZone StratZone The strategic zone. +-- @param #CHIEF.Resource Resource The required resources. +-- @return #boolean If `true` enough assets could be recruited. +function CHIEF:RecruitAssetsForZone(StratZone, Resource) + + -- Cohorts. + local Cohorts=self.commander:_GetCohorts() + + -- Shortcuts. + local MissionType=Resource.MissionType + local NassetsMin=Resource.Nmax + local NassetsMax=Resource.Nmax + local Categories=Resource.Categories + local Attributes=Resource.Attributes + local Properties=Resource.Properties + + -- Target position. + local TargetVec2=StratZone.opszone.zone:GetVec2() + + -- Max range in meters. + local RangeMax=nil + + -- Set max range to 250 NM because we use helos as transport for the infantry. + if MissionType==AUFTRAG.Type.PATROLZONE or MissionType==AUFTRAG.Type.ONGUARD then + RangeMax=UTILS.NMToMeters(250) + end + + -- Set max range to 50 NM because we use armor. + if MissionType==AUFTRAG.Type.ARMOREDGUARD then + RangeMax=UTILS.NMToMeters(50) + end + + -- Recruite infantry assets. + local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, MissionType, nil, NassetsMin, NassetsMax, TargetVec2, nil, RangeMax, nil, nil, nil, Categories, Attributes, Properties) + + if recruited then + + -- Mission for zone. + local mission=nil --Ops.Auftrag#AUFTRAG + + -- Debug messgage. + self:T2(self.lid..string.format("Recruited %d assets for %s mission STRATEGIC zone %s", #assets, MissionType, tostring(StratZone.opszone.zoneName))) + + local TargetZone = StratZone.opszone.zone + local TargetCoord = TargetZone:GetCoordinate() + + if MissionType==AUFTRAG.Type.PATROLZONE or MissionType==AUFTRAG.Type.ONGUARD then + + --- + -- PATROLZONE or ONGUARD + --- + + -- Debug messgage. + self:T2(self.lid..string.format("Recruited %d assets for PATROL mission", #assets)) + + -- First check if we need a transportation. + local recruitedTrans=true ; local transport=nil + if Attributes and Attributes[1]==GROUP.Attribute.GROUND_INFANTRY then + + -- Categories. Currently only helicopters are allowed due to problems with ground transports (might get stuck, might not be a land connection. + -- TODO: Check if ground transport is possible. For example, by trying land.getPathOnRoad or something. + local Categories=self.TransportCategories + + -- Recruit transport assets for infantry. + recruitedTrans, transport=LEGION.AssignAssetsForTransport(self.commander, self.commander.legions, assets, 1, 1, TargetZone, nil, Categories) + + end + + if recruitedTrans then + + if MissionType==AUFTRAG.Type.PATROLZONE then + mission=AUFTRAG:NewPATROLZONE(TargetZone) + + elseif MissionType==AUFTRAG.Type.ONGUARD then + mission=AUFTRAG:NewONGUARD(TargetZone:GetRandomCoordinate(nil, nil, {land.SurfaceType.LAND})) + end + + -- Engage detected targets. + mission:SetEngageDetected(25, {"Ground Units", "Light armed ships", "Helicopters"}) + + -- Attach OPS transport to mission. + mission.opstransport=transport + + -- Set ops zone to transport. + if transport then + transport.opszone=StratZone.opszone + transport.chief=self + transport.commander=self.commander + end + + else + -- No transport ==> no mission! + self:T(self.lid..string.format("Could not allocate transport of OPSZONE infantry!")) + LEGION.UnRecruitAssets(assets) + return false + end + + elseif MissionType==AUFTRAG.Type.CASENHANCED then + + --- + -- CAS ENHANCED + --- + + -- Create Patrol zone mission. + local height = UTILS.MetersToFeet(TargetCoord:GetLandHeight())+2500 + + local Speed=200 + if assets[1] then + if assets[1].speedmax then + Speed = UTILS.KmphToKnots(assets[1].speedmax * 0.7) or 200 + end + end + + -- CAS mission. + mission=AUFTRAG:NewCASENHANCED(TargetZone, height, Speed) + + elseif MissionType==AUFTRAG.Type.CAS then + + --- + -- CAS + --- + + -- Create Patrol zone mission. + local height = UTILS.MetersToFeet(TargetCoord:GetLandHeight())+2500 + + local Speed = 200 + if assets[1] then + if assets[1].speedmax then + Speed = UTILS.KmphToKnots(assets[1].speedmax * 0.7) or 200 + end + end + + -- Leg length. + local Leg = TargetZone:GetRadius() <= 10000 and 5 or UTILS.MetersToNM(TargetZone:GetRadius()) + + -- CAS mission. + mission=AUFTRAG:NewCAS(TargetZone, height, Speed, TargetCoord, math.random(0,359), Leg) + + elseif MissionType==AUFTRAG.Type.ARTY then + + --- + -- ARTY + --- + + -- Create ARTY zone mission. + local Radius = TargetZone:GetRadius() + + mission=AUFTRAG:NewARTY(TargetCoord, 120, Radius) + + elseif MissionType==AUFTRAG.Type.ARMOREDGUARD then + + --- + -- ARMORGUARD + --- + + -- Create Armored on guard mission + mission=AUFTRAG:NewARMOREDGUARD(TargetCoord) + + elseif MissionType==AUFTRAG.Type.BOMBCARPET then + + --- + -- BOMB CARPET + --- + + -- Create ARTY zone mission. + mission=AUFTRAG:NewBOMBCARPET(TargetCoord, nil, 1000) + + elseif MissionType==AUFTRAG.Type.BOMBING then + + --- + -- BOMBING + --- + + local coord=TargetZone:GetRandomCoordinate() + + mission=AUFTRAG:NewBOMBING(TargetCoord) + + elseif MissionType==AUFTRAG.Type.RECON then + + --- + -- RECON + --- + + mission=AUFTRAG:NewRECON(TargetZone, nil, 5000) + + elseif MissionType==AUFTRAG.Type.BARRAGE then + + --- + -- BARRAGE + --- + + mission=AUFTRAG:NewBARRAGE(TargetZone) + + elseif MissionType==AUFTRAG.Type.AMMOSUPPLY then + + --- + -- AMMO SUPPLY + --- + + mission=AUFTRAG:NewAMMOSUPPLY(TargetZone) + + end + + if mission then + + -- Add assets to mission. + for _,asset in pairs(assets) do + mission:AddAsset(asset) + end + + -- Assign mission to legions. + self:MissionAssign(mission, legions) + + -- Attach mission to ops zone. + StratZone.opszone:_AddMission(self.coalition, MissionType, mission) + + -- Attach mission to resource. + Resource.mission=mission + + return true + else + + -- Mission not supported. + self:E(self.lid..string.format("ERROR: Mission type not supported for OPSZONE! Unrecruiting assets...")) + LEGION.UnRecruitAssets(assets) + + return false + end + + end + + -- Debug messgage. + self:T2(self.lid..string.format("Could NOT recruit assets for %s mission of STRATEGIC zone %s", MissionType, tostring(StratZone.opszone.zoneName))) + + return false +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- **Ops** - Flotilla is a small naval group belonging to a fleet. +-- +-- **Main Features:** +-- +-- * Set parameters like livery, skill valid for all flotilla members. +-- * Define mission types, this flotilla can perform (see Ops.Auftrag#AUFTRAG). +-- * Pause/unpause flotilla operations. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module Ops.Flotilla +-- @image OPS_Flotilla.png + + +--- FLOTILLA class. +-- @type FLOTILLA +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity level. +-- @field Ops.OpsGroup#OPSGROUP.WeaponData weaponData Weapon data table with key=BitType. +-- @extends Ops.Cohort#COHORT + +--- *No captain can do very wrong if he places his ship alongside that of an enemy.* -- Horation Nelson +-- +-- === +-- +-- # The FLOTILLA Concept +-- +-- A FLOTILLA is an essential part of a FLEET. +-- +-- +-- +-- @field #FLOTILLA +FLOTILLA = { + ClassName = "FLOTILLA", + verbose = 0, + weaponData = {}, +} + +--- FLOTILLA class version. +-- @field #string version +FLOTILLA.version="0.1.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: A lot. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new FLOTILLA object and start the FSM. +-- @param #FLOTILLA self +-- @param #string TemplateGroupName Name of the template group. +-- @param #number Ngroups Number of asset groups of this flotilla. Default 3. +-- @param #string FlotillaName Name of the flotilla. Must be **unique**! +-- @return #FLOTILLA self +function FLOTILLA:New(TemplateGroupName, Ngroups, FlotillaName) + + -- Inherit everything from COHORT class. + local self=BASE:Inherit(self, COHORT:New(TemplateGroupName, Ngroups, FlotillaName)) -- #FLOTILLA + + -- All flotillas get mission type Nothing. + self:AddMissionCapability(AUFTRAG.Type.NOTHING, 50) + + -- Is naval. + self.isNaval=true + + -- Get initial ammo. + self.ammo=self:_CheckAmmo() + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + -- TODO: Flotilla specific user functions. + +--- Set fleet of this flotilla. +-- @param #FLOTILLA self +-- @param Ops.Fleet#FLEET Fleet The fleet. +-- @return #FLOTILLA self +function FLOTILLA:SetFleet(Fleet) + self.legion=Fleet + return self +end + +--- Get fleet of this flotilla. +-- @param #FLOTILLA self +-- @return Ops.Fleet#FLEET The fleet. +function FLOTILLA:GetFleet() + return self.legion +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Start & Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the FLIGHTGROUP FSM and event handlers. +-- @param #FLOTILLA self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLOTILLA:onafterStart(From, Event, To) + + -- Short info. + local text=string.format("Starting %s v%s %s", self.ClassName, self.version, self.name) + self:I(self.lid..text) + + -- Start the status monitoring. + self:__Status(-1) +end + +--- On after "Status" event. +-- @param #FLOTILLA self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLOTILLA:onafterStatus(From, Event, To) + + if self.verbose>=1 then + + -- FSM state. + local fsmstate=self:GetState() + + local callsign=self.callsignName and UTILS.GetCallsignName(self.callsignName) or "N/A" + local skill=self.skill and tostring(self.skill) or "N/A" + + local NassetsTot=#self.assets + local NassetsInS=self:CountAssets(true) + local NassetsQP=0 ; local NassetsP=0 ; local NassetsQ=0 + if self.legion then + NassetsQP, NassetsP, NassetsQ=self.legion:CountAssetsOnMission(nil, self) + end + + -- Short info. + local text=string.format("%s [Type=%s, Call=%s, Skill=%s]: Assets Total=%d, Stock=%d, Mission=%d [Active=%d, Queue=%d]", + fsmstate, self.aircrafttype, callsign, skill, NassetsTot, NassetsInS, NassetsQP, NassetsP, NassetsQ) + self:T(self.lid..text) + + -- Weapon data info. + if self.verbose>=3 and self.weaponData then + local text="Weapon Data:" + for bit,_weapondata in pairs(self.weaponData) do + local weapondata=_weapondata --Ops.OpsGroup#OPSGROUP.WeaponData + text=text..string.format("\n- Bit=%s: Rmin=%.1f km, Rmax=%.1f km", bit, weapondata.RangeMin/1000, weapondata.RangeMax/1000) + end + self:I(self.lid..text) + end + + -- Check if group has detected any units. + self:_CheckAssetStatus() + + end + + if not self:IsStopped() then + self:__Status(-60) + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- **Ops** - Fleet Warehouse. +-- +-- **Main Features:** +-- +-- * Manage flotillas +-- * Carry out ARTY and PATROLZONE missions (AUFTRAG) +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20Fleet). +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module Ops.Fleet +-- @image OPS_Fleet.png + + +--- FLEET class. +-- @type FLEET +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity of output. +-- @field Core.Set#SET_ZONE retreatZones Retreat zone set. +-- @field #boolean pathfinding Set pathfinding on for all spawned navy groups. +-- @extends Ops.Legion#LEGION + +--- *A fleet of British ships at war are the best negotiators.* -- Horatio Nelson +-- +-- === +-- +-- # The FLEET Concept +-- +-- A FLEET consists of one or multiple FLOTILLAs. These flotillas "live" in a WAREHOUSE that has a phyiscal struction (STATIC or UNIT) and can be captured or destroyed. +-- +-- # Basic Setup +-- +-- A new `FLEET` object can be created with the @{#FLEET.New}(`WarehouseName`, `FleetName`) function, where `WarehouseName` is the name of the static or unit object hosting the fleet +-- and `FleetName` is the name you want to give the fleet. This must be *unique*! +-- +-- myFleet=FLEET:New("myWarehouseName", "1st Fleet") +-- myFleet:SetPortZone(ZonePort1stFleet) +-- myFleet:Start() +-- +-- A fleet needs a *port zone*, which is set via the @{#FLEET.SetPortZone}(`PortZone`) function. This is the zone where the naval assets are spawned and return to. +-- +-- Finally, the fleet needs to be started using the @{#FLEET.Start}() function. If the fleet is not started, it will not process any requests. +-- +-- ## Adding Flotillas +-- +-- Flotillas can be added via the @{#FLEET.AddFlotilla}(`Flotilla`) function. See @{Ops.Flotilla#FLOTILLA} for how to create a flotilla. +-- +-- myFleet:AddFlotilla(FlotillaTiconderoga) +-- myFleet:AddFlotilla(FlotillaPerry) +-- +-- +-- +-- @field #FLEET +FLEET = { + ClassName = "FLEET", + verbose = 0, + pathfinding = false, +} + +--- Supply Zone. +-- @type FLEET.SupplyZone +-- @field Core.Zone#ZONE zone The zone. +-- @field Ops.Auftrag#AUFTRAG mission Mission assigned to supply ammo or fuel. +-- @field #boolean markerOn If `true`, marker is on. +-- @field Wrapper.Marker#MARKER marker F10 marker. + +--- FLEET class version. +-- @field #string version +FLEET.version="0.0.1" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Add routes? +-- DONE: Add weapon range. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new FLEET class object. +-- @param #FLEET self +-- @param #string WarehouseName Name of the warehouse STATIC or UNIT object representing the warehouse. +-- @param #string FleetName Name of the fleet. +-- @return #FLEET self +function FLEET:New(WarehouseName, FleetName) + + -- Inherit everything from LEGION class. + local self=BASE:Inherit(self, LEGION:New(WarehouseName, FleetName)) -- #FLEET + + -- Nil check. + if not self then + BASE:E(string.format("ERROR: Could not find warehouse %s!", WarehouseName)) + return nil + end + + -- Set some string id for output to DCS.log file. + self.lid=string.format("FLEET %s | ", self.alias) + + -- 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("*", "NavyOnMission", "*") -- An NAVYGROUP was send on a Mission (AUFTRAG). + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the FLEET. Initializes parameters and starts event handlers. + -- @function [parent=#FLEET] Start + -- @param #FLEET self + + --- Triggers the FSM event "Start" after a delay. Starts the FLEET. Initializes parameters and starts event handlers. + -- @function [parent=#FLEET] __Start + -- @param #FLEET self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Stop". Stops the FLEET and all its event handlers. + -- @param #FLEET self + + --- Triggers the FSM event "Stop" after a delay. Stops the FLEET and all its event handlers. + -- @function [parent=#FLEET] __Stop + -- @param #FLEET self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "NavyOnMission". + -- @function [parent=#FLEET] NavyOnMission + -- @param #FLEET self + -- @param Ops.NavyGroup#NAVYGROUP ArmyGroup The NAVYGROUP on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "NavyOnMission" after a delay. + -- @function [parent=#FLEET] __NavyOnMission + -- @param #FLEET self + -- @param #number delay Delay in seconds. + -- @param Ops.NavyGroup#NAVYGROUP ArmyGroup The NAVYGROUP on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "NavyOnMission" event. + -- @function [parent=#FLEET] OnAfterNavyOnMission + -- @param #FLEET self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.NavyGroup#NAVYGROUP NavyGroup The NAVYGROUP on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add a flotilla to the fleet. +-- @param #FLEET self +-- @param Ops.Flotilla#FLOTILLA Flotilla The flotilla object. +-- @return #FLEET self +function FLEET:AddFlotilla(Flotilla) + + -- Add flotilla to fleet. + table.insert(self.cohorts, Flotilla) + + -- Add assets to flotilla. + self:AddAssetToFlotilla(Flotilla, Flotilla.Ngroups) + + -- Set fleet of flotilla. + Flotilla:SetFleet(self) + + -- Start flotilla. + if Flotilla:IsStopped() then + Flotilla:Start() + end + + return self +end + +--- Add asset group(s) to flotilla. +-- @param #FLEET self +-- @param Ops.Flotilla#FLOTILLA Flotilla The flotilla object. +-- @param #number Nassets Number of asset groups to add. +-- @return #FLEET self +function FLEET:AddAssetToFlotilla(Flotilla, Nassets) + + if Flotilla then + + -- Get the template group of the flotilla. + local Group=GROUP:FindByName(Flotilla.templatename) + + if Group then + + -- Debug text. + local text=string.format("Adding asset %s to flotilla %s", Group:GetName(), Flotilla.name) + self:T(self.lid..text) + + -- Add assets to airwing warehouse. + self:AddAsset(Group, Nassets, nil, nil, nil, nil, Flotilla.skill, Flotilla.livery, Flotilla.name) + + else + self:E(self.lid.."ERROR: Group does not exist!") + end + + else + self:E(self.lid.."ERROR: Flotilla does not exit!") + end + + return self +end + +--- Set pathfinding for all spawned naval groups. +-- @param #FLEET self +-- @param #boolean Switch If `true`, pathfinding is used. +-- @return #FLEET self +function FLEET:SetPathfinding(Switch) + self.pathfinding=Switch + return self +end + +--- Define a set of retreat zones. +-- @param #FLEET self +-- @param Core.Set#SET_ZONE RetreatZoneSet Set of retreat zones. +-- @return #FLEET self +function FLEET:SetRetreatZones(RetreatZoneSet) + self.retreatZones=RetreatZoneSet or SET_ZONE:New() + return self +end + +--- Add a retreat zone. +-- @param #FLEET self +-- @param Core.Zone#ZONE RetreatZone Retreat zone. +-- @return #FLEET self +function FLEET:AddRetreatZone(RetreatZone) + self.retreatZones:AddZone(RetreatZone) + return self +end + +--- Get retreat zones. +-- @param #FLEET self +-- @return Core.Set#SET_ZONE Set of retreat zones. +function FLEET:GetRetreatZones() + return self.retreatZones +end + +--- Get flotilla by name. +-- @param #FLEET self +-- @param #string FlotillaName Name of the flotilla. +-- @return Ops.Flotilla#FLOTILLA The Flotilla object. +function FLEET:GetFlotilla(FlotillaName) + local flotilla=self:_GetCohort(FlotillaName) + return flotilla +end + +--- Get flotilla of an asset. +-- @param #FLEET self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The flotilla asset. +-- @return Ops.Flotilla#FLOTILLA The flotilla object. +function FLEET:GetFlotillaOfAsset(Asset) + local flotilla=self:GetFlotilla(Asset.squadname) + return flotilla +end + +--- Remove asset from flotilla. +-- @param #FLEET self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The flotilla asset. +function FLEET:RemoveAssetFromFlotilla(Asset) + local flotilla=self:GetFlotillaOfAsset(Asset) + if flotilla then + flotilla:DelAsset(Asset) + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Start FLEET FSM. +-- @param #FLEET self +function FLEET:onafterStart(From, Event, To) + + -- Start parent Warehouse. + self:GetParent(self, FLEET).onafterStart(self, From, Event, To) + + -- Info. + self:I(self.lid..string.format("Starting FLEET v%s", FLEET.version)) + +end + +--- Update status. +-- @param #FLEET self +function FLEET:onafterStatus(From, Event, To) + + -- Status of parent Warehouse. + self:GetParent(self).onafterStatus(self, From, Event, To) + + -- FSM state. + local fsmstate=self:GetState() + + ---------------- + -- Transport --- + ---------------- + + self:CheckTransportQueue() + + -------------- + -- Mission --- + -------------- + + -- Check if any missions should be cancelled. + self:CheckMissionQueue() + + ----------- + -- Info --- + ----------- + + -- General info: + if self.verbose>=1 then + + -- Count missions not over yet. + local Nmissions=self:CountMissionsInQueue() + + -- Asset count. + local Npq, Np, Nq=self:CountAssetsOnMission() + + -- Asset string. + local assets=string.format("%d [OnMission: Total=%d, Active=%d, Queued=%d]", self:CountAssets(), Npq, Np, Nq) + + -- Output. + local text=string.format("%s: Missions=%d, Flotillas=%d, Assets=%s", fsmstate, Nmissions, #self.cohorts, assets) + self:I(self.lid..text) + end + + ------------------ + -- Mission Info -- + ------------------ + if self.verbose>=2 then + local text=string.format("Missions Total=%d:", #self.missionqueue) + for i,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + local prio=string.format("%d/%s", mission.prio, tostring(mission.importance)) ; if mission.urgent then prio=prio.." (!)" end + local assets=string.format("%d/%d", mission:CountOpsGroups(), mission.Nassets or 0) + local target=string.format("%d/%d Damage=%.1f", mission:CountMissionTargets(), mission:GetTargetInitialNumber(), mission:GetTargetDamage()) + + text=text..string.format("\n[%d] %s %s: Status=%s, Prio=%s, Assets=%s, Targets=%s", i, mission.name, mission.type, mission.status, prio, assets, target) + end + self:I(self.lid..text) + end + + -------------------- + -- Transport Info -- + -------------------- + if self.verbose>=2 then + local text=string.format("Transports Total=%d:", #self.transportqueue) + for i,_transport in pairs(self.transportqueue) do + local transport=_transport --Ops.OpsTransport#OPSTRANSPORT + + local prio=string.format("%d/%s", transport.prio, tostring(transport.importance)) ; if transport.urgent then prio=prio.." (!)" end + local carriers=string.format("Ncargo=%d/%d, Ncarriers=%d", transport.Ncargo, transport.Ndelivered, transport.Ncarrier) + + text=text..string.format("\n[%d] UID=%d: Status=%s, Prio=%s, Cargo: %s", i, transport.uid, transport:GetState(), prio, carriers) + end + self:I(self.lid..text) + end + + ------------------- + -- Flotilla Info -- + ------------------- + if self.verbose>=3 then + local text="Flotillas:" + for i,_flotilla in pairs(self.cohorts) do + local flotilla=_flotilla --Ops.Flotilla#FLOTILLA + + local callsign=flotilla.callsignName and UTILS.GetCallsignName(flotilla.callsignName) or "N/A" + local modex=flotilla.modex and flotilla.modex or -1 + local skill=flotilla.skill and tostring(flotilla.skill) or "N/A" + + -- Flotilla text. + text=text..string.format("\n* %s %s: %s*%d/%d, Callsign=%s, Modex=%d, Skill=%s", flotilla.name, flotilla:GetState(), flotilla.aircrafttype, flotilla:CountAssets(true), #flotilla.assets, callsign, modex, skill) + end + self:I(self.lid..text) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "NavyOnMission". +-- @param #FLEET self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.ArmyGroup#ARMYGROUP ArmyGroup Ops army group on mission. +-- @param Ops.Auftrag#AUFTRAG Mission The requested mission. +function FLEET:onafterNavyOnMission(From, Event, To, NavyGroup, Mission) + -- Debug info. + self:T(self.lid..string.format("Group %s on %s mission %s", NavyGroup:GetName(), Mission:GetType(), Mission:GetName())) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- **Ops** - AWACS +-- +-- === +-- +-- **AWACS** - MOOSE AI AWACS Operations using text-to-speech. +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20Awacs/). +-- +-- ## Videos: +-- +-- Demo videos can be found on [Youtube](https://www.youtube.com/watch?v=ocdy8QzTNN4&list=PLFxp425SeXnq-oS0DSjam1HtddywH8i_k) +-- +-- === +-- +-- ### Author: **applevangelist** +-- @date Last Update July 2022 +-- @module Ops.AWACS +-- @image OPS_AWACS.jpg + +do +--- Ops AWACS Class +-- @type AWACS +-- @field #string ClassName Name of this class. +-- @field #string version Versioning. +-- @field #string lid LID for log entries. +-- @field #number coalition Colition side. +-- @field #string coalitiontxt e.g."blue" +-- @field Core.Zone#ZONE OpsZone, +-- @field Core.Zone#ZONE StationZone, +-- @field Core.Zone#ZONE BorderZone, +-- @field Core.Zone#ZONE RejectZone, +-- @field #number Frequency +-- @field #number Modulation +-- @field Wrapper.Airbase#AIRBASE Airbase +-- @field Ops.AirWing#AIRWING AirWing +-- @field #number AwacsAngels +-- @field Core.Zone#ZONE OrbitZone +-- @field #number CallSign +-- @field #number CallSignNo +-- @field #boolean debug +-- @field #number verbose +-- @field #table ManagedGrps +-- @field #number ManagedGrpID +-- @field #number ManagedTaskID +-- @field Utilities.FiFo#FIFO AnchorStacks +-- @field Utilities.FiFo#FIFO CAPIdleAI +-- @field Utilities.FiFo#FIFO CAPIdleHuman +-- @field Utilities.FiFo#FIFO TaskedCAPAI +-- @field Utilities.FiFo#FIFO TaskedCAPHuman +-- @field Utilities.FiFo#FIFO OpenTasks +-- @field Utilities.FiFo#FIFO ManagedTasks +-- @field Utilities.FiFo#FIFO PictureAO +-- @field Utilities.FiFo#FIFO PictureEWR +-- @field Utilities.FiFo#FIFO Contacts +-- @field #table CatchAllMissions +-- @field #table CatchAllFGs +-- @field #number Countactcounter +-- @field Utilities.FiFo#FIFO ContactsAO +-- @field Utilities.FiFo#FIFO RadioQueue +-- @field Utilities.FiFo#FIFO PrioRadioQueue +-- @field Utilities.FiFo#FIFO CAPAirwings +-- @field #number AwacsTimeOnStation +-- @field #number AwacsTimeStamp +-- @field #number EscortsTimeOnStation +-- @field #number EscortsTimeStamp +-- @field #string AwacsROE +-- @field #string AwacsROT +-- @field Ops.Auftrag#AUFTRAG AwacsMission +-- @field Ops.Auftrag#AUFTRAG EscortMission +-- @field Ops.Auftrag#AUFTRAG AwacsMissionReplacement +-- @field Ops.Auftrag#AUFTRAG EscortMissionReplacement +-- @field Utilities.FiFo#FIFO AICAPMissions FIFO for Ops.Auftrag#AUFTRAG for AI CAP +-- @field #boolean MenuStrict +-- @field #number MaxAIonCAP +-- @field #number AIonCAP +-- @field #boolean ShiftChangeAwacsFlag +-- @field #boolean ShiftChangeEscortsFlag +-- @field #boolean ShiftChangeAwacsRequested +-- @field #boolean ShiftChangeEscortsRequested +-- @field #AWACS.MonitoringData MonitoringData +-- @field #boolean MonitoringOn +-- @field Core.Set#SET_CLIENT clientset +-- @field Utilities.FiFo#FIFO FlightGroups +-- @field #number PictureInterval Interval in seconds for general picture +-- @field #number PictureTimeStamp Interval timestamp +-- @field #number maxassigndistance Only assing AI/Pilots to targets max this far away +-- @field #boolean PlayerGuidance if true additional callouts to guide/warn players +-- @field #boolean ModernEra if true we get more intel on targets, and EPLR on the AIC +-- @field #boolean callsignshort if true use short (group) callsigns, e.g. "Ghost 1", else "Ghost 1 1" +-- @field #number MeldDistance 25nm - distance for "Meld" Call , usually shortly before the actual engagement +-- @field #number TacDistance 30nm - distance for "TAC" Call +-- @field #number ThreatDistance 15nm - distance to declare untargeted (new) threats +-- @field #string AOName name of the FEZ, e.g. Rock +-- @field Core.Point#COORDINATE AOCoordinate Coordinate of bulls eye +-- @field Utilities.FiFo#FIFO clientmenus +-- @field #number RadarBlur Radar blur in % +-- @field #number ReassignmentPause Wait this many seconds before re-assignment of a player +-- @field #boolean NoGroupTags Set to true if you don't want group tags. +-- @field #boolean SuppressScreenOutput Set to true to suppress all screen output. +-- @field #boolean NoMissileCalls Suppress missile callouts +-- @field #boolean PlayerCapAssigment Assign players to CAP tasks when they are logged on +-- @field #number GoogleTTSPadding +-- @field #number WindowsTTSPadding +-- @field #boolean AllowMarkers +-- @field #string PlayerStationName +-- @field #boolean GCI Act as GCI +-- @field Wrapper.Group#GROUP GCIGroup EWR group object for GCI ops +-- @extends Core.Fsm#FSM + + +--- +-- +-- *Of all men\'s miseries the bitterest is this: to know so much and to have control over nothing.* (Herodotus) +-- +-- === +-- +-- # AWACS AI Air Controller +-- +-- * WIP (beta) +-- * AWACS replacement for the in-game AWACS +-- * Will control a fighter engagement zone and assign tasks to AI and human CAP flights +-- * Callouts referenced from: +-- ** References from ARN33396 ATP 3-52.4 (Sep 2021) (Combined Forces) +-- ** References from CNATRA P-877 (Rev 12-20) (NAVY) +-- * FSM events that the mission designer can hook into +-- * Can also be used as GCI Controller +-- +-- ## 0 Note for Multiplayer Setup +-- +-- Due to DCS limitations you need to set up a second, "normal" AWACS plane in multi-player/server environments to keep the EPLRS/DataLink going in these environments. +-- Though working in single player, the situational awareness screens of the e.g. F14/16/18 will else not receive datalink targets. +-- +-- ## 1 Prerequisites +-- +-- The radio callouts in this class are ***exclusively*** created with Text-To-Speech (TTS), based on the Moose @{Sound.SRS} Class, and output is via [Ciribob's SRS system](https://github.com/ciribob/DCS-SimpleRadioStandalone/releases) +-- Ensure you have this covered and working before tackling this class. TTS generation can thus be done via the Windows built-in system or via Google TTS; +-- the latter offers a wider range of voices and options, but you need to set up your own Google product account for this to work correctly. +-- +-- ## 2 Mission Design - Operational Priorities +-- +-- Basic operational target of the AWACS is to control a Fighter Engagement Zone, or FEZ, and defend itself. +-- +-- ## 3 Airwing(s) +-- +-- The AWACS plane, the optional escort planes, and the AI CAP planes work based on the @{Ops.AirWing} class. Read and understand the manual for this class in +-- order to set everything up correctly. You will at least need one Squadron containing the AWACS plane itself. +-- +-- Set up the AirWing +-- +-- local AwacsAW = AIRWING:New("AirForce WH-1","AirForce One") +-- AwacsAW:SetMarker(false) +-- AwacsAW:SetAirbase(AIRBASE:FindByName(AIRBASE.Caucasus.Kutaisi)) +-- AwacsAW:SetRespawnAfterDestroyed(900) +-- AwacsAW:SetTakeoffAir() +-- AwacsAW:__Start(2) +-- +-- Add the AWACS template Squadron - **Note**: remove the task AWACS in the mission editor under "Advanced Waypoint Actions" from the template to remove the DCS F10 AWACS menu +-- +-- local Squad_One = SQUADRON:New("Awacs One",2,"Awacs North") +-- Squad_One:AddMissionCapability({AUFTRAG.Type.ORBIT},100) +-- Squad_One:SetFuelLowRefuel(true) +-- Squad_One:SetFuelLowThreshold(0.2) +-- Squad_One:SetTurnoverTime(10,20) +-- AwacsAW:AddSquadron(Squad_One) +-- AwacsAW:NewPayload("Awacs One One",-1,{AUFTRAG.Type.ORBIT},100) +-- +-- Add Escorts Squad (recommended, optional) +-- +-- local Squad_Two = SQUADRON:New("Escorts",4,"Escorts North") +-- Squad_Two:AddMissionCapability({AUFTRAG.Type.ESCORT}) +-- Squad_Two:SetFuelLowRefuel(true) +-- Squad_Two:SetFuelLowThreshold(0.3) +-- Squad_Two:SetTurnoverTime(10,20) +-- Squad_Two:SetTakeoffAir() +-- Squad_Two:SetRadio(255,radio.modulation.AM) +-- AwacsAW:AddSquadron(Squad_Two) +-- AwacsAW:NewPayload("Escorts",-1,{AUFTRAG.Type.ESCORT},100) +-- +-- Add CAP Squad (recommended, optional) +-- +-- local Squad_Three = SQUADRON:New("CAP",10,"CAP North") +-- Squad_Three:AddMissionCapability({AUFTRAG.Type.ALERT5, AUFTRAG.Type.CAP, AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT},80) +-- Squad_Three:SetFuelLowRefuel(true) +-- Squad_Three:SetFuelLowThreshold(0.3) +-- Squad_Three:SetTurnoverTime(10,20) +-- Squad_Three:SetTakeoffAir() +-- Squad_Two:SetRadio(255,radio.modulation.AM) +-- AwacsAW:AddSquadron(Squad_Three) +-- AwacsAW:NewPayload("Aerial-1-2",-1,{AUFTRAG.Type.ALERT5,AUFTRAG.Type.CAP, AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT},100) +-- +-- ## 4 Zones +-- +-- For the setup, you need to set up a couple of zones: +-- +-- * An Orbit Zone, where your AWACS will orbit +-- * A Fighter Engagement Zone or FEZ +-- * A zone where your CAP flights will be stationed, waiting for assignments +-- * Optionally, an additional zone you wish to defend +-- * Optionally, a border of the opposing party +-- * Also, and move your BullsEye in the mission accordingly - this will be the key reference point for most AWACS callouts +-- +-- ### 4.1 Strategic considerations +-- +-- Your AWACS is an HVT or high-value-target. Thus it makes sense to position the Orbit Zone in a way that your FEZ and thus your CAP flights defend it. +-- It should hence be positioned behind the FEZ, away from the direction of enemy engagement. +-- The zone for CAP stations should be close to the FEZ, but not inside it. +-- The optional additional defense zone can be anywhere, but keep an eye on the location so your CAP flights don't take ages to get there. +-- The optional border is useful for e.g. "cold war" scenarios - planes across the border will not be considered as targets by AWACS. +-- +-- ## 5 Set up AWACS +-- +-- -- Set up AWACS called "AWACS North". It will use the AwacsAW AirWing set up above and be of the "blue" coalition. Homebase is Kutaisi. +-- -- The AWACS Orbit Zone is a round zone set in the mission editor named "Awacs Orbit", the FEZ is a Polygon-Zone called "Rock" we have also +-- -- set up in the mission editor with a late activated helo named "Rock#ZONE_POLYGON". Note this also sets the BullsEye to be referenced as "Rock". +-- -- The CAP station zone is called "Fremont". We will be on 255 AM. +-- local testawacs = AWACS:New("AWACS North",AwacsAW,"blue",AIRBASE.Caucasus.Kutaisi,"Awacs Orbit",ZONE:FindByName("Rock"),"Fremont",255,radio.modulation.AM ) +-- -- set two escorts +-- testawacs:SetEscort(2) +-- -- Callsign will be "Focus". We'll be a Angels 30, doing 300 knots, orbit leg to 88deg with a length of 25nm. +-- testawacs:SetAwacsDetails(CALLSIGN.AWACS.Focus,1,30,300,88,25) +-- -- Set up SRS on port 5010 - change the below to your path and port +-- testawacs:SetSRS("C:\\Program Files\\DCS-SimpleRadio-Standalone","female","en-GB",5010) +-- -- Add a "red" border we don't want to cross, set up in the mission editor with a late activated helo named "Red Border#ZONE_POLYGON" +-- testawacs:SetRejectionZone(ZONE:FindByName("Red Border")) +-- -- Our CAP flight will have the callsign "Ford", we want 4 AI planes, Time-On-Station is four hours, doing 300 kn IAS. +-- testawacs:SetAICAPDetails(CALLSIGN.Aircraft.Ford,4,4,300) +-- -- We're modern (default), e.g. we have EPLRS and get more fill-in information on detections +-- testawacs:SetModernEra() +-- -- And start +-- testawacs:__Start(5) +-- +-- ### 5.1 Alternative - Set up as GCI (no AWACS plane needed) Theater Air Control System (TACS) +-- +-- -- Set up as TACS called "GCI Senaki". It will use the AwacsAW AirWing set up above and be of the "blue" coalition. Homebase is Senaki. +-- -- No need to set the AWACS Orbit Zone; the FEZ is still a Polygon-Zone called "Rock" we have also +-- -- set up in the mission editor with a late activated helo named "Rock#ZONE_POLYGON". Note this also sets the BullsEye to be referenced as "Rock". +-- -- The CAP station zone is called "Fremont". We will be on 255 AM. Note the Orbit Zone is given as *nil* in the `New()`-Statement +-- local testawacs = AWACS:New("GCI Senaki",AwacsAW,"blue",AIRBASE.Caucasus.Senaki_Kolkhi,nil,ZONE:FindByName("Rock"),"Fremont",255,radio.modulation.AM ) +-- -- Set up SRS on port 5010 - change the below to your path and port +-- testawacs:SetSRS("C:\\Program Files\\DCS-SimpleRadio-Standalone","female","en-GB",5010) +-- -- Add a "red" border we don't want to cross, set up in the mission editor with a late activated helo named "Red Border#ZONE_POLYGON" +-- testawacs:SetRejectionZone(ZONE:FindByName("Red Border")) +-- -- Our CAP flight will have the callsign "Ford", we want 4 AI planes, Time-On-Station is four hours, doing 300 kn IAS. +-- testawacs:SetAICAPDetails(CALLSIGN.Aircraft.Ford,4,4,300) +-- -- We're modern (default), e.g. we have EPLRS and get more fill-in information on detections +-- testawacs:SetModernEra() +-- -- Give it a fancy callsign +-- testawacs:SetAwacsDetails(CALLSIGN.AWACS.Wizard) +-- -- And start as GCI using a group name "Blue EWR" as main EWR station +-- testawacs:SetAsGCI(GROUP:FindByName("Blue EWR"),2) +-- -- Set Custom CAP Flight Callsigns for use with TTS +-- testawacs:SetCustomCallsigns({ +-- Devil = 'Bengal', +-- Snake = 'Winder', +-- Colt = 'Camelot', +-- Enfield = 'Victory', +-- Uzi = 'Evil Eye' +-- }) +-- testawacs:__Start(4) +-- +-- ## 6 Menu entries +-- +-- **Note on Radio Menu entries**: Due to a DCS limitation, these are on GROUP level and not individual (UNIT level). Hence, either put each player in his/her own group, +-- or ensure that only the flight lead will use the menu. Recommend the 1st option, unless you have a disciplined team. +-- +-- ### 6.1 Check-in +-- +-- In the base setup, you need to check in to the AWACS to get the full menu. This can be done once the AWACS is airborne. You will get an Alpha Check callout +-- and be assigned a CAP station. +-- +-- ### 6.2 Check-out +-- +-- You can check-out anytime, of course. +-- +-- ### 6.3 Picture +-- +-- Get a picture from the AWACS. It will call out the three most important groups. References are **always** to the (named) BullsEye position. +-- **Note** that AWACS will anyway do a regular picture call to all stations every five minutes. +-- +-- ### 6.4 Bogey Dope +-- +-- Get bogey dope from the AWACS. It will call out the closest bogey group, if any. Reference is BRAA to the Player position. +-- +-- ### 6.5 Declare +-- +-- AWACS will declare, if the bogey closest to the calling player in a 3nm circle is hostile, friendly or neutral. +-- +-- ### 6.6 Tasking +-- +-- Tasking will show you the current task with "Showtask". Updated directions are shown, also. +-- You can decline a **requested** task with "unable", and abort **any task but CAP station** with "abort". +-- You can "commit" to a requested task within 3 minutes. +-- "VID" - if AWACS is set to Visial ID or VID oncoming planes first, there will also be an "VID" entry. Similar to "Declare" you can declare the requested contact +-- to be hostile, friendly or neutral if you are close enough to it (3nm). If hostile, at the time of writing, an engagement task will be assigned to you (not: requested). +-- If neutral/friendly, contact will be excluded from further tasking. +-- +-- ## 7 Air-to-Air Timeline Support +-- +-- To support your engagement timeline, AWACS will make Tac-Range, Meld, Merge and Threat call-outs to the player/group (Figure 7-3, CNATRA P-877). Default settings in NM are +-- +-- Tac Distance = 45 +-- Meld Distance = 35 +-- Threat Distance = 25 +-- Merge Distance = 5 +-- +-- ## 8 Bespoke Player CallSigns +-- +-- Append the GROUP name of your client slots with "#CallSign" to use bespoke callsigns in AWACS callouts. E.g. "Player F14#Ghostrider" will be refered to +-- as "Ghostrider" plus group number, e.g. "Ghostrider 9". +-- +-- ## 9 Options +-- +-- There's a number of functions available, to set various options for the setup. +-- +-- * @{#AWACS.SetBullsEyeAlias}() : Set the alias name of the Bulls Eye. +-- * @{#AWACS.SetTOS}() : Set time on station for AWACS and CAP. +-- * @{#AWACS.SetReassignmentPause}() : Pause this number of seconds before re-assigning a Player to a task. +-- * @{#AWACS.SuppressScreenMessages}() : Suppress message output on screen. +-- * @{#AWACS.SetRadarBlur}() : Set the radar blur faktor in percent. +-- * @{#AWACS.SetColdWar}() : Set to cold war - no fill-ins, no EPLRS, VID as standard. +-- * @{#AWACS.SetModernEraDefensive}() : Set to modern, EPLRS, BVR/IFF engagement, fill-ins. +-- * @{#AWACS.SetModernEraAgressive}() : Set to modern, EPLRS, BVR/IFF engagement, fill-ins. +-- * @{#AWACS.SetPolicingModern}() : Set to modern, EPLRS, VID engagement, fill-ins. +-- * @{#AWACS.SetPolicingColdWar}() : Set to cold war, no EPLRS, VID engagement, no fill-ins. +-- * @{#AWACS.SetInterceptTimeline}() : Set distances for TAC, Meld and Threat range calls. +-- * @{#AWACS.SetAdditionalZone}() : Add one additional defense zone, e.g. own border. +-- * @{#AWACS.SetRejectionZone}() : Add one foreign border. Targets beyond will be ignored for tasking. +-- * @{#AWACS.DrawFEZ}() : Show the FEZ on the F10 map. +-- * @{#AWACS.SetAWACSDetails}() : Set AWACS details. +-- * @{#AWACS.AddGroupToDetection}() : Add a GROUP or SET_GROUP object to INTEL detection, e.g. EWR. +-- * @{#AWACS.SetSRS}() : Set SRS details. +-- * @{#AWACS.SetSRSVoiceCAP}() : Set voice details for AI CAP planes, using Windows dektop TTS. +-- * @{#AWACS.SetAICAPDetails}() : Set AI CAP details. +-- * @{#AWACS.SetEscort}() : Set number of escorting planes for AWACS. +-- * @{#AWACS.AddCAPAirWing}() : Add an additional @{Ops.Airwing#AIRWING} for CAP flights. +-- * @{#AWACS.ZipLip}() : Do not show messages on screen, no extra calls for player guidance, use short callsigns, no group tags. +-- +-- ## 9.1 Single Options +-- +-- Further single options (set before starting your AWACS instance, but after `:New()`) +-- +-- testawacs.PlayerGuidance = true -- allow missile warning call-outs. +-- testawacs.NoGroupTags = false -- use group tags like Alpha, Bravo .. etc in call outs. +-- testawacs.callsignshort = true -- use short callsigns, e.g. "Moose 1", not "Moose 1-1". +-- testawacs.DeclareRadius = 5 -- you need to be this close to the lead unit for declare/VID to work, in NM. +-- testawacs.MenuStrict = true -- Players need to check-in to see the menu; check-in still require to use the menu. +-- testawacs.maxassigndistance = 100 -- Don't assign targets further out than this, in NM. +-- testawacs.debug = false -- set to true to produce more log output. +-- testawacs.NoMissileCalls = true -- suppress missile callouts +-- testawacs.PlayerCapAssigment = true -- no intercept task assignments for players +-- testawacs.invisible = false -- set AWACS to be invisible to hostiles +-- testawacs.immortal = false -- set AWACS to be immortal +-- -- By default, the radio queue is checked every 10 secs. This is altered by the calculated length of the sentence to speak +-- -- over the radio. Google and Windows speech speed is different. Use the below to fine-tune the setup in case of overlapping +-- -- messages or too long pauses +-- testawacs.GoogleTTSPadding = 1 -- seconds +-- testawacs.WindowsTTSPadding = 2.5 -- seconds +-- +-- ## 9.2 Bespoke random voices for AI CAP (Google TTS only) +-- +-- Currently there are 10 voices defined which are randomly assigned to the AI CAP flights: +-- +-- Defaults are: +-- +-- testawacs.CapVoices = { +-- [1] = "de-DE-Wavenet-A", +-- [2] = "de-DE-Wavenet-B", +-- [3] = "fr-FR-Wavenet-A", +-- [4] = "fr-FR-Wavenet-B", +-- [5] = "en-GB-Wavenet-A", +-- [6] = "en-GB-Wavenet-B", +-- [7] = "en-GB-Wavenet-D", +-- [8] = "en-AU-Wavenet-B", +-- [9] = "en-US-Wavenet-J", +-- [10] = "en-US-Wavenet-H", +-- } +-- +-- ## 10 Using F10 map markers to create new player station points +-- +-- You can use F10 map markers to create new station points for human CAP flights. The latest created station will take priority for (new) station assignments for humans. +-- Enable this option with +-- +-- testawacs.AllowMarkers = true +-- +-- Set a marker on the map and add the following text to create a station: "AWACS Station London" - "AWACS Station" are the necessary keywords, "London" +-- in this example will be the name of the new station point. The user marker can then be deleted, an info marker point at the same place will remain. +-- You can delete a player station point the same way: "AWACS Delete London"; note this will only work if currently there are no assigned flights on this station. +-- Lastly, you can move the station around with keyword "Move": "AWACS Move London". +-- +-- ## 11 Discussion +-- +-- If you have questions or suggestions, please visit the [MOOSE Discord](https://discord.gg/AeYAkHP) #ops-awacs channel. +-- +-- +-- +-- +-- @field #AWACS +AWACS = { + ClassName = "AWACS", -- #string + version = "beta 0.2.33", -- #string + lid = "", -- #string + coalition = coalition.side.BLUE, -- #number + coalitiontxt = "blue", -- #string + OpsZone = nil, + StationZone = nil, + AirWing = nil, + Frequency = 271, -- #number + Modulation = radio.modulation.AM, -- #number + Airbase = nil, + AwacsAngels = 25, -- orbit at 25'000 ft + OrbitZone = nil, + CallSign = CALLSIGN.AWACS.Magic, -- #number + CallSignNo = 1, -- #number + debug = false, + verbose = false, + ManagedGrps = {}, + ManagedGrpID = 0, -- #number + ManagedTaskID = 0, -- #number + AnchorStacks = {}, -- Utilities.FiFo#FIFO + CAPIdleAI = {}, + CAPIdleHuman = {}, + TaskedCAPAI = {}, + TaskedCAPHuman = {}, + OpenTasks = {}, -- Utilities.FiFo#FIFO + ManagedTasks = {}, -- Utilities.FiFo#FIFO + PictureAO = {}, -- Utilities.FiFo#FIFO + PictureEWR = {}, -- Utilities.FiFo#FIFO + Contacts = {}, -- Utilities.FiFo#FIFO + Countactcounter = 0, + ContactsAO = {}, -- Utilities.FiFo#FIFO + RadioQueue = {}, -- Utilities.FiFo#FIFO + PrioRadioQueue = {}, -- Utilities.FiFo#FIFO + AwacsTimeOnStation = 4, + AwacsTimeStamp = 0, + EscortsTimeOnStation = 4, + EscortsTimeStamp = 0, + CAPTimeOnStation = 4, + AwacsROE = "", + AwacsROT = "", + MenuStrict = true, + MaxAIonCAP = 3, + AIonCAP = 0, + AICAPMissions = {}, -- Utilities.FiFo#FIFO + ShiftChangeAwacsFlag = false, + ShiftChangeEscortsFlag = false, + ShiftChangeAwacsRequested = false, + ShiftChangeEscortsRequested = false, + CAPAirwings = {}, -- Utilities.FiFo#FIFO + MonitoringData = {}, + MonitoringOn = false, + FlightGroups = {}, + AwacsMission = nil, + AwacsInZone = false, -- not yet arrived or gone again + AwacsReady = false, + CatchAllMissions = {}, + CatchAllFGs = {}, + PictureInterval = 300, + ReassignTime = 120, + PictureTimeStamp = 0, + BorderZone = nil, + RejectZone = nil, + maxassigndistance = 100, + PlayerGuidance = true, + ModernEra = true, + callsignshort = true, + TacDistance = 45, + MeldDistance = 35, + ThreatDistance = 25, + AOName = "Rock", + AOCoordinate = nil, + clientmenus = nil, + RadarBlur = 15, + ReassignmentPause = 180, + NoGroupTags = false, + SuppressScreenOutput = false, + NoMissileCalls = true, + GoogleTTSPadding = 1, + WindowsTTSPadding = 2.5, + PlayerCapAssigment = true, + AllowMarkers = false, + PlayerStationName = nil, + GCI = false, + GCIGroup = nil, +} + +--- +--@field CallSignClear +AWACS.CallSignClear = { + [1]="Overlord", + [2]="Magic", + [3]="Wizard", + [4]="Focus", + [5]="Darkstar", +} + +--- +-- @field AnchorNames +AWACS.AnchorNames = { + [1] = "One", + [2] = "Two", + [3] = "Three", + [4] = "Four", + [5] = "Five", + [6] = "Six", + [7] = "Seven", + [8] = "Eight", + [9] = "Nine", + [10] = "Ten", +} + +--- +-- @field IFF +AWACS.IFF = +{ + SPADES = "Spades", + NEUTRAL = "Neutral", + FRIENDLY = "Friendly", + ENEMY = "Hostile", + BOGEY = "Bogey", +} + +--- +-- @field Phonetic +AWACS.Phonetic = +{ + [1] = 'Alpha', + [2] = 'Bravo', + [3] = 'Charlie', + [4] = 'Delta', + [5] = 'Echo', + [6] = 'Foxtrot', + [7] = 'Golf', + [8] = 'Hotel', + [9] = 'India', + [10] = 'Juliett', + [11] = 'Kilo', + [12] = 'Lima', + [13] = 'Mike', + [14] = 'November', + [15] = 'Oscar', + [16] = 'Papa', + [17] = 'Quebec', + [18] = 'Romeo', + [19] = 'Sierra', + [20] = 'Tango', + [21] = 'Uniform', + [22] = 'Victor', + [23] = 'Whiskey', + [24] = 'Xray', + [25] = 'Yankee', + [26] = 'Zulu', +} + +--- +-- @field Shipsize +AWACS.Shipsize = +{ + [1] = "Singleton", + [2] = "Two-Ship", + [3] = "Heavy", + [4] = "Gorilla", +} + +--- +-- @field ROE +AWACS.ROE = { + POLICE = "Police", + VID = "Visual ID", + IFF = "IFF", + BVR = "Beyond Visual Range", +} + +--- +-- @field AWACS.ROT +AWACS.ROT = { + BYPASSESCAPE = "Bypass and Escape", + EVADE = "Evade Fire", + PASSIVE = "Passive Defense", + RETURNFIRE = "Return Fire", + OPENFIRE = "Open Fire", + } + +--- +--@field THREATLEVEL -- can be 1-10, thresholds +AWACS.THREATLEVEL = { + GREEN = 3, + AMBER = 7, + RED = 10, +} + +--- +--@field CapVoices -- Random CAP voices +AWACS.CapVoices = { + [1] = "de-DE-Wavenet-A", + [2] = "de-DE-Wavenet-B", + [3] = "fr-FR-Wavenet-A", + [4] = "fr-FR-Wavenet-B", + [5] = "en-GB-Wavenet-A", + [6] = "en-GB-Wavenet-B", + [7] = "en-GB-Wavenet-D", + [8] = "en-AU-Wavenet-B", + [9] = "en-US-Wavenet-J", + [10] = "en-US-Wavenet-H", +} +--- +-- @type AWACS.MonitoringData +-- @field #string AwacsStateMission +-- @field #string AwacsStateFG +-- @field #boolean AwacsShiftChange +-- @field #string EscortsStateMission +-- @field #string EscortsStateFG +-- @field #boolean EscortsShiftChange +-- @field #number AICAPMax +-- @field #number AICAPCurrent +-- @field #number Airwings +-- @field #number Players +-- @field #number PlayersCheckedin + +--- +-- @type AWACS.MenuStructure +-- @field #boolean menuset +-- @field #string groupname +-- @field Core.Menu#MENU_GROUP basemenu +-- @field Core.Menu#MENU_GROUP_COMMAND checkin +-- @field Core.Menu#MENU_GROUP_COMMAND checkout +-- @field Core.Menu#MENU_GROUP_COMMAND picture +-- @field Core.Menu#MENU_GROUP_COMMAND bogeydope +-- @field Core.Menu#MENU_GROUP_COMMAND declare +-- @field Core.Menu#MENU_GROUP tasking +-- @field Core.Menu#MENU_GROUP_COMMAND showtask +-- @field Core.Menu#MENU_GROUP_COMMAND judy +-- @field Core.Menu#MENU_GROUP_COMMAND unable +-- @field Core.Menu#MENU_GROUP_COMMAND abort +-- @field Core.Menu#MENU_GROUP_COMMAND commit +-- @field Core.Menu#MENU_GROUP vid +-- @field Core.Menu#MENU_GROUP_COMMAND neutral +-- @field Core.Menu#MENU_GROUP_COMMAND hostile +-- @field Core.Menu#MENU_GROUP_COMMAND friendly + +--- Group Data +-- @type AWACS.ManagedGroup +-- @field Wrapper.Group#GROUP Group +-- @field #string GroupName +-- @field Ops.FlightGroup#FLIGHTGROUP FlightGroup for AI +-- @field #boolean IsPlayer +-- @field #boolean IsAI +-- @field #string CallSign +-- @field #number CurrentAuftrag -- Auftragsnummer for AI +-- @field #number CurrentTask -- ManagedTask ID +-- @field #boolean HasAssignedTask +-- @field #number GID +-- @field #number AnchorStackNo +-- @field #number AnchorStackAngels +-- @field #number ContactCID +-- @field Core.Point#COORDINATE LastKnownPosition +-- @field #number LastTasking TimeStamp + +--- Contact Data +-- @type AWACS.ManagedContact +-- @field #number CID +-- @field Ops.Intelligence#INTEL.Contact Contact +-- @field Ops.Intelligence#INTEL.Cluster Cluster +-- @field #string IFF -- ID'ed or not (yet) +-- @field Ops.Target#TARGET Target +-- @field #number LinkedTask --> TID +-- @field #number LinkedGroup --> GID +-- @field #string Status - #AWACS.TaskStatus +-- @field #string TargetGroupNaming -- Alpha, Charlie +-- @field #string ReportingName -- NATO platform name +-- @field #string EngagementTag +-- @field #boolean TACCallDone +-- @field #boolean MeldCallDone +-- @field #boolean MergeCallDone + +--- +-- @type AWACS.TaskDescription +AWACS.TaskDescription = { + ANCHOR = "Anchor", + REANCHOR = "Re-Anchor", + VID = "VID", + IFF = "IFF", + INTERCEPT = "Intercept", + SWEEP = "Sweep", + RTB = "RTB", +} + +--- +-- @type AWACS.TaskStatus +AWACS.TaskStatus = { + IDLE = "Idle", + UNASSIGNED = "Unassigned", + REQUESTED = "Requested", + ASSIGNED = "Assigned", + EXECUTING = "Executing", + SUCCESS = "Success", + FAILED = "Failed", + DEAD = "Dead", +} + +--- +-- @type AWACS.ManagedTask +-- @field #number TID +-- @field #number AssignedGroupID +-- @field #boolean IsPlayerTask +-- @field #boolean IsUnassigned +-- @field Ops.Target#TARGET Target +-- @field Ops.Auftrag#AUFTRAG Auftrag +-- @field #AWACS.TaskStatus Status +-- @field #AWACS.TaskDescription ToDo +-- @field #string ScreenText Long descrition +-- @field Ops.Intelligence#INTEL.Contact Contact +-- @field Ops.Intelligence#INTEL.Cluster Cluster +-- @field #number CurrentAuftrag +-- @field #number RequestedTimestamp + +--- +-- @type AWACS.AnchorAssignedEntry +-- @field #number ID +-- @field #number Angels + +--- +-- @type AWACS.AnchorData +-- @field #number AnchorBaseAngels +-- @field Core.Zone#ZONE_RADIUS StationZone +-- @field Core.Point#COORDINATE StationZoneCoordinate +-- @field #string StationZoneCoordinateText +-- @field #string StationName +-- @field Utilities.FiFo#FIFO AnchorAssignedID FiFo of #AWACS.AnchorAssignedEntry +-- @field Utilities.FiFo#FIFO Anchors FiFo of available stacks +-- @field Wrapper.Marker#MARKER AnchorMarker Tag for this station + +--- +--@type RadioEntry +--@field #string TextTTS +--@field #string TextScreen +--@field #boolean IsNew +--@field #boolean IsGroup +--@field #boolean GroupID +--@field #number Duration +--@field #boolean ToScreen +--@field #boolean FromAI + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO-List 0.2.33 +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- +-- DONE - WIP - Player tasking, VID +-- TODO - Localization (sensible?) +-- TODO - (LOW) LotATC +-- TODO - SW Optimization +-- WONTDO - Maybe check in AI only when airborne +-- DONE - remove SSML tag when not on google (currently sometimes spoken) +-- DONE - Maybe - Assign specific number of AI CAP to a station +-- DONE - Multiple AIRWING connection? Can't really get recruit to work, switched to random round robin +-- DONE - System for Players to VID contacts? +-- DONE - Task reassignment - if a player reject a task, don't choose him again for 3 minutes +-- DONE - added SSML tags to make google readouts nicer +-- DONE - 2nd audio queue for priority messages +-- DONE - (WIP) Missile launch callout +-- DONE - Event detection, Player joining, eject, crash, dead, leaving; AI shot -> DEFEND +-- DONE - AI Tasking +-- DONE - Shift Change, Change on asset RTB or dead or mission done (done for AWACS and Escorts) +-- DONE - TripWire - WIP - Threat (35nm), Meld (45nm, on mission), Merged (<3nm) +-- +-- DONE - Escorts via AirWing not staying on +-- DONE - Borders for INTEL. Optional, i.e. land based defense within borders +-- DONE - Use AO as Anchor of Bulls, AO as default +-- DONE - SRS TTS output +-- DONE - Check-In/Out Humans +-- DONE - Check-In/Out AI +-- DONE - Picture +-- DONE - Declare +-- DONE - Bogey Dope +-- DONE - Radio Menu +-- DONE - Intel Detection +-- DONE - ROE +-- DONE - Anchor Stack Management +-- DONE - Shift Length AWACS/AI +-- DONE - (WIP) Reporting + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO Constructor + +--- Set up a new AI AWACS. +-- @param #AWACS self +-- @param #string Name Name of this AWACS for the radio menu. +-- @param #string AirWing The core Ops.AirWing#AIRWING managing the AWACS, Escort and (optionally) AI CAP planes for us. +-- @param #number Coalition Coalition, e.g. coalition.side.BLUE. Can also be passed as "blue", "red" or "neutral". +-- @param #string AirbaseName Name of the home airbase. +-- @param #string AwacsOrbit Name of the round, mission editor created zone where this AWACS orbits. +-- @param #string OpsZone Name of the round, mission editor created Fighter Engagement operations zone (FEZ) this AWACS controls. Can be passed as #ZONE_POLYGON. +-- The name of the zone will be used in reference calls as bulls eye name, so ensure a radio friendly name that does not collide with NATOPS keywords. +-- @param #string StationZone Name of the round, mission editor created anchor zone where CAP groups will be stationed. Usually a short city name. +-- @param #number Frequency Radio frequency, e.g. 271. +-- @param #number Modulation Radio modulation, e.g. radio.modulation.AM or radio.modulation.FM. +-- @return #AWACS self +-- @usage +-- You can set up the OpsZone/FEZ in a number of ways: +-- * As a string denominating a normal, round zone you have created and named in the mission editor, e.g. "Rock". +-- * As a polygon zone, defined e.g. like `ZONE_POLYGON:New("Rock",GROUP:FindByName("RockZone"))` where "RockZone" is the name of a late activated helo, and it\'s waypoints (not more than 10) describe a closed polygon zone in the mission editor. +-- * As a string denominating a polygon zone from the mission editor (same late activated helo, but named "Rock#ZONE_POLYGON" in the mission editor. Here, Moose will auto-create a polygon zone when loading, and name it "Rock". Pass as `ZONE:FindByName("Rock")`. +function AWACS:New(Name,AirWing,Coalition,AirbaseName,AwacsOrbit,OpsZone,StationZone,Frequency,Modulation) + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) + + --set Coalition + if Coalition and type(Coalition)=="string" then + if Coalition=="blue" then + self.coalition=coalition.side.BLUE + self.coalitiontxt = Coalition + elseif Coalition=="red" then + self.coalition=coalition.side.RED + self.coalitiontxt = Coalition + elseif Coalition=="neutral" then + self.coalition=coalition.side.NEUTRAL + self.coalitiontxt = Coalition + else + self:E("ERROR: Unknown coalition in AWACS!") + end + else + self.coalition = Coalition + self.coalitiontxt = string.lower(UTILS.GetCoalitionName(self.coalition)) + end + + -- base setup + self.Name = Name -- #string + self.AirWing = AirWing -- Ops.AirWing#AIRWING object + + AirWing:SetUsingOpsAwacs(self) + + self.CAPAirwings = FIFO:New() -- Utilities.FiFo#FIFO + self.CAPAirwings:Push(AirWing,1) + + self.AwacsFG = nil + --self.AwacsPayload = PayLoad -- Ops.AirWing#AIRWING.Payload + --self.ModernEra = true -- use of EPLRS + self.RadarBlur = 15 -- +/-15% detection precision i.e. 85-115 reported group size + if type(OpsZone) == "string" then + self.OpsZone = ZONE:New(OpsZone) -- Core.Zone#ZONE + elseif type(OpsZone) == "table" and OpsZone.ClassName and string.find(OpsZone.ClassName,"ZONE") then + self.OpsZone = OpsZone + else + self:E("AWACS - Invalid OpsZone passed!") + return + end + + --self.AOCoordinate = self.OpsZone:GetCoordinate() + self.AOCoordinate = COORDINATE:NewFromVec3( coalition.getMainRefPoint( self.coalition ) ) -- bulls eye from ME + self.AOName = self.OpsZone:GetName() + self.UseBullsAO = true -- as per NATOPS + self.ControlZoneRadius = 100 -- nm + self.StationZone = ZONE:New(StationZone) -- Core.Zone#ZONE + self.StationZoneName = StationZone + self.Frequency = Frequency or 271 -- #number + self.Modulation = Modulation or radio.modulation.AM + self.Airbase = AIRBASE:FindByName(AirbaseName) + self.AwacsAngels = 25 -- orbit at 25'000 ft + if AwacsOrbit then + self.OrbitZone = ZONE:New(AwacsOrbit) -- Core.Zone#ZONE + end + self.BorderZone = nil + self.CallSign = CALLSIGN.AWACS.Magic -- #number + self.CallSignNo = 1 -- #number + self.NoHelos = true + self.AIRequested = 0 + self.AIonCAP = 0 + self.AICAPMissions = FIFO:New() -- Utilities.FiFo#FIFO + self.FlightGroups = FIFO:New() -- Utilities.FiFo#FIFO + self.Countactcounter = 0 + + self.PictureInterval = 300 -- picture every 5s mins + self.PictureTimeStamp = 0 -- timestamp + self.ReassignTime = 120 -- time for player re-assignment + + self.intelstarted = false + self.sunrisedone = false + + local speed = 250 + self.SpeedBase = speed + self.Speed = UTILS.KnotsToAltKIAS(speed,self.AwacsAngels*1000) + + self.Heading = 0 -- north + self.Leg = 50 -- nm + self.invisible = false + self.immortal = false + self.callsigntxt = "AWACS" + + self.AwacsTimeOnStation = 4 + self.AwacsTimeStamp = 0 + self.EscortsTimeOnStation = 4 + self.EscortsTimeStamp = 0 + self.ShiftChangeTime = 0.25 -- 15mins + self.ShiftChangeAwacsFlag = false + self.ShiftChangeEscortsFlag = false + + self.CapSpeedBase = 270 + self.CAPTimeOnStation = 4 + self.MaxAIonCAP = 4 + self.AICAPCAllName = CALLSIGN.Aircraft.Colt + self.AICAPCAllNumber = 0 + self.CAPGender = "male" + self.CAPCulture = "en-US" + self.CAPVoice = nil + + self.AwacsMission = nil + self.AwacsInZone = false -- not yet arrived or gone again + self.AwacsReady = false + + self.AwacsROE = AWACS.ROE.IFF + self.AwacsROT = AWACS.ROT.BYPASSESCAPE + + -- Escorts + self.HasEscorts = false + self.EscortTemplate = "" + self.EscortMission = {} + self.EscortMissionReplacement = {} + + -- SRS + self.PathToSRS = "C:\\Program Files\\DCS-SimpleRadio-Standalone" + self.Gender = "female" + self.Culture = "en-GB" + self.Voice = nil + self.Port = 5002 + self.Volume = 1.0 + self.RadioQueue = FIFO:New() -- Utilities.FiFo#FIFO + self.PrioRadioQueue = FIFO:New() -- Utilities.FiFo#FIFO + self.maxspeakentries = 3 + self.GoogleTTSPadding = 1 + self.WindowsTTSPadding = 2.5 + + -- Client SET + self.clientset = SET_CLIENT:New():FilterActive(true):FilterCoalitions(self.coalitiontxt):FilterCategories("plane"):FilterStart() + + -- Player options + self.PlayerGuidance = true + self.ModernEra = true + self.NoGroupTags = false + self.SuppressScreenOutput = false + self.ReassignmentPause = 180 + self.callsignshort = true + self.DeclareRadius = 5 -- NM + self.MenuStrict = true + self.maxassigndistance = 100 --nm + self.NoMissileCalls = true + self.PlayerCapAssigment = true + + -- managed groups + self.ManagedGrps = {} -- #table of #AWACS.ManagedGroup entries + self.ManagedGrpID = 0 + self.callsignTranslations = nil + + -- Anchor stacks init + self.AnchorStacks = FIFO:New() -- Utilities.FiFo#FIFO + self.AnchorBaseAngels = 22 + self.AnchorStackDistance = 2 + self.AnchorMaxStacks = 4 + self.AnchorMaxAnchors = 2 + self.AnchorMaxZones = 6 + self.AnchorCurrZones = 1 + self.AnchorTurn = -(360/self.AnchorMaxZones) + + self:_CreateAnchorStack() + + -- Task lists + self.ManagedTasks = FIFO:New() -- Utilities.FiFo#FIFO + --self.OpenTasks = FIFO:New() -- Utilities.FiFo#FIFO + + -- Monitoring, init + local MonitoringData = {} -- #AWACS.MonitoringData + MonitoringData.AICAPCurrent = 0 + MonitoringData.AICAPMax = self.MaxAIonCAP + MonitoringData.Airwings = 1 + MonitoringData.PlayersCheckedin = 0 + MonitoringData.Players = 0 + MonitoringData.AwacsShiftChange = false + MonitoringData.AwacsStateFG = "unknown" + MonitoringData.AwacsStateMission = "unknown" + MonitoringData.EscortsShiftChange = false + MonitoringData.EscortsStateFG= "unknown" + MonitoringData.EscortsStateMission = "unknown" + self.MonitoringOn = false -- #boolean + self.MonitoringData = MonitoringData + + self.CatchAllMissions = {} + self.CatchAllFGs = {} + + -- Picture, Contacts, Bogeys + self.PictureAO = FIFO:New() -- Utilities.FiFo#FIFO + self.PictureEWR = FIFO:New() -- Utilities.FiFo#FIFO + self.Contacts = FIFO:New() -- Utilities.FiFo#FIFO + --self.ManagedContacts = FIFO:New() + self.CID = 0 + self.ContactsAO = FIFO:New() -- Utilities.FiFo#FIFO + + self.clientmenus = FIFO:New() -- Utilities.FiFo#FIFO + + -- SET for Intel Detection + self.DetectionSet=SET_GROUP:New() + + -- Set some string id for output to DCS.log file. + self.lid=string.format("%s (%s) | ", self.Name, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown") + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "StartUp") -- Start FSM. + self:AddTransition("StartUp", "Started", "Running") + self:AddTransition("*", "Status", "*") -- Status update. + self:AddTransition("*", "CheckedIn", "*") + self:AddTransition("*", "CheckedOut", "*") + self:AddTransition("*", "AssignAnchor", "*") + self:AddTransition("*", "AssignedAnchor", "*") + self:AddTransition("*", "ReAnchor", "*") + self:AddTransition("*", "NewCluster", "*") + self:AddTransition("*", "NewContact", "*") + self:AddTransition("*", "LostCluster", "*") + self:AddTransition("*", "LostContact", "*") + self:AddTransition("*", "CheckRadioQueue", "*") + self:AddTransition("*", "EscortShiftChange", "*") + self:AddTransition("*", "AwacsShiftChange", "*") + self:AddTransition("*", "FlightOnMission", "*") + self:AddTransition("*", "Intercept", "*") + self:AddTransition("*", "InterceptSuccess", "*") + self:AddTransition("*", "InterceptFailure", "*") + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + + local text = string.format("%sAWACS Version %s Initiated",self.lid,self.version) + + self:I(text) + + -- Events + -- Player joins + self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler) + self:HandleEvent(EVENTS.PlayerEnterUnit, self._EventHandler) + -- Player leaves + self:HandleEvent(EVENTS.PlayerLeaveUnit, self._EventHandler) + self:HandleEvent(EVENTS.Ejection, self._EventHandler) + self:HandleEvent(EVENTS.Crash, self._EventHandler) + self:HandleEvent(EVENTS.Dead, self._EventHandler) + self:HandleEvent(EVENTS.UnitLost, self._EventHandler) + self:HandleEvent(EVENTS.BDA, self._EventHandler) + self:HandleEvent(EVENTS.PilotDead, self._EventHandler) + -- Missile warning + self:HandleEvent(EVENTS.Shot, self._EventHandler) + + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the AWACS. Initializes parameters and starts event handlers. + -- @function [parent=#AWACS] Start + -- @param #AWACS self + + --- Triggers the FSM event "Start" after a delay. Starts the AWACS. Initializes parameters and starts event handlers. + -- @function [parent=#AWACS] __Start + -- @param #AWACS self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the AWACS and all its event handlers. + -- @param #AWACS self + + --- Triggers the FSM event "Stop" after a delay. Stops the AWACS and all its event handlers. + -- @function [parent=#AWACS] __Stop + -- @param #AWACS self + -- @param #number delay Delay in seconds. + + --- On After "CheckedIn" event. AI or Player checked in. + -- @function [parent=#AWACS] OnAfterCheckedIn + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "CheckedOut" event. AI or Player checked out. + -- @function [parent=#AWACS] OnAfterCheckedOut + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "AssignedAnchor" event. AI or Player has been assigned a CAP station. + -- @function [parent=#AWACS] OnAfterAssignedAnchor + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "ReAnchor" event. AI or Player has been send back to station. + -- @function [parent=#AWACS] OnAfterReAnchor + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "NewCluster" event. AWACS detected a cluster. + -- @function [parent=#AWACS] OnAfterNewCluster + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "NewContact" event. AWACS detected a contact. + -- @function [parent=#AWACS] OnAfterNewContact + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "LostCluster" event. AWACS lost a radar cluster. + -- @function [parent=#AWACS] OnAfterLostCluster + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "LostContact" event. AWACS lost a radar contact. + -- @function [parent=#AWACS] OnAfterLostContact + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "EscortShiftChange" event. AWACS escorts shift change. + -- @function [parent=#AWACS] OnAfterEscortShiftChange + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "AwacsShiftChange" event. AWACS shift change. + -- @function [parent=#AWACS] OnAfterAwacsShiftChange + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "Intercept" event. CAP send on intercept. + -- @function [parent=#AWACS] OnAfterIntercept + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "InterceptSuccess" event. Intercept successful. + -- @function [parent=#AWACS] OnAfterIntercept + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "InterceptFailure" event. Intercept failure. + -- @function [parent=#AWACS] OnAfterIntercept + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + return self +end + +-- TODO Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- [User] Set this instance to act as GCI TACS Theater Air Control System +-- @param #AWACS self +-- @param Wrapper.Group#GROUP EWR The **main** Early Warning Radar (EWR) GROUP object for GCI. +-- @param #number Delay (option) Start after this many seconds (optional). +-- @return #AWACS self +function AWACS:SetAsGCI(EWR,Delay) + self:T(self.lid.."SetGCI") + local delay = Delay or -5 + if type(EWR) == "string" then + self.GCIGroup = GROUP:FindByName(EWR) + else + self.GCIGroup = EWR + end + self.GCI = true + self:SetEscort(0) + return self +end + +--- [Internal] Create a AIC-TTS message entry +-- @param #AWACS self +-- @param #string TextTTS Text to speak +-- @param #string TextScreen Text for screen +-- @param #number GID Group ID #AWACS.ManagedGroup GID +-- @param #boolean IsGroup Has a group +-- @param #boolean ToScreen Show on screen +-- @param #boolean IsNew New +-- @param #boolean FromAI From AI +-- @param #boolean IsPrio Priority entry +-- @return #AWACS self +function AWACS:_NewRadioEntry(TextTTS, TextScreen,GID,IsGroup,ToScreen,IsNew,FromAI,IsPrio) + self:T(self.lid.."_NewRadioEntry") + local RadioEntry = {} -- #AWACS.RadioEntry + RadioEntry.IsNew = IsNew + RadioEntry.TextTTS = TextTTS + RadioEntry.TextScreen = TextScreen + RadioEntry.GroupID = GID + RadioEntry.ToScreen = ToScreen + RadioEntry.Duration = STTS.getSpeechTime(TextTTS,0.95,false) or 8 + RadioEntry.FromAI = FromAI + RadioEntry.IsGroup = IsGroup + if IsPrio then + self.PrioRadioQueue:Push(RadioEntry) + else + self.RadioQueue:Push(RadioEntry) + end + return self +end + +--- [User] Change the bulls eye alias for AWACS callout. Defaults to "Rock" +-- @param #AWACS self +-- @param #string Name +-- @return #AWACS self +function AWACS:SetBullsEyeAlias(Name) + self:T(self.lid.."_SetBullsEyeAlias") + self.AOName = Name or "Rock" + return self +end + +--- [User] Set TOS Time-on-Station in Hours +-- @param #AWACS self +-- @param #number AICHours AWACS stays this number of hours on station before shift change, default is 4. +-- @param #number CapHours (optional) CAP stays this number of hours on station before shift change, default is 4. +-- @return #AWACS self +function AWACS:SetTOS(AICHours,CapHours) + self:T(self.lid.."SetTOS") + self.AwacsTimeOnStation = AICHours or 4 + self.CAPTimeOnStation = CapHours or 4 + return self +end + +--- [User] Change number of seconds AWACS waits until a Player is re-assigned a different task. Defaults to 180. +-- @param #AWACS self +-- @param #number Seconds +-- @return #AWACS self +function AWACS:SetReassignmentPause(Seconds) + self.ReassignmentPause = Seconds or 180 + return self +end + +--- [User] Do not show messages on screen +-- @param #AWACS self +-- @param #boolean Switch If true, no messages will be shown on screen. +-- @return #AWACS self +function AWACS:SuppressScreenMessages(Switch) + self:T(self.lid.."_SetBullsEyeAlias") + self.SuppressScreenOutput = Switch or false + return self +end + +--- [User] Do not show messages on screen, no extra calls for player guidance, use short callsigns etc. +-- @param #AWACS self +-- @return #AWACS self +function AWACS:ZipLip() + self:T(self.lid.."ZipLip") + self:SuppressScreenMessages(true) + self.PlayerGuidance = false + self.callsignshort = true + --self.NoGroupTags = true + self.NoMissileCalls = true + return self +end + +--- [User] For CAP flights: Replace ME callsigns with user-defined callsigns for use with TTS and on-screen messaging +-- @param #AWACS self +-- @param #table translationTable with DCS callsigns as keys and replacements as values +-- @return #AWACS self +-- @usage +-- -- Set Custom CAP Flight Callsigns for use with TTS +-- testawacs:SetCustomCallsigns({ +-- Devil = 'Bengal', +-- Snake = 'Winder', +-- Colt = 'Camelot', +-- Enfield = 'Victory', +-- Uzi = 'Evil Eye' +-- }) +function AWACS:SetCustomCallsigns(translationTable) + self.callsignTranslations = translationTable +end + +--- [Internal] Event handler +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group, can also be passed as #string group name +-- @return #boolean found +-- @return #number GID +-- @return #string CallSign +function AWACS:_GetGIDFromGroupOrName(Group) + self:T(self.lid.."_GetGIDFromGroupOrName") + self:T({Group}) + local GID = 0 + local Outcome = false + local CallSign = "Ghost 1" + local nametocheck = CallSign + if Group and type(Group) == "string" then + nametocheck = Group + elseif Group and Group:IsInstanceOf("GROUP") then + nametocheck = Group:GetName() + else + return false, 0, CallSign + end + + local managedgrps = self.ManagedGrps or {} + for _,_managed in pairs (managedgrps) do + local managed = _managed -- #AWACS.ManagedGroup + if managed.GroupName == nametocheck then + GID = managed.GID + Outcome = true + CallSign = managed.CallSign + end + end + self:T({Outcome, GID, CallSign}) + return Outcome, GID, CallSign +end + +--- [Internal] Event handler +-- @param #AWACS self +-- @param Core.Event#EVENTDATA EventData +-- @return #AWACS self +function AWACS:_EventHandler(EventData) + self:T(self.lid.."_EventHandler") + self:T({Event = EventData.id}) + + local Event = EventData -- Core.Event#EVENTDATA + + if Event.id == EVENTS.PlayerEnterAircraft or Event.id == EVENTS.PlayerEnterUnit then --player entered unit + --self:T("Player enter unit: " .. Event.IniPlayerName) + --self:T("Coalition = " .. UTILS.GetCoalitionName(Event.IniCoalition)) + if Event.IniCoalition == self.coalition then + self:_SetClientMenus() + end + end + + if Event.id == EVENTS.PlayerLeaveUnit then --player left unit + -- check known player? + self:T("Player group left unit: " .. Event.IniGroupName) + self:T("Player name left: " .. Event.IniPlayerName) + self:T("Coalition = " .. UTILS.GetCoalitionName(Event.IniCoalition)) + if Event.IniCoalition == self.coalition then + local Outcome, GID, CallSign = self:_GetGIDFromGroupOrName(Event.IniGroupName) + if Outcome and GID > 0 then + self:T("Task Abort and Checkout Called") + self:_TaskAbort(Event.IniGroupName) + self:_CheckOut(nil,GID,true) + end + end + end + + if Event.id == EVENTS.Ejection or Event.id == EVENTS.Crash or Event.id == EVENTS.Dead or Event.id == EVENTS.PilotDead then --unit or player dead + -- check known group? + if Event.IniCoalition == self.coalition then + --self:T("Ejection/Crash/Dead/PilotDead Group: " .. Event.IniGroupName) + --self:T("Coalition = " .. UTILS.GetCoalitionName(Event.IniCoalition)) + local Outcome, GID, CallSign = self:_GetGIDFromGroupOrName(Event.IniGroupName) + if Outcome and GID > 0 then + self:_TaskAbort(Event.IniGroupName) + self:_CheckOut(nil,GID,true) + end + end + end + + if Event.id == EVENTS.Shot and self.PlayerGuidance and not self.NoMissileCalls then + if Event.IniCoalition ~= self.coalition then + self:T("Shot from: " .. Event.IniGroupName) + --self:T(UTILS.OneLineSerialize(Event)) + local position = Event.IniGroup:GetCoordinate() + if not position then return self end + --self:T("Coalition = " .. UTILS.GetCoalitionName(Event.IniCoalition)) + -- Check missile type + local Category = Event.WeaponCategory + local WeaponDesc = EventData.Weapon:getDesc() -- https://wiki.hoggitworld.com/view/DCS_enum_weapon + self:T({WeaponDesc}) + --self:T("Weapon = " .. tostring(WeaponDesc.displayName)) + if WeaponDesc.category == 1 and (WeaponDesc.missileCategory == 1 or WeaponDesc.missileCategory == 2) then + self:T("AAM or SAM Missile fired") + -- Missile fired + -- WIP Missile Callouts + local warndist = 25 + local Type = "SAM" + if WeaponDesc.category == 1 then + Type = "Missile" + -- AAM + local guidance = WeaponDesc.guidance -- IR=2, Radar Active=3, Radar Semi Active=4, Radar Passive = 5 + if guidance == 2 then + warndist = 10 + elseif guidance == 3 then + warndist = 25 + elseif guidance == 4 then + warndist = 15 + elseif guidance == 5 then + warndist = 10 + end -- guidance + end -- cat 1 + self:_MissileWarning(position,Type,warndist) + end -- cat 1 or 2 + + end -- end coalition + end -- end shot + + return self +end + +--- [Internal] Missile Warning Callout +-- @param #AWACS self +-- @param Core.Point#COORDINATE Coordinate Where the shot happened +-- @param #string Type Type to call out, e.i. "SAM" or "Missile" +-- @param #number Warndist Distance in NM to find friendly planes +-- @return #AWACS self +function AWACS:_MissileWarning(Coordinate,Type,Warndist) + self:T(self.lid.."_MissileWarning Type="..Type.." WarnDist="..Warndist) + --self:T(UTILS.OneLineSerialize(Coordinate)) + if not Coordinate then return self end + local shotzone = ZONE_RADIUS:New("WarningZone",Coordinate:GetVec2(),UTILS.NMToMeters(Warndist)) + local targetgrpset = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterCategoryAirplane():FilterActive():FilterZones({shotzone}):FilterOnce() + if targetgrpset:Count() > 0 then + local targets = targetgrpset:GetSetObjects() + for _,_grp in pairs (targets) do + -- TODO -- player callouts only + if _grp and _grp:IsAlive() then + local isPlayer = _grp:IsPlayer() + --if self.debug or isPlayer then + if isPlayer then + local callsign = self:_GetCallSign(_grp) + local text = string.format("%s, %s! %s! %s! Defend!",callsign,Type,Type,Type) + self:_NewRadioEntry(text, text,0,false,self.debug,true,false,true) + end + end + end + end + return self +end + +--- [User] Set AWACS Radar Blur - the radar contact count per group/cluster will be distored up or down by this number percent. Defaults to 15 in Modern Era and 25 in Cold War. +-- @param #AWACS self +-- @param #number Percent +-- @return #AWACS self +function AWACS:SetRadarBlur(Percent) + local percent = Percent or 15 + if percent < 0 then percent = 0 end + if percent > 100 then percent = 100 end + self.RadarBlur = Percent + return self +end + +--- [User] Set AWACS to Cold War standards - ROE to VID, ROT to Passive (bypass and escape). Radar blur 25%. +-- Sets TAC/Meld/Threat call distances to 35, 25 and 15 nm. +-- @param #AWACS self +-- @return #AWACS self +function AWACS:SetColdWar() + self.ModernEra = false + self.AwacsROT = AWACS.ROT.PASSIVE + self.AwacsROE = AWACS.ROE.VID + self.RadarBlur = 25 + self:SetInterceptTimeline(35, 25, 15) + return self +end + +--- [User] Set AWACS to Modern Era standards - ROE to BVR, ROT to defensive (evade fire). Radar blur 15%. +-- @param #AWACS self +-- @return #AWACS self +function AWACS:SetModernEra() + self.ModernEra = true + self.AwacsROT = AWACS.ROT.EVADE + self.AwacsROE = AWACS.ROE.BVR + self.RadarBlur = 15 + return self +end + +--- [User] Set AWACS to Modern Era standards - ROE to IFF, ROT to defensive (evade fire). Radar blur 15%. +-- @param #AWACS self +-- @return #AWACS self +function AWACS:SetModernEraDefensive() + self.ModernEra = true + self.AwacsROT = AWACS.ROT.EVADE + self.AwacsROE = AWACS.ROE.IFF + self.RadarBlur = 15 + return self +end + +--- [User] Set AWACS to Modern Era standards - ROE to BVR, ROT to return fire. Radar blur 15%. +-- @param #AWACS self +-- @return #AWACS self +function AWACS:SetModernEraAgressive() + self.ModernEra = true + self.AwacsROT = AWACS.ROT.RETURNFIRE + self.AwacsROE = AWACS.ROE.BVR + self.RadarBlur = 15 + return self +end + +--- [User] Set AWACS to Policing standards - ROE to VID, ROT to Lock (bypass and escape). Radar blur 15%. +-- @param #AWACS self +-- @return #AWACS self +function AWACS:SetPolicingModern() + self.ModernEra = true + self.AwacsROT = AWACS.ROT.BYPASSESCAPE + self.AwacsROE = AWACS.ROE.VID + self.RadarBlur = 15 + return self +end + +--- [User] Set AWACS to Policing standards - ROE to VID, ROT to Lock (bypass and escape). Radar blur 25%. +-- Sets TAC/Meld/Threat call distances to 35, 25 and 15 nm. +-- @param #AWACS self +-- @return #AWACS self +function AWACS:SetPolicingColdWar() + self.ModernEra = false + self.AwacsROT = AWACS.ROT.BYPASSESCAPE + self.AwacsROE = AWACS.ROE.VID + self.RadarBlur = 25 + self:SetInterceptTimeline(35, 25, 15) + return self +end + +--- [User] Set AWACS Player Guidance - influences missile callout and the "New" label in group callouts. +-- @param #AWACS self +-- @param #boolean Switch If true (default) it is on, if false, it is off. +-- @return #AWACS self +function AWACS:SetPlayerGuidance(Switch) + if (Switch == nil) or (Switch == true) then + self.PlayerGuidance = true + else + self.PlayerGuidance = false + end + return self +end + +--- [User] Get AWACS Name +-- @param #AWACS self +-- @return #string Name of this instance +function AWACS:GetName() + return self.Name or "not set" +end + +--- [User] Set AWACS intercept timeline support distance. +-- @param #AWACS self +-- @param #number TacDistance Distance for TAC call, default 45nm +-- @param #number MeldDistance Distance for Meld call, default 35nm +-- @param #number ThreatDistance Distance for Threat call, default 25nm +-- @return #AWACS self +function AWACS:SetInterceptTimeline(TacDistance, MeldDistance, ThreatDistance) + self.TacDistance = TacDistance or 45 + self.MeldDistance = MeldDistance or 35 + self.ThreatDistance = ThreatDistance or 25 + return self +end + +--- [User] Set additional defensive zone, e.g. the zone behind the FEZ to also be defended +-- @param #AWACS self +-- @param Core.Zone#ZONE Zone +-- @param #boolean Draw Draw lines around this zone if true +-- @return #AWACS self +function AWACS:SetAdditionalZone(Zone, Draw) + self:T(self.lid.."SetAdditionalZone") + self.BorderZone = Zone + if self.debug then + Zone:DrawZone(-1,{1,0.64,0},1,{1,0.64,0},0.2,1,true) + MARKER:New(Zone:GetCoordinate(),"Defensive Zone"):ToAll() + elseif Draw then + Zone:DrawZone(-1,{1,0.64,0},1,{1,0.64,0},0.2,1,true) + end + return self +end + +--- [User] Set rejection zone, e.g. a border of a foreign country. Detected bogeys in here won't be engaged. +-- @param #AWACS self +-- @param Core.Zone#ZONE Zone +-- @param #boolean Draw Draw lines around this zone if true +-- @return #AWACS self +function AWACS:SetRejectionZone(Zone,Draw) + self:T(self.lid.."SetRejectionZone") + self.RejectZone = Zone + if Draw then + Zone:DrawZone(-1,{1,0.64,0},1,{1,0.64,0},0.2,1,true) + --MARKER:New(Zone:GetCoordinate(),"Rejection Zone"):ToAll() + elseif self.debug then + Zone:DrawZone(-1,{1,0.64,0},1,{1,0.64,0},0.2,1,true) + MARKER:New(Zone:GetCoordinate(),"Rejection Zone"):ToAll() + end + return self +end + +--- [User] Draw a line around the FEZ on the F10 map. +-- @param #AWACS self +-- @return #AWACS self +function AWACS:DrawFEZ() + self.OpsZone:DrawZone(-1,{1,0,0},1,{1,0,0},0.2,5,true) + return self +end + +--- [User] Set AWACS flight details +-- @param #AWACS self +-- @param #number CallSign Defaults to CALLSIGN.AWACS.Magic +-- @param #number CallSignNo Defaults to 1 +-- @param #number Angels Defaults to 25 (i.e. 25000 ft) +-- @param #number Speed Defaults to 250kn +-- @param #number Heading Defaults to 0 (North) +-- @param #number Leg Defaults to 25nm +-- @return #AWACS self +function AWACS:SetAwacsDetails(CallSign,CallSignNo,Angels,Speed,Heading,Leg) + self:T(self.lid.."SetAwacsDetails") + self.CallSign = CallSign or CALLSIGN.AWACS.Magic + self.CallSignNo = CallSignNo or 1 + self.Angels = Angels or 25 + local speed = Speed or 250 + self.SpeedBase = speed + self.Speed = UTILS.KnotsToAltKIAS(speed,self.Angels*1000) + self.Heading = Heading or 0 + self.Leg = Leg or 25 + return self +end + +--- [User] Set AWACS custom callsigns for TTS +-- @param #AWACS self +-- @param #table CallsignTable Table of custom callsigns to use with TTS +-- @return #AWACS self +-- @usage +-- You can overwrite the standard AWACS callsign for TTS usage with your own naming, e.g. like so: +-- testawacs:SetCustomAWACSCallSign({ +-- [1]="Overlord", -- Overlord +-- [2]="Bookshelf", -- Magic +-- [3]="Wizard", -- Wizard +-- [4]="Focus", -- Focus +-- [5]="Darkstar", -- Darkstar +-- }) +-- The default callsign used in AWACS is "Magic". With the above change, the AWACS will call itself "Bookshelf" over TTS instead. +function AWACS:SetCustomAWACSCallSign(CallsignTable) + self:T(self.lid.."SetCustomAWACSCallSign") + self.CallSignClear = CallsignTable + return self +end + +--- [User] Add a radar GROUP object to the INTEL detection SET_GROUP +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group The GROUP to be added. Can be passed as SET_GROUP. +-- @return #AWACS self +function AWACS:AddGroupToDetection(Group) + self:T(self.lid.."AddGroupToDetection") + if Group and Group.ClassName and Group.ClassName == "GROUP" then + self.DetectionSet:AddGroup(Group) + elseif Group and Group.ClassName and Group.ClassName == "SET_GROUP" then + self.DetectionSet:AddSet(Group) + end + return self +end + +--- [User] Set AWACS SRS TTS details - see @{Sound.SRS} for details +-- @param #AWACS self +-- @param #string PathToSRS Defaults to "C:\\Program Files\\DCS-SimpleRadio-Standalone" +-- @param #string Gender Defaults to "male" +-- @param #string Culture Defaults to "en-US" +-- @param #number Port Defaults to 5002 +-- @param #string Voice (Optional) Use a specifc voice with the @{Sound.SRS.SetVoice} function, e.g, `:SetVoice("Microsoft Hedda Desktop")`. +-- Note that this must be installed on your windows system. Can also be Google voice types, if you are using Google TTS. +-- @param #number Volume Volume - between 0.0 (silent) and 1.0 (loudest) +-- @param #string PathToGoogleKey Path to your google key if you want to use google TTS +-- @return #AWACS self +function AWACS:SetSRS(PathToSRS,Gender,Culture,Port,Voice,Volume,PathToGoogleKey) + self:T(self.lid.."SetSRS") + self.PathToSRS = PathToSRS or "C:\\Program Files\\DCS-SimpleRadio-Standalone" + self.Gender = Gender or "male" + self.Culture = Culture or "en-US" + self.Port = Port or 5002 + self.Voice = Voice + self.PathToGoogleKey = PathToGoogleKey + self.Volume = Volume or 1.0 + return self +end + +--- [User] Set AWACS Voice Details for AI CAP Planes - SRS TTS - see @{Sound.SRS} for details +-- @param #AWACS self +-- @param #string Gender Defaults to "male" +-- @param #string Culture Defaults to "en-US" +-- @param #string Voice (Optional) Use a specifc voice with the @{#MSRS.SetVoice} function, e.g, `:SetVoice("Microsoft Hedda Desktop")`. +-- Note that this must be installed on your windows system. Can also be Google voice types, if you are using Google TTS. +-- @return #AWACS self +function AWACS:SetSRSVoiceCAP(Gender, Culture, Voice) + self:T(self.lid.."SetSRSVoiceCAP") + self.CAPGender = Gender or "male" + self.CAPCulture = Culture or "en-US" + self.CAPVoice = Voice or "en-GB-Standard-B" + return self +end + +--- [User] Set AI CAP Plane Details +-- @param #AWACS self +-- @param #number Callsign Callsign name of AI CAP, e.g. CALLSIGN.Aircraft.Dodge. Defaults to CALLSIGN.Aircraft.Colt. Note that not all available callsigns work for all plane types. +-- @param #number MaxAICap Maximum number of AI CAP planes on station that AWACS will set up automatically. Default to 4. +-- @param #number TOS Time on station, in hours. AI planes might go back to base earlier if they run out of fuel or missiles. +-- @param #number Speed Airspeed to be used in knots. Will be adjusted to flight height automatically. Defaults to 270. +-- @return #AWACS self +function AWACS:SetAICAPDetails(Callsign,MaxAICap,TOS,Speed) + self:T(self.lid.."SetAICAPDetails") + self.CapSpeedBase = Speed or 270 + self.CAPTimeOnStation = TOS or 4 + self.MaxAIonCAP = MaxAICap or 4 + self.AICAPCAllName = Callsign or CALLSIGN.Aircraft.Colt + return self +end + +--- [User] Set AWACS Escorts Template +-- @param #AWACS self +-- @param #number EscortNumber Number of fighther planes to accompany this AWACS. 0 or nil means no escorts. +-- @return #AWACS self +function AWACS:SetEscort(EscortNumber) + self:T(self.lid.."SetEscort") + if EscortNumber and EscortNumber > 0 then + self.HasEscorts = true + self.EscortNumber = EscortNumber + else + self.HasEscorts = false + self.EscortNumber = 0 + end + return self +end + +--- [Internal] Message a vector BR to a position +-- @param #AWACS self +-- @param #number GID Group GID +-- @param #string Tag (optional) Text to add after Vector, e.g. " to Anchor" - NOTE the leading space +-- @param Core.Point#COORDINATE Coordinate The Coordinate to use +-- @param #number Angels (Optional) Add Angels +-- @return #AWACS self +function AWACS:_MessageVector(GID,Tag,Coordinate,Angels) + self:T(self.lid.."_MessageVector") + + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + local Tag = Tag or "" + + if managedgroup and Coordinate then + + local tocallsign = managedgroup.CallSign or "Ghost 1" + local group = managedgroup.Group + local groupposition = group:GetCoordinate() + + --local BRtext = Coordinate:ToStringBR(groupposition) + local BRtext,BRtextTTS = self:_ToStringBR(groupposition,Coordinate) + + local text = string.format("%s, %s. Vector%s %s",tocallsign, self.callsigntxt,Tag,BRtextTTS) + local textScreen = string.format("%s, %s, Vector%s %s",tocallsign, self.callsigntxt,Tag,BRtext) + + if Angels then + text = text .. ". Angels "..tostring(Angels).."." + textScreen = textScreen .. ". Angels "..tostring(Angels).."." + end + + self:_NewRadioEntry(text,textScreen,0,false,self.debug,true,false) + + end + + return self +end + +--- [Internal] Start AWACS Escorts FlightGroup +-- @param #AWACS self +-- @param #boolean Shiftchange This is a shift change call +-- @return #AWACS self +function AWACS:_StartEscorts(Shiftchange) + self:T(self.lid.."_StartEscorts") + + local AwacsFG = self.AwacsFG -- Ops.FlightGroup#FLIGHTGROUP + local group = AwacsFG:GetGroup() + + local timeonstation = (self.EscortsTimeOnStation + self.ShiftChangeTime) * 3600 -- hours to seconds + for i=1,self.EscortNumber do + -- every + local escort = AUFTRAG:NewESCORT(group, {x= -100*((i + (i%2))/2), y=0, z=(100 + 100*((i + (i%2))/2))*(-1)^i},45,{"Air"}) + escort:SetRequiredAssets(1) + escort:SetTime(nil,timeonstation) + self.AirWing:AddMission(escort) + self.CatchAllMissions[#self.CatchAllMissions+1] = escort + + if Shiftchange then + self.EscortMissionReplacement[i] = mission + else + self.EscortMission[i] = mission + end + end + + return self +end + +--- [Internal] AWACS further Start Settings +-- @param #AWACS self +-- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup +-- @param Ops.Auftrag#AUFTRAG Mission +-- @return #AWACS self +function AWACS:_StartSettings(FlightGroup,Mission) + self:T(self.lid.."_StartSettings") + + local Mission = Mission -- Ops.Auftrag#AUFTRAG + local AwacsFG = FlightGroup -- Ops.FlightGroup#FLIGHTGROUP + + -- Is this our Awacs mission? + if self.AwacsMission:GetName() == Mission:GetName() then + self:T("Setting up Awacs") + AwacsFG:SetDefaultRadio(self.Frequency,self.Modulation,false) + AwacsFG:SwitchRadio(self.Frequency,self.Modulation) + AwacsFG:SetDefaultAltitude(self.AwacsAngels*1000) + AwacsFG:SetHomebase(self.Airbase) + AwacsFG:SetDefaultCallsign(self.CallSign,self.CallSignNo) + AwacsFG:SetDefaultROE(ENUMS.ROE.WeaponHold) + AwacsFG:SetDefaultAlarmstate(AI.Option.Ground.val.ALARM_STATE.GREEN) + AwacsFG:SetDefaultEPLRS(self.ModernEra) + AwacsFG:SetDespawnAfterLanding() + AwacsFG:SetFuelLowRTB(true) + AwacsFG:SetFuelLowThreshold(20) + + local group = AwacsFG:GetGroup() -- Wrapper.Group#GROUP + + group:SetCommandInvisible(self.invisible) + group:SetCommandImmortal(self.immortal) + group:CommandSetCallsign(self.CallSign,self.CallSignNo,2) + group:CommandEPLRS(self.ModernEra,5) + -- Non AWACS does not seem take AWACS CS in DCS Group + + self.AwacsFG = AwacsFG + + self.AwacsFG:SetSRS(self.PathToSRS,self.Gender,self.Culture,self.Voice,self.Port,self.PathToGoogleKey,"AWACS",self.Volume) + self.callsigntxt = string.format("%s",AWACS.CallSignClear[self.CallSign]) + + self:__CheckRadioQueue(10) + + if self.HasEscorts then + --mission:SetRequiredEscorts(self.EscortNumber) + self:_StartEscorts() + end + + self.AwacsTimeStamp = timer.getTime() + self.EscortsTimeStamp = timer.getTime() + + self.PictureTimeStamp = timer.getTime() + 10*60 + + self.AwacsReady = true + -- set FSM to started + self:Started() + + elseif self.ShiftChangeAwacsRequested and self.AwacsMissionReplacement and self.AwacsMissionReplacement:GetName() == Mission:GetName() then + self:I("Setting up Awacs Replacement") + -- manage AWACS Replacement + AwacsFG:SetDefaultRadio(self.Frequency,self.Modulation,false) + AwacsFG:SwitchRadio(self.Frequency,self.Modulation) + AwacsFG:SetDefaultAltitude(self.AwacsAngels*1000) + AwacsFG:SetHomebase(self.Airbase) + self.CallSignNo = self.CallSignNo+1 + AwacsFG:SetDefaultCallsign(self.CallSign,self.CallSignNo) + AwacsFG:SetDefaultROE(ENUMS.ROE.WeaponHold) + AwacsFG:SetDefaultAlarmstate(AI.Option.Ground.val.ALARM_STATE.GREEN) + AwacsFG:SetDefaultEPLRS(self.ModernEra) + AwacsFG:SetDespawnAfterLanding() + AwacsFG:SetFuelLowRTB(true) + AwacsFG:SetFuelLowThreshold(20) + + local group = AwacsFG:GetGroup() -- Wrapper.Group#GROUP + + group:SetCommandInvisible(self.invisible) + group:SetCommandImmortal(self.immortal) + group:CommandSetCallsign(self.CallSign,self.CallSignNo,2) + -- Non AWACS does not seem take AWACS CS in DCS Group + -- group:CommandSetCallsign(CALLSIGN.Aircraft.Pig,self.CallSignNo,2) + + AwacsFG:SetSRS(self.PathToSRS,self.Gender,self.Culture,self.Voice,self.Port,nil,"AWACS") + --self.callsigntxt = string.format("%s %d %d",AWACS.CallSignClear[self.CallSign],1,self.CallSignNo) + self.callsigntxt = string.format("%s",AWACS.CallSignClear[self.CallSign]) + + local text = string.format("%s shift change for %s control.",self.callsigntxt,self.AOName or "Rock") + self:T(self.lid..text) + + AwacsFG:RadioTransmission(text,1,false) + + self.AwacsFG = AwacsFG + + --self:__CheckRadioQueue(10) + + if self.HasEscorts then + --mission:SetRequiredEscorts(self.EscortNumber) + self:_StartEscorts(true) + end + + self.AwacsTimeStamp = timer.getTime() + self.EscortsTimeStamp = timer.getTime() + + self.AwacsReady = true + + end + return self +end + +--- [Internal] Return Bullseye BR for Alpha Check etc, returns e.g. "Rock 021, 16" ("Rock" being the set BE name) +-- @param #AWACS self +-- @param Core.Point#COORDINATE Coordinate +-- @param #boolean ssml Add SSML tag +-- @param #boolean TTS For non-Alpha checks, hand back in format "Rock 0 2 1, 16" +-- @return #string BullseyeBR +function AWACS:_ToStringBULLS( Coordinate, ssml, TTS ) + -- local BullsCoordinate = COORDINATE:NewFromVec3( coalition.getMainRefPoint( self.coalition ) ) + local bullseyename = self.AOName or "Rock" + --local BullsCoordinate = self.OpsZone:GetCoordinate() + local BullsCoordinate = self.AOCoordinate + local DirectionVec3 = BullsCoordinate:GetDirectionVec3( Coordinate ) + local AngleRadians = Coordinate:GetAngleRadians( DirectionVec3 ) + local Distance = Coordinate:Get2DDistance( BullsCoordinate ) + local AngleDegrees = UTILS.Round( UTILS.ToDegree( AngleRadians ), 0 ) + local Bearing = string.format( '%03d', AngleDegrees ) + local Distance = UTILS.Round( UTILS.MetersToNM( Distance ), 0 ) + if ssml then + return string.format("%s %03d, %d",bullseyename,Bearing,Distance) + elseif TTS then + Bearing = self:_ToStringBullsTTS(Bearing) + local BearingTTS = string.gsub(Bearing,"0","zero") + return string.format("%s %s, %d",bullseyename,BearingTTS,Distance) + else + return string.format("%s %s, %d",bullseyename,Bearing,Distance) + end +end + +--- [Internal] Change Bullseye string to be TTS friendly, "Bullseye 021, 16" returns e.g. "Bulls eye 0 2 1. 1 6" +-- @param #AWACS self +-- @param #string Text Input text +-- @return #string BullseyeBRTTS +function AWACS:_ToStringBullsTTS(Text) + local text = Text + text=string.gsub(text,"Bullseye","Bulls eye") + text=string.gsub(text,"%d","%1 ") + text=string.gsub(text," ," ,".") + text=string.gsub(text," $","") + return text +end + + +--- [Internal] Check if a group has checked in +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to check +-- @return #number ID +-- @return #boolean CheckedIn +-- @return #string CallSign +function AWACS:_GetManagedGrpID(Group) + if not Group or not Group:IsAlive() then + self:T(self.lid.."_GetManagedGrpID - Requested Group is not alive!") + return 0,false,"" + end + self:T(self.lid.."_GetManagedGrpID for "..Group:GetName()) + local GID = 0 + local Outcome = false + local CallSign = "Ghost 1" + local nametocheck = Group:GetName() + local managedgrps = self.ManagedGrps or {} + for _,_managed in pairs (managedgrps) do + local managed = _managed -- #AWACS.ManagedGroup + if managed.GroupName == nametocheck then + GID = managed.GID + Outcome = true + CallSign = managed.CallSign + end + end + return GID, Outcome, CallSign +end + +--- [Internal] AWACS Get TTS compatible callsign +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @param #number GID GID to use +-- @return #string Callsign +function AWACS:_GetCallSign(Group,GID) + self:T(self.lid.."_GetCallSign - GID "..tostring(GID)) + + if GID and type(GID) == "number" and GID > 0 then + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + self:T("Saved Callsign for TTS = " .. tostring(managedgroup.CallSign)) + return managedgroup.CallSign + end + + local callsign = "Ghost 1" + if Group and Group:IsAlive() then + local shortcallsign = Group:GetCallsign() or "unknown11"-- e.g.Uzi11, but we want Uzi 1 1 + local callsignroot = string.match(shortcallsign, '(%a+)') + if self.callsignTranslations and self.callsignTranslations[callsignroot] then + shortcallsign = string.gsub(shortcallsign, callsignroot, self.callsignTranslations[callsignroot]) + end + + local groupname = Group:GetName() + local callnumber = string.match(shortcallsign, "(%d+)$" ) or "unknown11" + local callnumbermajor = string.char(string.byte(callnumber,1)) + local callnumberminor = string.char(string.byte(callnumber,2)) + if string.find(groupname,"#") then + -- personalized flight name in group naming + shortcallsign = string.match(groupname,"#([%a]+)") + end + if self.callsignshort then + callsign = string.gsub(shortcallsign,callnumber,"").." "..callnumbermajor + else + callsign = string.gsub(shortcallsign,callnumber,"").." "..callnumbermajor.." "..callnumberminor + end + self:T("Generated Callsign for TTS = " .. callsign) + end + + return callsign +end + +--- [Internal] Update contact from cluster data +-- @param #AWACS self +-- @param #number CID Contact ID +-- @return #AWACS self +function AWACS:_UpdateContactFromCluster(CID) + self:T(self.lid.."_UpdateContactFromCluster CID="..CID) + + local existingcontact = self.Contacts:PullByID(CID) -- #AWACS.ManagedContact + local ContactTable = existingcontact.Cluster.Contacts or {} + + local function GetFirstAliveContact(table) + for _,_contact in pairs (table) do + local contact = _contact -- Ops.Intelligence#INTEL.Contact + if contact and contact.group and contact.group:IsAlive() then + return contact + end + end + return nil + end + + local NewContact = GetFirstAliveContact(ContactTable) + + if NewContact then + existingcontact.Contact = NewContact + self.Contacts:Push(existingcontact,existingcontact.CID) + end + + return self +end + +--- [Internal] Check merges for Players +-- @param #AWACS self +-- @return #AWACS self +function AWACS:_CheckMerges() + self:T(self.lid.."_CheckMerges") + for _id,_pilot in pairs (self.ManagedGrps) do + local pilot = _pilot -- #AWACS.ManagedGroup + if pilot.Group and pilot.Group:IsAlive() then + local ppos = pilot.Group:GetCoordinate() + local pcallsign = pilot.CallSign + self:T(self.lid.."Checking for "..pcallsign) + if ppos then + self.Contacts:ForEach( + function (Contact) + local contact = Contact -- #AWACS.ManagedContact + local cpos = contact.Cluster.coordinate or contact.Contact.position or contact.Contact.group:GetCoordinate() + local dist = ppos:Get2DDistance(cpos) + local distnm = UTILS.Round(UTILS.MetersToNM(dist),0) + if (pilot.IsPlayer or self.debug) and distnm <= 5 and not contact.MergeCallDone then + local label = contact.EngagementTag or "" + if not contact.MergeCallDone or not string.find(label,pcallsign) then + self:T(self.lid.."Merged") + self:_MergedCall(_id) + contact.MergeCallDone = true + end + end + end + ) + end + end + end + return self +end + +--- [Internal] Clean up contacts list +-- @param #AWACS self +-- @return #AWACS self +function AWACS:_CleanUpContacts() + self:T(self.lid.."_CleanUpContacts") + + if self.Contacts:Count() > 0 then + local deadcontacts = FIFO:New() + self.Contacts:ForEach( + function (Contact) + local contact = Contact -- #AWACS.ManagedContact + if not contact.Contact.group:IsAlive() or contact.Target:IsDead() then + deadcontacts:Push(contact,contact.CID) + self:T("DEAD contact CID="..contact.CID) + end + end + ) + + --local aliveclusters = FIFO:New() + -- announce VANISHED + if deadcontacts:Count() > 0 and (not self.NoGroupTags) then + + self:T("DEAD count="..deadcontacts:Count()) + deadcontacts:ForEach( + function (Contact) + local contact = Contact -- #AWACS.ManagedContact + local text = string.format("%s, %s Group. Vanished.",self.callsigntxt, contact.TargetGroupNaming) + local textScreen = string.format("%s, %s group vanished.", self.callsigntxt, contact.TargetGroupNaming) + self:_NewRadioEntry(text,textScreen,0,false,self.debug,true,false,true) + self.Contacts:PullByID(contact.CID) + -- end + end + ) + + end + + if self.Contacts:Count() > 0 then + self.Contacts:ForEach( + function (Contact) + local contact = Contact -- #AWACS.ManagedContact + self:_UpdateContactFromCluster(contact.CID) + end + ) + end + + -- cleanup + deadcontacts:Clear() + -- aliveclusters:Clear() + + end + return self +end + +--- [Internal] Select pilots available for tasking, return AI and Human +-- @param #AWACS self +-- @return #table AIPilots Table of #AWACS.ManagedGroup +-- @return #table HumanPilots Table of #AWACS.ManagedGroup +function AWACS:_GetIdlePilots() + self:T(self.lid.."_GetIdlePilots") + local AIPilots = {} + local HumanPilots = {} + + for _name,_entry in pairs (self.ManagedGrps) do + local entry = _entry -- #AWACS.ManagedGroup + self:T("Looking at entry "..entry.GID.." Name "..entry.GroupName) + local managedtask = self:_ReadAssignedTaskFromGID(entry.GID) -- #AWACS.ManagedTask + local overridetask = false + if managedtask then + self:T("Current task = "..(managedtask.ToDo or "Unknown")) + if managedtask.ToDo == AWACS.TaskDescription.ANCHOR then + overridetask = true + end + end + if entry.IsAI then + if entry.FlightGroup:IsAirborne() and ((not entry.HasAssignedTask) or overridetask) then -- must be idle, or? + self:T("Adding AI with Callsign: "..entry.CallSign) + AIPilots[#AIPilots+1] = _entry + end + elseif entry.IsPlayer and not entry.Blocked then + if (not entry.HasAssignedTask) or overridetask then -- must be idle, or? + -- check last assignment + local TNow = timer.getTime() + if entry.LastTasking and (TNow-entry.LastTasking > self.ReassignTime) then + self:T("Adding Human with Callsign: "..entry.CallSign) + HumanPilots[#HumanPilots+1] = _entry + end + end + end + end + + return AIPilots, HumanPilots + +end + +--- [Internal] Select max 3 targets for picture, bogey dope etc +-- @param #AWACS self +-- @param #boolean Untargeted Return not yet targeted contacts only +-- @return #boolean HaveTargets True if targets could be found, else false +-- @return Utilities.FiFo#FIFO Targetselection +function AWACS:_TargetSelectionProcess(Untargeted) + self:T(self.lid.."_TargetSelectionProcess") + + local maxtargets = 3 -- handleable number of callouts + local contactstable = self.Contacts:GetDataTable() + local targettable = FIFO:New() + local sortedtargets = FIFO:New() + local prefiltered = FIFO:New() + local HaveTargets = false + + self:T(self.lid.."Initial count: "..self.Contacts:Count()) + + -- Bucket sort + + if Untargeted then + -- pre-filter + self.Contacts:ForEach( + function (Contact) + local contact = Contact -- #AWACS.ManagedContact + if contact.Contact.group:IsAlive() and (contact.Status == AWACS.TaskStatus.IDLE or contact.Status == AWACS.TaskStatus.UNASSIGNED) then + if self.AwacsROE == AWACS.ROE.POLICE or self.AwacsROE == AWACS.ROE.VID then + -- filter out VID'd non-hostiles + if not (contact.IFF == AWACS.IFF.FRIENDLY or contact.IFF == AWACS.IFF.NEUTRAL) then + prefiltered:Push(contact,contact.CID) + end + else + prefiltered:Push(contact,contact.CID) + end + end + end + ) + contactstable = prefiltered:GetDataTable() + self:T(self.lid.."Untargeted: "..prefiltered:Count()) + end + + -- Loop through + for _,_contact in pairs(contactstable) do + local contact = _contact -- #AWACS.ManagedContact + local checked = false + local contactname = contact.TargetGroupNaming or "ZETA" + local typename = contact.ReportingName or "Unknown" + self:T(self.lid..string.format("Looking at group %s type %s",contactname,typename)) + local contactcoord = contact.Cluster.coordinate or contact.Contact.position or contact.Contact.group:GetCoordinate() + local contactvec2 = contactcoord:GetVec2() + -- self:T({contactcoord:ToStringMGRS()}) + -- self:T({contactvec2}) + + -- Bucket 0 - NOT in Rejection Zone :) + if self.RejectZone then + local isinrejzone = self.RejectZone:IsVec2InZone(contactvec2) + --local distance = self.OpsZone:Get2DDistance(contactcoord) + if isinrejzone then + self:T(self.lid.."Across Border = YES - ignore") + --targettable:Push(contact,distance) + checked = true + end + end + -- Bucket 1 - close to AIC (HVT) ca ~45nm + if not self.GCI then + local HVTCoordinate = self.OrbitZone:GetCoordinate() + local distance = UTILS.NMToMeters(200) + if contactcoord then + distance = HVTCoordinate:Get2DDistance(contactcoord) + end + self:T(self.lid.."HVT Distance = "..UTILS.Round(UTILS.MetersToNM(distance),0)) + if UTILS.MetersToNM(distance) <= 45 and not checked then + self:T(self.lid.."In HVT Distance = YES") + targettable:Push(contact,distance) + checked = true + end + end + + -- Bucket 2 - in AO/FEZ + local isinopszone = self.OpsZone:IsVec2InZone(contactvec2) + local distance = self.OpsZone:Get2DDistance(contactcoord) + if isinopszone and not checked then + self:T(self.lid.."In FEZ = YES") + targettable:Push(contact,distance) + checked = true + end + + -- Bucket 3 - in Radar(Control)Zone, < 100nm to AO, Aspect HOT on AO + local isinopszone = self.ControlZone:IsVec2InZone(contactvec2) + if isinopszone and not checked then + self:T(self.lid.."In Radar Zone = YES") + -- Close to Bulls Eye? + local distance = self.AOCoordinate:Get2DDistance(contactcoord) -- m + local AOdist = UTILS.Round(UTILS.MetersToNM(distance),0) -- NM + if not contactcoord.Heading then + contactcoord.Heading = self.intel:CalcClusterDirection(contact.Cluster) + end -- end heading + local aspect = contactcoord:ToStringAspect(self.ControlZone:GetCoordinate()) + local sizing = contact.Cluster.size or self.intel:ClusterCountUnits(contact.Cluster) or 1 + -- prefer heavy groups + sizing = math.fmod((sizing * 0.1),1) + local AOdist2 = (AOdist / 2) * sizing + AOdist2 = UTILS.Round((AOdist/2)+((AOdist/2)-AOdist2), 0) + self:T(self.lid.."Aspect = "..aspect.." | Size = "..sizing ) + if (AOdist2 < 75) or (aspect == "Hot") then + local text = string.format("In AO(Adj) dist = %d(%d) NM",AOdist,AOdist2) + self:T(self.lid..text) + --if sizing > 2 then distance = math.floor(distance / sizing)+1 end + targettable:Push(contact,distance) + checked = true + end + end + + -- Bucket 4 (if set) within the border polyzone to be defended + if self.BorderZone then + local isinborderzone = self.BorderZone:IsVec2InZone(contactvec2) + if isinborderzone and not checked then + self:T(self.lid.."In BorderZone = YES") + targettable:Push(contact,distance) + checked = true + end + end + end + + self:T(self.lid.."Post filter count: "..targettable:Count()) + + if targettable:Count() > maxtargets then + local targets = targettable:GetSortedDataTable() + targettable:Clear() + for i=1,maxtargets do + targettable:Push(targets[i]) + end + end + + sortedtargets:Clear() + prefiltered:Clear() + + if targettable:Count() > 0 then + HaveTargets = true + end + + return HaveTargets, targettable +end + +--- [Internal] AWACS Speak Picture AO/EWR entries +-- @param #AWACS self +-- @param #boolean AO If true this is for AO, else EWR +-- @param #string Callsign Callsign to address +-- @param #number GID GroupID for comms +-- @param #number MaxEntries Max entries to show +-- @param #boolean IsGeneral Is a general picture, address all stations +-- @return #AWACS self +function AWACS:_CreatePicture(AO,Callsign,GID,MaxEntries,IsGeneral) + self:T(self.lid.."_CreatePicture AO="..tostring(AO).." for "..Callsign.." GID "..GID) + + local managedgroup = nil + local group = nil + local groupcoord = nil + + if not IsGeneral then + managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + group = managedgroup.Group -- Wrapper.Group#GROUP + groupcoord = group:GetCoordinate() + end + + local fifo = self.PictureAO -- Utilities.FiFo#FIFO + + local maxentries = self.maxspeakentries or 3 + + if MaxEntries and MaxEntries>0 and MaxEntries <= 3 then + maxentries = MaxEntries + end + + local counter = 0 + + if not AO then + -- fifo = self.PictureEWR + end + + local entries = fifo:GetSize() + + if entries < maxentries then maxentries = entries end + + local text = "" + local textScreen = "" + + -- " group, BRA for at angels , , " + while counter < maxentries do + counter = counter + 1 + local contact = fifo:Pull() -- #AWACS.ManagedContact + self:T({contact}) + if contact and contact.Contact.group and contact.Contact.group:IsAlive() then + --local coordinate = contact.Contact.group:GetCoordinate() + local coordinate = contact.Cluster.coordinate or contact.Contact.position or contact.Contact.group:GetCoordinate() -- Core.Point#COORDINATE + if not coordinate then + self:E(self.lid.."NO Coordinate for this cluster! CID="..contact.CID) + self:E({contact}) + break + end + if not coordinate.Heading then + coordinate.Heading = contact.Contact.heading or contact.Contact.group:GetHeading() + end + local refBRAA = "" + local refBRAATTS = "" + + if self.NoGroupTags then + text = "Group." -- Alpha Group. + textScreen = "Group," + else + text = contact.TargetGroupNaming.." group." -- Alpha Group. + textScreen = contact.TargetGroupNaming.." group," + end + + if IsGeneral or not self.PlayerGuidance then + refBRAA=self:_ToStringBULLS(coordinate) + refBRAATTS = self:_ToStringBULLS(coordinate, false, true) + local alt = contact.Contact.group:GetAltitude() or 8000 + alt = UTILS.Round(UTILS.MetersToFeet(alt)/1000,0) + -- Alpha Group. Bulls eye 0 2 1, 16 miles, 25 thousand. + text = text .. " "..refBRAATTS.." miles, "..alt.." thousand." -- Alpha Group. Bulls eye 0 2 1, 16 miles, 25 thousand. + textScreen = textScreen .. " "..refBRAA.." miles, "..alt.." thousand." -- Alpha Group, Bullseye 021, 16 miles, 25 thousand, + else + -- pilot reference + refBRAA = coordinate:ToStringBRAANATO(groupcoord,true,true) + refBRAATTS = string.gsub(refBRAA,"BRAA","brah") + refBRAATTS = string.gsub(refBRAATTS,"BRA","brah") + -- Charlie group, BRAA 045, 105 miles, Angels 41, Flanking, Track North-East, Bogey, Spades. + if self.PathToGoogleKey then + refBRAATTS = coordinate:ToStringBRAANATO(groupcoord,true,true,true,false,true) + end + if contact.IFF ~= AWACS.IFF.BOGEY then + refBRAA = string.gsub(refBRAA,"Bogey", contact.IFF) + refBRAATTS = string.gsub(refBRAATTS,"Bogey", contact.IFF) + end + text = text .. " "..refBRAATTS + textScreen = textScreen .." "..refBRAA + end + + -- Aspect + local aspect = "" + + -- sizing + local size = contact.Contact.group:CountAliveUnits() + local threatsize, threatsizetext = self:_GetBlurredSize(size) + + if threatsize > 1 then + text = text.." "..threatsizetext.."." -- Alpha Group. Heavy. + textScreen = textScreen.." "..threatsizetext.."." + end + + -- engagement tag? + if contact.EngagementTag then + text = text .. " "..contact.EngagementTag -- Alpha Group. Bulls eye 0 2 1, 16. Heavy. Targeted by Jazz 1 1. + textScreen = textScreen .. " "..contact.EngagementTag -- Alpha Group, Bullseye 021, 16, Flanking. Targeted by Jazz 1 1. + end + + -- Transmit Radio + local RadioEntry_IsGroup = false + local RadioEntry_ToScreen = self.debug + if managedgroup and not IsGeneral then + RadioEntry_IsGroup = managedgroup.IsPlayer + RadioEntry_ToScreen = managedgroup.IsPlayer + end + + self:_NewRadioEntry(text,textScreen,GID,RadioEntry_IsGroup,RadioEntry_ToScreen,true,false) + + end + end + + -- empty queue from leftovers + fifo:Clear() + + return self +end + +--- [Internal] AWACS Speak Bogey Dope entries +-- @param #AWACS self +-- @param #string Callsign Callsign to address +-- @param #number GID GroupID for comms +-- @return #AWACS self +function AWACS:_CreateBogeyDope(Callsign,GID) + self:T(self.lid.."_CreateBogeyDope for "..Callsign.." GID "..GID) + + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + local group = managedgroup.Group -- Wrapper.Group#GROUP + local groupcoord = group:GetCoordinate() + + local fifo = self.ContactsAO -- Utilities.FiFo#FIFO + --local maxentries = self.maxspeakentries + local maxentries = 1 + local counter = 0 + + local entries = fifo:GetSize() + + if entries < maxentries then maxentries = entries end + + local sortedIDs = fifo:GetIDStackSorted() -- sort by distance + + while counter < maxentries do + counter = counter + 1 + local contact = fifo:PullByID(sortedIDs[counter]) -- #AWACS.ManagedContact + self:T({contact}) + local position = contact.Cluster.coordinate or contact.Contact.position + if contact and position then + local tag = contact.TargetGroupNaming + local reportingname = contact.ReportingName + -- DONE - add tag + self:_AnnounceContact(contact,false,group,true,tag,false,reportingname) + end + end + + -- empty queue from leftovers + fifo:Clear() + + return self +end + +--- [Internal] AWACS Menu for Picture +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @param #boolean IsGeneral General picture if true, address no-one specific +-- @return #AWACS self +function AWACS:_Picture(Group,IsGeneral) + self:T(self.lid.."_Picture") + local text = "" + local textScreen = text + local general = IsGeneral + local GID, Outcome, gcallsign = self:_GetManagedGrpID(Group) + --local gcallsign = "" + + if general then + gcallsign = "All Stations" + --else + --gcallsign = self:_GetCallSign(Group,GID) or "Ghost 1" + end + + if Group and Outcome then + general = false + end + + if not self.intel then + -- no intel yet! + text = string.format("%s. %s. Picture Clean.",self.callsigntxt, gcallsign) + textScreen = text + + self:_NewRadioEntry(text,text,GID,false,true,true,false) + + return self + end + + if Outcome or general then + -- Pilot is checked in + -- get clusters from Intel + -- DONE Use contacts table! + local contactstable = self.Contacts:GetDataTable() + + --local clustertable = self.intel:GetClusterTable() or {} + -- sort into buckets + for _,_contact in pairs(contactstable) do + + local contact = _contact -- #AWACS.ManagedContact + + --self:T(UTILS.OneLineSerialize(contact)) + + local coordVec2 = contact.Contact.position:GetVec2() + + --local coordVec2 = cluster.coordinate:GetVec2() + + if self.OpsZone:IsVec2InZone(coordVec2) then + self.PictureAO:Push(contact) + elseif self.OrbitZone and self.OrbitZone:IsVec2InZone(coordVec2) then + self.PictureAO:Push(contact) + elseif self.ControlZone:IsVec2InZone(coordVec2) then + local distance = math.floor((contact.Contact.position:Get2DDistance(self.ControlZone:GetCoordinate()) / 1000) + 1) -- km + self.PictureEWR:Push(contact,distance) + end + + end + + local clustersAO = self.PictureAO:GetSize() + local clustersEWR = self.PictureEWR:GetSize() + + if clustersAO < 3 and clustersEWR > 0 then + -- make sure we have 3, can only add 1, 2 or 3 + local IDstack = self.PictureEWR:GetSortedDataTable() + -- how many do we need? + local weneed = 3-clustersAO + -- do we have enough? + self:T(string.format("Picture - adding %d/%d contacts from EWR",weneed,clustersEWR)) + if weneed > clustersEWR then + weneed = clustersEWR + end + for i=1,weneed do + self.PictureAO:Push(IDstack[i]) + end + end + + clustersAO = self.PictureAO:GetSize() + + if clustersAO == 0 and clustersEWR == 0 then + -- clean + self:_NewRadioEntry(text,textScreen,GID,Outcome,true,true,false) + else + + if clustersAO > 0 then + --if general then + --text = string.format("%s, %s. ",gcallsign, self.callsigntxt) + --textScreen = string.format("%s, %s. ",gcallsign, self.callsigntxt) + --else + text = string.format("%s, %s. Picture. ",gcallsign, self.callsigntxt) + textScreen = string.format("%s, %s. Picture. ",gcallsign, self.callsigntxt) + --end + if clustersAO == 1 then + text = text .. "One group. " + textScreen = textScreen .. "One group.\n" + else + text = text .. clustersAO .. " groups. " + textScreen = textScreen .. clustersAO .. " groups.\n" + end + self:_NewRadioEntry(text,textScreen,GID,Outcome,true,true,false) + + self:_CreatePicture(true,gcallsign,GID,3,general) + + self.PictureAO:Clear() + self.PictureEWR:Clear() + end + end + + elseif self.AwacsFG then + -- no, unknown + text = string.format("%s. %s. Negative. You are not checked in.",gcallsign, self.callsigntxt) + self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) + end + return self +end + +--- [Internal] AWACS Menu for Bogey Dope +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @return #AWACS self +function AWACS:_BogeyDope(Group) + self:T(self.lid.."_BogeyDope") + local text = "" + local textScreen = "" + local GID, Outcome = self:_GetManagedGrpID(Group) + local gcallsign = self:_GetCallSign(Group,GID) or "Ghost 1" + + if not self.intel then + -- no intel yet! + text = string.format("%s. %s. Clean.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + self:_NewRadioEntry(text,text,0,false,true,true,false,true) + + return self + end + + if Outcome then + -- Pilot is checked in + + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + local pilotgroup = managedgroup.Group + local pilotcoord = managedgroup.Group:GetCoordinate() + + -- TODO - Use known contacts + local contactstable = self.Contacts:GetDataTable() + + -- sort into buckets - AO only for bogey dope! + for _,_contact in pairs(contactstable) do + local managedcontact = _contact -- #AWACS.ManagedContact + local contactposition = managedcontact.Cluster.coordinate or managedcontact.Contact.position -- Core.Point#COORDINATE + local coordVec2 = contactposition:GetVec2() + -- Get distance for sorting + local dist = pilotcoord:Get2DDistance(contactposition) + + if self.ControlZone:IsVec2InZone(coordVec2) then + self.ContactsAO:Push(managedcontact,dist) + elseif self.BorderZone and self.BorderZone:IsVec2InZone(coordVec2) then + self.ContactsAO:Push(managedcontact,dist) + else + if self.OrbitZone then + local distance = contactposition:Get2DDistance(self.OrbitZone:GetCoordinate()) + if (distance <= UTILS.NMToMeters(45)) then + self.ContactsAO:Push(managedcontact,distance) + end + end + end + end + + local contactsAO = self.ContactsAO:GetSize() + + if contactsAO == 0 then + -- clean + + text = string.format("%s. %s. Clean.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + + self:_NewRadioEntry(text,textScreen,GID,Outcome,Outcome,true,false,true) + + else + + if contactsAO > 0 then + text = string.format("%s. %s. Bogey Dope. ",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + if contactsAO == 1 then + text = text .. "One group. " + textScreen = text .. "\n" + else + text = text .. contactsAO .. " groups. " + textScreen = textScreen .. contactsAO .. " groups.\n" + end + + self:_NewRadioEntry(text,textScreen,GID,Outcome,true,true,false,true) + + self:_CreateBogeyDope(self:_GetCallSign(Group,GID) or "Ghost 1",GID) + end + end + + elseif self.AwacsFG then + -- no, unknown + text = string.format("%s. %s. Negative. You are not checked in.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + + self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) + + end + return self +end + +--- [Internal] AWACS Menu for Show Info +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @return #AWACS self +function AWACS:_ShowAwacsInfo(Group) + self:I(self.lid.."_ShowAwacsInfo") + local report = REPORT:New("Info") + report:Add("====================") + report:Add(string.format("AWACS %s",self.callsigntxt)) + report:Add(string.format("Radio: %d %s",self.Frequency,UTILS.GetModulationName(self.Modulation))) + report:Add(string.format("Bulls Alias: %s",self.AOName)) + report:Add(string.format("Coordinate: %s",self.AOCoordinate:ToStringLLDDM())) + report:Add("====================") + report:Add(string.format("Assignment Distance: %d NM",self.maxassigndistance)) + report:Add(string.format("TAC Distance: %d NM",self.TacDistance)) + report:Add(string.format("MELD Distance: %d NM",self.MeldDistance)) + report:Add(string.format("THREAT Distance: %d NM",self.ThreatDistance)) + report:Add("====================") + report:Add(string.format("ROE/ROT: %s, %s",self.AwacsROE,self.AwacsROT)) + MESSAGE:New(report:Text(),45,"AWACS"):ToGroup(Group) + return self +end + +--- [Internal] AWACS Menu for VID +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @param #string Declaration Text declaration the player used +-- @return #AWACS self +function AWACS:_VID(Group,Declaration) + self:T(self.lid.."_VID") + + local GID, Outcome, Callsign = self:_GetManagedGrpID(Group) + local text = "" + local TextTTS = "" + + if Outcome then + --yes, known + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + local group = managedgroup.Group + local position = group:GetCoordinate() + local radius = UTILS.NMToMeters(self.DeclareRadius) or UTILS.NMToMeters(5) + + -- find tasked contact + local TID = managedgroup.CurrentTask or 0 + if TID > 0 then + local task = self.ManagedTasks:ReadByID(TID) -- #AWACS.ManagedTask + -- correct task? + if task.ToDo ~= AWACS.TaskDescription.VID then + return self + end + -- already done? + if task.Status ~= AWACS.TaskStatus.ASSIGNED then + return self + end + local CID = task.Cluster.CID + local cluster = self.Contacts:ReadByID(CID) -- #AWACS.ManagedContact + if cluster then + local gposition = cluster.Contact.group:GetCoordinate() + local cposition = gposition or cluster.Cluster.coordinate or cluster.Contact.position + local distance = cposition:Get2DDistance(position) + distance = UTILS.Round(distance,0) + 1 + if distance <= radius or self.debug then + -- we can VID + self:T("Contact VID as "..Declaration) + -- update + cluster.IFF = Declaration + task.Status = AWACS.TaskStatus.SUCCESS + self.ManagedTasks:PullByID(TID) + self.ManagedTasks:Push(task,TID) + self.Contacts:PullByID(CID) + self.Contacts:Push(cluster,CID) + text = string.format("%s. %s. Copy, target identified as %s.",Callsign,self.callsigntxt, Declaration) + self:T(text) + else + -- too far away + self:T("Contact VID not close enough") + text = string.format("%s. %s. Negative, get closer to target.",Callsign,self.callsigntxt) + self:T(text) + end + self:_NewRadioEntry(text,text,GID,Outcome,true,true,false,true) + end + end + -- + elseif self.AwacsFG then + -- no, unknown + text = string.format("%s. %s. Negative. You are not checked in.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) + end + return self +end + +--- [Internal] AWACS Menu for Declare +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @return #AWACS self +function AWACS:_Declare(Group) + self:T(self.lid.."_Declare") + + local GID, Outcome, Callsign = self:_GetManagedGrpID(Group) + local text = "" + local TextTTS = "" + + if Outcome then + --yes, known + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + local group = managedgroup.Group + local position = group:GetCoordinate() + local radius = UTILS.NMToMeters(self.DeclareRadius) or UTILS.NMToMeters(5) + -- find contacts nearby + local groupzone = ZONE_GROUP:New(group:GetName(),group, radius) + local Coalitions = {"red","neutral"} + if self.coalition == coalition.side.NEUTRAL then + Coalitions = {"red","blue"} + elseif self.coalition == coalition.side.RED then + Coalitions = {"blue","neutral"} + end + local contactset = SET_GROUP:New():FilterCategoryAirplane():FilterCoalitions(Coalitions):FilterZones({groupzone}):FilterOnce() + local numbercontacts = contactset:CountAlive() or 0 + local foundcontacts = {} + if numbercontacts > 0 then + -- we have some around + -- sort by distance + contactset:ForEach( + function (airpl) + local distance = position:Get2DDistance(airpl:GetCoordinate()) + distance = UTILS.Round(distance,0) + 1 + foundcontacts[distance] = airpl + end + ,{} + ) + for _dist,_contact in UTILS.spairs(foundcontacts) do + local distanz = _dist + local contact = _contact -- Wrapper.Group#GROUP + local ccoalition = contact:GetCoalition() + local ctypename = contact:GetTypeName() + + local friendorfoe = "Neutral" + if self.self.ModernEra then + if ccoalition == self.coalition then + friendorfoe = "Friendly" + elseif ccoalition == coalition.side.NEUTRAL then + friendorfoe = "Neutral" + elseif ccoalition ~= self.coalition then + friendorfoe = "Hostile" + end + else + friendorfoe = "Spades" + end + -- see if that works + self:T(string.format("Distance %d ContactName %s Coalition %d (%s) TypeName %s",distanz,contact:GetName(),ccoalition,friendorfoe,ctypename)) + + text = string.format("%s. %s. %s.",Callsign,self.callsigntxt,friendorfoe) + TextTTS = text + if self.ModernEra then + text = string.format("%s %s.",text,ctypename) + end + break + end + else + -- clean + text = string.format("%s. %s. %s.",Callsign,self.callsigntxt,"Clean") + TextTTS = text + end + self:_NewRadioEntry(TextTTS,text,GID,Outcome,true,true,false,true) + -- + elseif self.AwacsFG then + -- no, unknown + text = string.format("%s. %s. Negative. You are not checked in.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) + end + return self +end + +--- [Internal] AWACS Menu for Commit +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @return #AWACS self +function AWACS:_Commit(Group) + self:T(self.lid.."_Commit") + local GID, Outcome = self:_GetManagedGrpID(Group) + local text = "" + if Outcome then + local Pilot = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + -- Get current task from the group + local currtaskid = Pilot.CurrentTask + local managedtask = self.ManagedTasks:ReadByID(currtaskid) -- #AWACS.ManagedTask + self:T(string.format("TID %d(%d) | ToDo %s | Status %s",currtaskid,managedtask.TID,managedtask.ToDo,managedtask.Status)) + if managedtask then + -- got a task, status? + if managedtask.Status == AWACS.TaskStatus.REQUESTED then + -- ok let's commit this one + managedtask = self.ManagedTasks:PullByID(currtaskid) + managedtask.Status = AWACS.TaskStatus.ASSIGNED + self.ManagedTasks:Push(managedtask,currtaskid) + self:T(string.format("COMMITTED - TID %d(%d) for GID %d | ToDo %s | Status %s",currtaskid,GID,managedtask.TID,managedtask.ToDo,managedtask.Status)) + -- link to Pilot + Pilot.HasAssignedTask = true + Pilot.CurrentTask = currtaskid + self.ManagedGrps[GID] = Pilot + text = string.format("%s. %s. Copy.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + local EngagementTag = string.format("Targeted by %s.",Pilot.CallSign) + self:_UpdateContactEngagementTag(Pilot.ContactCID,EngagementTag,false,false,AWACS.TaskStatus.ASSIGNED) + self:_NewRadioEntry(text,text,GID,Outcome,true,true,false,true) + else + self:E(self.lid.."Cannot find REQUESTED managed task with TID="..currtaskid.." for GID="..GID) + end + else + self:E(self.lid.."Cannot find managed task with TID="..currtaskid.." for GID="..GID) + end + elseif self.AwacsFG then + -- no, unknown + text = string.format("%s. %s. Negative. You are not checked in.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) + end + return self +end + +--- [Internal] AWACS Menu for Judy +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @return #AWACS self +function AWACS:_Judy(Group) + self:T(self.lid.."_Judy") + local GID, Outcome = self:_GetManagedGrpID(Group) + local text = "" + if Outcome then + local Pilot = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + -- Get current task from the group + local currtaskid = Pilot.CurrentTask + local managedtask = self.ManagedTasks:ReadByID(currtaskid) -- #AWACS.ManagedTask + if managedtask then + -- got a task, status? + if managedtask.Status == AWACS.TaskStatus.REQUESTED or managedtask.Status == AWACS.TaskStatus.UNASSIGNED then + -- ok let's commit this one + managedtask = self.ManagedTasks:PullByID(currtaskid) + managedtask.Status = AWACS.TaskStatus.ASSIGNED + self.ManagedTasks:Push(managedtask,currtaskid) + text = string.format("%s. %s. Copy.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + local EngagementTag = string.format("Targeted by %s.",Pilot.CallSign) + self:_UpdateContactEngagementTag(Pilot.ContactCID,EngagementTag,false,false,AWACS.TaskStatus.ASSIGNED) + self:_NewRadioEntry(text,text,GID,Outcome,true,true,false,true) + else + self:E(self.lid.."Cannot find REQUESTED or UNASSIGNED managed task with TID="..currtaskid.." for GID="..GID) + end + else + self:E(self.lid.."Cannot find managed task with TID="..currtaskid.." for GID="..GID) + end + elseif self.AwacsFG then + -- no, unknown + text = string.format("%s. %s. Negative. You are not checked in.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) + end + return self +end + +--- [Internal] AWACS Menu for Unable +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @return #AWACS self +function AWACS:_Unable(Group) + self:T(self.lid.."_Unable") + local GID, Outcome = self:_GetManagedGrpID(Group) + local text = "" + if Outcome then + local Pilot = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + -- Get current task from the group + local currtaskid = Pilot.CurrentTask + local managedtask = self.ManagedTasks:ReadByID(currtaskid) -- #AWACS.ManagedTask + self:T(string.format("UNABLE for TID %d(%d) | ToDo %s | Status %s",currtaskid,managedtask.TID,managedtask.ToDo,managedtask.Status)) + if managedtask then + -- got a task, status? + if managedtask.Status == AWACS.TaskStatus.REQUESTED then + -- ok let's commit this one + managedtask = self.ManagedTasks:PullByID(currtaskid) + --managedtask.AssignedGroupID = 0 + managedtask.IsUnassigned = true + managedtask.Status = AWACS.TaskStatus.FAILED + self.ManagedTasks:Push(managedtask,currtaskid) + self:T(string.format("REJECTED - TID %d(%d) for GID %d | ToDo %s | Status %s",currtaskid,GID,managedtask.TID,managedtask.ToDo,managedtask.Status)) + -- unlink group from task + Pilot.HasAssignedTask = false + Pilot.CurrentTask = 0 + Pilot.LastTasking = timer.getTime() + self.ManagedGrps[GID] = Pilot + text = string.format("%s. %s. Copy.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + local EngagementTag = "" + self:_UpdateContactEngagementTag(Pilot.ContactCID,EngagementTag,false,false,AWACS.TaskStatus.UNASSIGNED) + self:_NewRadioEntry(text,text,GID,Outcome,true,true,false,true) + else + self:E(self.lid.."Cannot find REQUESTED managed task with TID="..currtaskid.." for GID="..GID) + end + else + self:E(self.lid.."Cannot find managed task with TID="..currtaskid.." for GID="..GID) + end + elseif self.AwacsFG then + -- no, unknown + text = string.format("%s. %s. Negative. You are not checked in.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) + end + return self +end + +--- [Internal] AWACS Menu for Abort +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @return #AWACS self +function AWACS:_TaskAbort(Group) + self:T(self.lid.."_TaskAbort") + --local GID, Outcome = self:_GetManagedGrpID(Group) + local Outcome,GID = self:_GetGIDFromGroupOrName(Group) + local text = "" + if Outcome then + local Pilot = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + self:T({Pilot}) + -- Get current task from the group + local currtaskid = Pilot.CurrentTask + local managedtask = self.ManagedTasks:ReadByID(currtaskid) -- #AWACS.ManagedTask + if managedtask then + -- got a task, status? + self:T(string.format("ABORT for TID %d(%d) | ToDo %s | Status %s",currtaskid,managedtask.TID,managedtask.ToDo,managedtask.Status)) + if managedtask.Status == AWACS.TaskStatus.ASSIGNED then + -- ok let's un-commit this one + managedtask = self.ManagedTasks:PullByID(currtaskid) + managedtask.Status = AWACS.TaskStatus.FAILED + --managedtask.AssignedGroupID = 0 + managedtask.IsUnassigned = true + self.ManagedTasks:Push(managedtask,currtaskid) + -- unlink group + self:T(string.format("ABORTED - TID %d(%d) for GID %d | ToDo %s | Status %s",currtaskid,GID,managedtask.TID,managedtask.ToDo,managedtask.Status)) + -- unlink group from task + Pilot.HasAssignedTask = false + Pilot.CurrentTask = 0 + Pilot.LastTasking = timer.getTime() + self.ManagedGrps[GID] = Pilot + text = string.format("%s. %s. Copy.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + local EngagementTag = "" + self:_UpdateContactEngagementTag(Pilot.ContactCID,EngagementTag,false,false,AWACS.TaskStatus.UNASSIGNED) + self:_NewRadioEntry(text,text,GID,Outcome,true,true,false,true) + else + self:E(self.lid.."Cannot find ASSIGNED managed task with TID="..currtaskid.." for GID="..GID) + end + else + self:E(self.lid.."Cannot find managed task with TID="..currtaskid.." for GID="..GID) + end + elseif self.AwacsFG then + -- no, unknown + text = string.format("%s. %s. Negative. You are not checked in.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) + end + + return self +end + +--- [Internal] AWACS Menu for Showtask +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @return #AWACS self +function AWACS:_Showtask(Group) + self:T(self.lid.."_Showtask") + + local GID, Outcome, Callsign = self:_GetManagedGrpID(Group) + local text = "" + + if Outcome then + -- known group + + -- Do we have a task? + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + + if managedgroup.IsPlayer then + + if managedgroup.CurrentTask >0 and self.ManagedTasks:HasUniqueID(managedgroup.CurrentTask) then + -- get task structure + local currenttask = self.ManagedTasks:ReadByID(managedgroup.CurrentTask) -- #AWACS.ManagedTask + if currenttask then + local status = currenttask.Status + local targettype = currenttask.Target:GetCategory() + local targetstatus = currenttask.Target:GetState() + local ToDo = currenttask.ToDo + local description = currenttask.ScreenText + local callsign = Callsign + + if self.debug then + local taskreport = REPORT:New("AWACS Tasking Display") + taskreport:Add("===============") + taskreport:Add(string.format("Task for Callsign: %s",Callsign)) + taskreport:Add(string.format("Task: %s with Status: %s",ToDo,status)) + taskreport:Add(string.format("Target of Type: %s",targettype)) + taskreport:Add(string.format("Target in State: %s",targetstatus)) + taskreport:Add("===============") + self:I(taskreport:Text()) + end + + local pposition = managedgroup.Group:GetCoordinate() or managedgroup.LastKnownPosition + if currenttask.ToDo == AWACS.TaskDescription.INTERCEPT or currenttask.ToDo == AWACS.TaskDescription.VID then + local targetpos = currenttask.Target:GetCoordinate() + if pposition and targetpos then + local alti = currenttask.Cluster.altitude or currenttask.Contact.altitude or currenttask.Contact.group:GetAltitude() + local direction = self:_ToStringBRA(pposition,targetpos,alti) + description = description .. "\nBRA "..direction + end + elseif currenttask.ToDo == AWACS.TaskDescription.ANCHOR or currenttask.ToDo == AWACS.TaskDescription.REANCHOR then + local targetpos = currenttask.Target:GetCoordinate() + local direction = self:_ToStringBR(pposition,targetpos) + description = description .. "\nBR "..direction + end + + MESSAGE:New(string.format("%s\nStatus %s",description,status),30,"AWACS",true):ToGroup(Group) + + end + end + end + + elseif self.AwacsFG then + -- no, unknown + text = string.format("%s. %s. Negative. You are not checked in.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) + end + return self +end + +--- [Internal] AWACS Menu for Check in +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @return #AWACS self +function AWACS:_CheckIn(Group) + self:T(self.lid.."_CheckIn "..Group:GetName()) + -- check if already known + local GID, Outcome = self:_GetManagedGrpID(Group) + local text = "" + local textTTS = "" + if not Outcome then + self.ManagedGrpID = self.ManagedGrpID + 1 + local managedgroup = {} -- #AWACS.ManagedGroup + managedgroup.Group = Group + --managedgroup.GroupName = string.match(Group:GetName(),"([%a%s]+)#") + managedgroup.GroupName = Group:GetName() + managedgroup.IsPlayer = true + managedgroup.IsAI = false + managedgroup.CallSign = self:_GetCallSign(Group,GID) or "Ghost 1" + managedgroup.CurrentAuftrag = 0 + managedgroup.CurrentTask = 0 + managedgroup.HasAssignedTask = true + managedgroup.Blocked = true + managedgroup.GID = self.ManagedGrpID + --managedgroup.TaskQueue = FIFO:New() + managedgroup.LastKnownPosition = Group:GetCoordinate() + managedgroup.LastTasking = timer.getTime() + + GID = managedgroup.GID + self.ManagedGrps[self.ManagedGrpID]=managedgroup + + local alphacheckbulls = self:_ToStringBULLS(Group:GetCoordinate()) + --local alphacheckbullstts = self:_ToStringBullsTTS(alphacheckbulls)-- make tts friendly + local alphacheckbullstts = self:_ToStringBULLS(Group:GetCoordinate(),false,true) + + --self.ManagedGrps[self.ManagedGrpID]=managedgroup + text = string.format("%s. %s. Alpha Check. %s",managedgroup.CallSign,self.callsigntxt,alphacheckbulls) + textTTS = string.format("%s. %s. Alpha Check. %s",managedgroup.CallSign,self.callsigntxt,alphacheckbullstts) + + self:__CheckedIn(1,managedgroup.GID) + + if self.PlayerStationName then + self:__AssignAnchor(5,managedgroup.GID,true,self.PlayerStationName) + else + self:__AssignAnchor(5,managedgroup.GID) + end + + elseif self.AwacsFG then + text = string.format("%s. %s. Negative. You are already checked in.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + end + + self:_NewRadioEntry(textTTS,text,GID,Outcome,true,true,false) + + return self +end + +--- [Internal] AWACS Menu for CheckInAI +-- @param #AWACS self +-- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup to use +-- @param Wrapper.Group#GROUP Group Group to use +-- @param #number AuftragsNr Ops.Auftrag#AUFTRAG.auftragsnummer +-- @return #AWACS self +function AWACS:_CheckInAI(FlightGroup,Group,AuftragsNr) + self:T(self.lid.."_CheckInAI "..Group:GetName() .. " to Auftrag Nr "..AuftragsNr) + -- check if already known + local GID, Outcome = self:_GetManagedGrpID(Group) + local text = "" + if not Outcome then + self.ManagedGrpID = self.ManagedGrpID + 1 + local managedgroup = {} -- #AWACS.ManagedGroup + managedgroup.Group = Group + managedgroup.GroupName = Group:GetName() + managedgroup.FlightGroup = FlightGroup + managedgroup.IsPlayer = false + managedgroup.IsAI = true + local callsignstring = UTILS.GetCallsignName(self.AICAPCAllName) + local callsignmajor = math.fmod(self.AICAPCAllNumber,9) + local callsign = string.format("%s %d 1",callsignstring,callsignmajor) + if self.callsignshort then + callsign = string.format("%s %d",callsignstring,callsignmajor) + end + self:T("Assigned Callsign: ".. callsign) + managedgroup.CallSign = callsign + managedgroup.CurrentAuftrag = AuftragsNr + managedgroup.HasAssignedTask = false + managedgroup.GID = self.ManagedGrpID + managedgroup.LastKnownPosition = Group:GetCoordinate() + + self.ManagedGrps[self.ManagedGrpID]=managedgroup + + -- SRS voice for CAP + FlightGroup:SetDefaultRadio(self.Frequency,self.Modulation,false) + FlightGroup:SwitchRadio(self.Frequency,self.Modulation) + + local CAPVoice = self.CAPVoice + + if self.PathToGoogleKey then + CAPVoice = AWACS.CapVoices[math.floor(math.random(1,10))] + end + + FlightGroup:SetSRS(self.PathToSRS,self.CAPGender,self.CAPCulture,CAPVoice,self.Port,self.PathToGoogleKey,"FLIGHT") + + text = string.format("%s. %s. Checking in as fragged. Expected playtime %d hours. Request Alpha Check %s.",self.callsigntxt, managedgroup.CallSign, self.CAPTimeOnStation, self.AOName) + + self:_NewRadioEntry(text,text,managedgroup.GID,Outcome,false,true,true) + + local alphacheckbulls = self:_ToStringBULLS(Group:GetCoordinate(),false,true) + --alphacheckbulls = self:_ToStringBullsTTS(alphacheckbulls)-- make tts friendly + + text = string.format("%s. %s. Alpha Check. %s",managedgroup.CallSign,self.callsigntxt,alphacheckbulls) + self:__CheckedIn(1,managedgroup.GID) + + local AW = FlightGroup:GetAirWing() + if AW.HasOwnStation then + self:__AssignAnchor(5,managedgroup.GID,AW.HasOwnStation,AW.StationName) + else + self:__AssignAnchor(5,managedgroup.GID) + end + else + text = string.format("%s. %s. Negative. You are already checked in.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + end + + self:_NewRadioEntry(text,text,GID,Outcome,false,true,false) + + return self +end + +--- [Internal] AWACS Menu for Check Out +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @param #number GID GroupID +-- @param #boolean dead If true, group is dead crashed or otherwise n/a +-- @return #AWACS self +function AWACS:_CheckOut(Group,GID,dead) + self:T(self.lid.."_CheckOut") + + -- check if already known + local GID, Outcome = self:_GetManagedGrpID(Group) + local text = "" + if Outcome then + -- yes, known + text = string.format("%s. %s. Copy. Have a safe flight home.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + self:T(text) + -- grab some data before we nil the entry + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + local Stack = managedgroup.AnchorStackNo + local Angels = managedgroup.AnchorStackAngels + -- remove menus + if managedgroup.IsPlayer then + -- DONE Move to FIFO + if self.clientmenus:HasUniqueID(managedgroup.GroupName) then + local menus = self.clientmenus:PullByID(managedgroup.GroupName) --#AWACS.MenuStructure + menus.basemenu:Remove() + --self.clientmenus[AnchorAssigned.GroupName] = nil + end + end + -- delete open tasks + if managedgroup.CurrentTask and managedgroup.CurrentTask > 0 then + self.ManagedTasks:PullByID(managedgroup.CurrentTask ) + self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",false,false) + end + self.ManagedGrps[GID] = nil + self:__CheckedOut(1,GID,Stack,Angels) + else + -- no, unknown + if not dead then + text = string.format("%s. %s. Negative. You are not checked in.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + end + end + + if not dead then + self:_NewRadioEntry(text,text,GID,Outcome,false,true,false) + end + + return self +end + +--- [Internal] AWACS set client menus +-- @param #AWACS self +-- @return #AWACS self +function AWACS:_SetClientMenus() + self:T(self.lid.."_SetClientMenus") + local clientset = self.clientset -- Core.Set#SET_CLIENT + local aliveset = clientset:GetSetObjects() or {}-- #table of #CLIENT objects + --local clientmenus = {} + local clientcount = 0 + local clientcheckedin = 0 + for _,_group in pairs(aliveset) do + -- go through set and build the menu + local grp = _group -- Wrapper.Client#CLIENT + local cgrp = grp:GetGroup() + local cgrpname = nil + if cgrp and cgrp:IsAlive() then + cgrpname = cgrp:GetName() + self:T(cgrpname) + end + --cgrpname = string.match(cgrpname,"([%a%s]+)#") + if self.MenuStrict then + -- check if pilot has checked in + if cgrp and cgrp:IsAlive() then + clientcount = clientcount + 1 + local GID, checkedin = self:_GetManagedGrpID(cgrp) + if checkedin then + -- full menu minus checkin + clientcheckedin = clientcheckedin + 1 + --self.clientmenus:Flush() + local hasclientmenu = self.clientmenus:ReadByID(cgrpname) -- #AWACS.MenuStructure + --self:T({hasclientmenu}) + local basemenu = hasclientmenu.basemenu -- Core.Menu#MENU_GROUP + + if hasclientmenu and (not hasclientmenu.menuset) then + + self:T(self.lid.."Setting Menus for "..cgrpname) + + basemenu:RemoveSubMenus() + --basemenu:Refresh() + local tasking = MENU_GROUP:New(cgrp,"Tasking",basemenu) + local showtask = MENU_GROUP_COMMAND:New(cgrp,"Showtask",tasking,self._Showtask,self,cgrp) + local commit = MENU_GROUP_COMMAND:New(cgrp,"Commit",tasking,self._Commit,self,cgrp) + local unable = MENU_GROUP_COMMAND:New(cgrp,"Unable",tasking,self._Unable,self,cgrp) + local abort = MENU_GROUP_COMMAND:New(cgrp,"Abort",tasking,self._TaskAbort,self,cgrp) + --local judy = MENU_GROUP_COMMAND:New(cgrp,"Judy",tasking,self._Judy,self,cgrp) + + if self.AwacsROE == AWACS.ROE.POLICE or self.AwacsROE == AWACS.ROE.VID then + local vid = MENU_GROUP:New(cgrp,"VID as",tasking) + local hostile = MENU_GROUP_COMMAND:New(cgrp,"Hostile",vid,self._VID,self,cgrp,AWACS.IFF.ENEMY) + local neutral = MENU_GROUP_COMMAND:New(cgrp,"Neutral",vid,self._VID,self,cgrp,AWACS.IFF.NEUTRAL) + local friendly = MENU_GROUP_COMMAND:New(cgrp,"Friendly",vid,self._VID,self,cgrp,AWACS.IFF.FRIENDLY) + end + + local picture = MENU_GROUP_COMMAND:New(cgrp,"Picture",basemenu,self._Picture,self,cgrp) + local bogeydope = MENU_GROUP_COMMAND:New(cgrp,"Bogey Dope",basemenu,self._BogeyDope,self,cgrp) + + local declare = MENU_GROUP_COMMAND:New(cgrp,"Declare",basemenu,self._Declare,self,cgrp) + local ainfo = MENU_GROUP_COMMAND:New(cgrp,"Awacs Info",basemenu,self._ShowAwacsInfo,self,cgrp) + local checkout = MENU_GROUP_COMMAND:New(cgrp,"Check Out",basemenu,self._CheckOut,self,cgrp) + + --basemenu:Set() + basemenu:Refresh() + + local menus = { -- #AWACS.MenuStructure + groupname = cgrpname, + menuset = true, + basemenu = basemenu, + checkout= checkout, + picture = picture, + bogeydope = bogeydope, + declare = declare, + tasking = tasking, + showtask = showtask, + --judy = judy, + unable = unable, + abort = abort, + commit=commit, + } + self.clientmenus:PullByID(cgrpname) + self.clientmenus:Push(menus,cgrpname) + end + elseif not self.clientmenus:HasUniqueID(cgrpname) then + -- check in only + local basemenu = MENU_GROUP:New(cgrp,self.Name,nil) + --basemenu:RemoveSubMenus() + local checkin = MENU_GROUP_COMMAND:New(cgrp,"Check In",basemenu,self._CheckIn,self,cgrp) + checkin:SetTag(cgrp:GetName()) + --basemenu:Set() + basemenu:Refresh() + local menus = { -- #AWACS.MenuStructure + groupname = cgrpname, + menuset = false, + basemenu = basemenu, + checkin = checkin, + } + self.clientmenus:Push(menus,cgrpname) + end + end + else + if cgrp and cgrp:IsAlive() and not self.clientmenus:HasUniqueID(cgrpname) then + local basemenu = MENU_GROUP:New(cgrp,self.Name,nil) + --basemenu:RemoveSubMenus() + --basemenu:Refresh() + local picture = MENU_GROUP_COMMAND:New(cgrp,"Picture",basemenu,self._Picture,self,cgrp) + local bogeydope = MENU_GROUP_COMMAND:New(cgrp,"Bogey Dope",basemenu,self._BogeyDope,self,cgrp) + local declare = MENU_GROUP_COMMAND:New(cgrp,"Declare",basemenu,self._Declare,self,cgrp) + + local tasking = MENU_GROUP:New(cgrp,"Tasking",basemenu) + local showtask = MENU_GROUP_COMMAND:New(cgrp,"Showtask",tasking,self._Showtask,self,cgrp) + local commit = MENU_GROUP_COMMAND:New(cgrp,"Commit",tasking,self._Commit,self,cgrp) + local unable = MENU_GROUP_COMMAND:New(cgrp,"Unable",tasking,self._Unable,self,cgrp) + local abort = MENU_GROUP_COMMAND:New(cgrp,"Abort",tasking,self._TaskAbort,self,cgrp) + --local judy = MENU_GROUP_COMMAND:New(cgrp,"Judy",tasking,self._Judy,self,cgrp) + + if self.AwacsROE == AWACS.ROE.POLICE or self.AwacsROE == AWACS.ROE.VID then + local vid = MENU_GROUP:New(cgrp,"VID as",tasking) + local hostile = MENU_GROUP_COMMAND:New(cgrp,"Hostile",vid,self._VID,self,cgrp,AWACS.IFF.ENEMY) + local neutral = MENU_GROUP_COMMAND:New(cgrp,"Neutral",vid,self._VID,self,cgrp,AWACS.IFF.NEUTRAL) + local friendly = MENU_GROUP_COMMAND:New(cgrp,"Friendly",vid,self._VID,self,cgrp,AWACS.IFF.FRIENDLY) + end + + local ainfo = MENU_GROUP_COMMAND:New(cgrp,"Awacs Info",basemenu,self._ShowAwacsInfo,self,cgrp) + local checkin = MENU_GROUP_COMMAND:New(cgrp,"Check In",basemenu,self._CheckIn,self,cgrp) + local checkout = MENU_GROUP_COMMAND:New(cgrp,"Check Out",basemenu,self._CheckOut,self,cgrp) + + --basemenu:Set() + basemenu:Refresh() + + local menus = { -- #AWACS.MenuStructure + groupname = cgrpname, + menuset = true, + basemenu = basemenu, + checkin = checkin, + checkout= checkout, + picture = picture, + bogeydope = bogeydope, + declare = declare, + showtask = showtask, + tasking = tasking, + --judy = judy, + unable = unable, + abort = abort, + commit = commit, + } + self.clientmenus:Push(menus,cgrpname) + end + end + end + + --self.clientmenus = clientmenus + self.MonitoringData.Players = clientcount or 0 + self.MonitoringData.PlayersCheckedin = clientcheckedin or 0 + + return self +end + +--- [Internal] AWACS Delete a new Anchor Stack from a Marker - only works if no assignments are on the station +-- @param #AWACS self +-- @return #AWACS self +function AWACS:_DeleteAnchorStackFromMarker(Name,Coord) + self:I(self.lid.."_DeleteAnchorStackFromMarker") + if self.AnchorStacks:HasUniqueID(Name) and self.PlayerStationName == Name then + local stack = self.AnchorStacks:ReadByID(Name) -- #AWACS.AnchorData + local marker = stack.AnchorMarker + if stack.AnchorAssignedID:Count() == 0 then + marker:Remove() + if self.debug then + stack.StationZone:UndrawZone() + end + self.AnchorStacks:PullByID(Name) + self.PlayerStationName = nil + else + if self.debug then + self:I(self.lid.."**** Cannot delete station, there are CAPs assigned!") + local text = marker:GetText() + marker:TextUpdate(text.."\nMarked for deletion") + end + end + end + return self +end + +--- [Internal] AWACS Move a new Anchor Stack from a Marker +-- @param #AWACS self +-- @return #AWACS self +function AWACS:_MoveAnchorStackFromMarker(Name,Coord) + self:I(self.lid.."_MoveAnchorStackFromMarker") + if self.AnchorStacks:HasUniqueID(Name) and self.PlayerStationName == Name then + local station = self.AnchorStacks:PullByID(Name) -- #AWACS.AnchorData + local stationtag = string.format("Station: %s\nCoordinate: %s",Name,Coord:ToStringLLDDM()) + local marker = station.AnchorMarker + local zone = station.StationZone + if self.debug then + zone:UndrawZone() + end + local radius = self.StationZone:GetRadius() + if radius < 10000 then radius = 10000 end + station.StationZone = ZONE_RADIUS:New(Name, Coord:GetVec2(), radius) + marker:UpdateCoordinate(Coord) + marker:UpdateText(stationtag) + station.AnchorMarker = marker + if self.debug then + station.StationZone:DrawZone(-1,{0,0,1},1,{0,0,1},0.2,5,true) + end + self.AnchorStacks:Push(station,Name) + end + return self +end + +--- [Internal] AWACS Create a new Anchor Stack from a Marker - this then is the preferred station for players +-- @param #AWACS self +-- @return #AWACS self +function AWACS:_CreateAnchorStackFromMarker(Name,Coord) + self:I(self.lid.."_CreateAnchorStackFromMarker") + local AnchorStackOne = {} -- #AWACS.AnchorData + AnchorStackOne.AnchorBaseAngels = self.AnchorBaseAngels + AnchorStackOne.Anchors = FIFO:New() -- Utilities.FiFo#FIFO + AnchorStackOne.AnchorAssignedID = FIFO:New() -- Utilities.FiFo#FIFO + + local newname = Name + + for i=1,self.AnchorMaxStacks do + AnchorStackOne.Anchors:Push((i-1)*self.AnchorStackDistance+self.AnchorBaseAngels) + end + local radius = self.StationZone:GetRadius() + if radius < 10000 then radius = 10000 end + AnchorStackOne.StationZone = ZONE_RADIUS:New(newname, Coord:GetVec2(), radius) + AnchorStackOne.StationZoneCoordinate = Coord + AnchorStackOne.StationZoneCoordinateText = Coord:ToStringLLDDM() + AnchorStackOne.StationName = newname + + --push to AnchorStacks + if self.debug then + AnchorStackOne.StationZone:DrawZone(-1,{0,0,1},1,{0,0,1},0.2,5,true) + local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) + AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToAll() + else + local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) + AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToAll() + end + + self.AnchorStacks:Push(AnchorStackOne,newname) + self.PlayerStationName = newname + + return self +end + +--- [Internal] AWACS Create a new Anchor Stack +-- @param #AWACS self +-- @return #boolean success +-- @return #number AnchorStackNo +function AWACS:_CreateAnchorStack() + self:T(self.lid.."_CreateAnchorStack") + local stackscreated = self.AnchorStacks:GetSize() + if stackscreated == self.AnchorMaxAnchors then + -- only create self.AnchorMaxAnchors Anchors + return false, 0 + end + local AnchorStackOne = {} -- #AWACS.AnchorData + AnchorStackOne.AnchorBaseAngels = self.AnchorBaseAngels + AnchorStackOne.Anchors = FIFO:New() -- Utilities.FiFo#FIFO + AnchorStackOne.AnchorAssignedID = FIFO:New() -- Utilities.FiFo#FIFO + + local newname = self.StationZone:GetName() + + for i=1,self.AnchorMaxStacks do + AnchorStackOne.Anchors:Push((i-1)*self.AnchorStackDistance+self.AnchorBaseAngels) + end + + if stackscreated == 0 then + local newsubname = AWACS.AnchorNames[stackscreated+1] or tostring(stackscreated+1) + newname = self.StationZone:GetName() .. "-"..newsubname + AnchorStackOne.StationZone = self.StationZone + AnchorStackOne.StationZoneCoordinate = self.StationZone:GetCoordinate() + AnchorStackOne.StationZoneCoordinateText = self.StationZone:GetCoordinate():ToStringLLDDM() + AnchorStackOne.StationName = newname + --push to AnchorStacks + if self.debug then + --self.AnchorStacks:Flush() + AnchorStackOne.StationZone:DrawZone(-1,{0,0,1},1,{0,0,1},0.2,5,true) + local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) + AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToAll() + else + local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) + AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToAll() + end + self.AnchorStacks:Push(AnchorStackOne,newname) + else + local newsubname = AWACS.AnchorNames[stackscreated+1] or tostring(stackscreated+1) + newname = self.StationZone:GetName() .. "-"..newsubname + local anchorbasecoord = self.OpsZone:GetCoordinate() -- Core.Point#COORDINATE + -- OpsZone can be Polygon, so use distance to StationZone as radius + local anchorradius = anchorbasecoord:Get2DDistance(self.StationZone:GetCoordinate()) + local angel = self.StationZone:GetCoordinate():GetAngleDegrees(self.OpsZone:GetVec3()) + self:T("Angel Radians= " .. angel) + local turn = math.fmod(self.AnchorTurn*stackscreated,360) -- #number + if self.AnchorTurn < 0 then turn = -turn end + local newanchorbasecoord = anchorbasecoord:Translate(anchorradius,turn+angel) -- Core.Point#COORDINATE + local radius = self.StationZone:GetRadius() + if radius < 10000 then radius = 10000 end + AnchorStackOne.StationZone = ZONE_RADIUS:New(newname, newanchorbasecoord:GetVec2(), radius) + AnchorStackOne.StationZoneCoordinate = newanchorbasecoord + AnchorStackOne.StationZoneCoordinateText = newanchorbasecoord:ToStringLLDDM() + AnchorStackOne.StationName = newname + --push to AnchorStacks + if self.debug then + --self.AnchorStacks:Flush() + AnchorStackOne.StationZone:DrawZone(-1,{0,0,1},1,{0,0,1},0.2,5,true) + local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) + AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToAll() + else + local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) + AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToAll() + end + self.AnchorStacks:Push(AnchorStackOne,newname) + end + + return true,self.AnchorStacks:GetSize() + +end + +--- [Internal] AWACS get free anchor stack for managed groups +-- @param #AWACS self +-- @return #number AnchorStackNo +-- @return #boolean free +function AWACS:_GetFreeAnchorStack() + self:T(self.lid.."_GetFreeAnchorStack") + local AnchorStackNo, Free = 0, false + --return AnchorStackNo, Free + local availablestacks = self.AnchorStacks:GetPointerStack() or {} -- #table + for _id,_entry in pairs(availablestacks) do + local entry = _entry -- Utilities.FiFo#FIFO.IDEntry + local data = entry.data -- #AWACS.AnchorData + if data.Anchors:IsNotEmpty() then + AnchorStackNo = _id + Free = true + break + end + end + -- TODO - if extension of anchor stacks to max, send AI home + if not Free then + -- try to create another stack + local created, number = self:_CreateAnchorStack() + if created then + -- we could create a new one - phew! + self:_GetFreeAnchorStack() + end + end + return AnchorStackNo, Free +end + +--- [Internal] AWACS Assign Anchor Position to a Group +-- @param #AWACS self +-- @param #number GID Managed Group ID +-- @param #boolean HasOwnStation +-- @param #string StationName +-- @return #AWACS self +function AWACS:_AssignAnchorToID(GID, HasOwnStation, StationName) + self:T(self.lid.."_AssignAnchorToID") + if not HasOwnStation then + local AnchorStackNo, Free = self:_GetFreeAnchorStack() + if Free then + -- get the Anchor from the stack + local Anchor = self.AnchorStacks:PullByPointer(AnchorStackNo) -- #AWACS.AnchorData + -- pull one free angels + local freeangels = Anchor.Anchors:Pull() + -- push GID on anchor + Anchor.AnchorAssignedID:Push(GID) + -- push back to AnchorStacks + self.AnchorStacks:Push(Anchor,Anchor.StationName) + self:T({Anchor,freeangels}) + self:__AssignedAnchor(5,GID,Anchor,AnchorStackNo,freeangels) + else + self:E(self.lid .. "Cannot assign free anchor stack to GID ".. GID .. " Trying again in 10secs.") + -- try again ... + self:__AssignAnchor(10,GID) + end + else + local Anchor = self.AnchorStacks:PullByID(StationName) -- #AWACS.AnchorData + -- pull one free angels + local freeangels = Anchor.Anchors:Pull() or 25 + -- push GID on anchor + Anchor.AnchorAssignedID:Push(GID) + -- push back to AnchorStacks + self.AnchorStacks:Push(Anchor,StationName) + self:T({Anchor,freeangels}) + local StackNo = self.AnchorStacks.stackbyid[StationName].pointer + self:__AssignedAnchor(5,GID,Anchor,StackNo,freeangels) + end + return self +end + +--- [Internal] Remove GID (group) from Anchor Stack +-- @param #AWACS self +-- @param #AWACS.ManagedGroup.GID ID +-- @param #number AnchorStackNo +-- @param #number Angels +-- @return #AWACS self +function AWACS:_RemoveIDFromAnchor(GID,AnchorStackNo,Angels) + local gid = GID or 0 + local stack = AnchorStackNo or 0 + local angels = Angels or 0 + local debugstring = string.format("%s_RemoveIDFromAnchor for GID=%d Stack=%d Angels=%d",self.lid,gid,stack,angels) + self:T(debugstring) + -- pull correct anchor + if stack > 0 and angels > 0 then + local AnchorStackNo = AnchorStackNo or 1 + local Anchor = self.AnchorStacks:ReadByPointer(AnchorStackNo) -- #AWACS.AnchorData + -- pull GID from stack + local removedID = Anchor.AnchorAssignedID:PullByID(GID) + -- push free angels to stack + Anchor.Anchors:Push(Angels) + -- push back AnchorStack + --self.AnchorStacks:Push(Anchor) + end + return self +end + +--- [Internal] Start INTEL detection when we reach the AWACS Orbit Zone +-- @param #AWACS self +-- @param Wrapper.Group#GROUP awacs +-- @return #AWACS self +function AWACS:_StartIntel(awacs) + self:T(self.lid.."_StartIntel") + + if self.intelstarted then return self end + + self.DetectionSet:AddGroup(awacs) + + local intel = INTEL:New(self.DetectionSet,self.coalition,self.callsigntxt) + + intel:SetClusterAnalysis(true,false,false) + + local acceptzoneset = SET_ZONE:New() + acceptzoneset:AddZone(self.ControlZone) + acceptzoneset:AddZone(self.OpsZone) + + if not self.GCI then + self.OrbitZone:SetRadius(UTILS.NMToMeters(55)) + acceptzoneset:AddZone(self.OrbitZone) + end + + if self.BorderZone then + acceptzoneset:AddZone(self.BorderZone) + end + + --self.AwacsInZone + intel:SetAcceptZones(acceptzoneset) + + if self.NoHelos then + intel:SetFilterCategory({Unit.Category.AIRPLANE}) + else + intel:SetFilterCategory({Unit.Category.AIRPLANE,Unit.Category.HELICOPTER}) + end + + -- Callbacks + local function NewCluster(Cluster) + self:__NewCluster(5,Cluster) + end + function intel:OnAfterNewCluster(From,Event,To,Cluster) + NewCluster(Cluster) + end + + local function NewContact(Contact) + self:__NewContact(5,Contact) + end + function intel:OnAfterNewContact(From,Event,To,Contact) + NewContact(Contact) + end + + local function LostContact(Contact) + self:__LostContact(5,Contact) + end + function intel:OnAfterLostContact(From,Event,To,Contact) + LostContact(Contact) + end + + local function LostCluster(Cluster,Mission) + self:__LostCluster(5,Cluster,Mission) + end + function intel:OnAfterLostCluster(From,Event,To,Cluster,Mission) + LostCluster(Cluster,Mission) + end + + self.intelstarted = true + + intel.statusupdate = -30 + + intel:__Start(5) + + self.intel = intel -- Ops.Intelligence#INTEL + return self +end + +--- [Internal] Get blurred size of group or cluster +-- @param #AWACS self +-- @param #number size +-- @return #number adjusted size +-- @return #string AWACS.Shipsize entry for size 1..4 +function AWACS:_GetBlurredSize(size) + self:T(self.lid.."_GetBlurredSize") + local threatsize = 0 + local blur = self.RadarBlur + local blurmin = 100 - blur + local blurmax = 100 + blur + local actblur = math.random(blurmin,blurmax) / 100 + threatsize = math.floor(size * actblur) + if threatsize == 0 then threatsize = 1 end + if threatsize then end + local threatsizetext = AWACS.Shipsize[1] + if threatsize == 2 then + threatsizetext = AWACS.Shipsize[2] + elseif threatsize == 3 then + threatsizetext = AWACS.Shipsize[3] + elseif threatsize > 3 then + threatsizetext = AWACS.Shipsize[4] + end + return threatsize, threatsizetext +end + +--- [Internal] Get threat level as clear test +-- @param #AWACS self +-- @param #number threatlevel +-- @return #string threattext +function AWACS:_GetThreatLevelText(threatlevel) + self:T(self.lid.."_GetThreatLevelText") + local threattext = "GREEN" + if threatlevel <= AWACS.THREATLEVEL.GREEN then + threattext = "GREEN" + elseif threatlevel <= AWACS.THREATLEVEL.AMBER then + threattext = "AMBER" + else + threattext = "RED" + end + return threattext +end + + +--- [Internal] Get BR text for TTS +-- @param #AWACS self +-- @param Core.Point#COORDINATE FromCoordinate +-- @param Core.Point#COORDINATE ToCoordinate +-- @return #string BRText Desired Output (BR) "214, 35 miles" +-- @return #string BRTextTTS Desired Output (BR) "2 1 4, 35 miles" +function AWACS:_ToStringBR(FromCoordinate,ToCoordinate) + self:T(self.lid.."_ToStringBR") + local BRText = "" + local BRTextTTS = "" + local DirectionVec3 = FromCoordinate:GetDirectionVec3( ToCoordinate ) + local AngleRadians = FromCoordinate:GetAngleRadians( DirectionVec3 ) + local AngleDegrees = UTILS.Round( UTILS.ToDegree( AngleRadians ), 0 ) -- degrees + + local AngleDegText = string.format("%03d",AngleDegrees) -- 051 + local AngleDegTextTTS = "" + + --if self.PathToGoogleKey then + --AngleDegTextTTS = string.format("%s",AngleDegText) + --else + --AngleDegTextTTS = string.format("%s",AngleDegText) + --end + AngleDegText = string.gsub(AngleDegText,"%d","%1 ") -- "0 5 1 " + AngleDegText = string.gsub(AngleDegText," $","") -- "0 5 1" + + AngleDegTextTTS = string.gsub(AngleDegText,"0","zero") + + local Distance = ToCoordinate:Get2DDistance( FromCoordinate ) --meters + local distancenm = UTILS.Round(UTILS.MetersToNM(Distance),0) + + BRText = string.format("%03d, %d miles",AngleDegrees,distancenm) + BRTextTTS = string.format("%s, %d miles",AngleDegText,distancenm) + + if self.PathToGoogleKey then + BRTextTTS = string.format("%s, %d miles",AngleDegTextTTS,distancenm) + end + + self:T(BRText,BRTextTTS) + return BRText,BRTextTTS +end + +--- [Internal] Get BRA text for TTS +-- @param #AWACS self +-- @param Core.Point#COORDINATE FromCoordinate +-- @param Core.Point#COORDINATE ToCoordinate +-- @param #number Altitude Altitude in meters +-- @return #string BRText Desired Output (BRA) "214, 35 miles, 20 thousand" +-- @return #string BRTextTTS Desired Output (BRA) "2 1 4, 35 miles, 20 thousand" +function AWACS:_ToStringBRA(FromCoordinate,ToCoordinate,Altitude) + self:T(self.lid.."_ToStringBRA") + local BRText = "" + local BRTextTTS = "" + local altitude = UTILS.Round(UTILS.MetersToFeet(Altitude)/1000,0) + local DirectionVec3 = FromCoordinate:GetDirectionVec3( ToCoordinate ) + local AngleRadians = FromCoordinate:GetAngleRadians( DirectionVec3 ) + local AngleDegrees = UTILS.Round( UTILS.ToDegree( AngleRadians ), 0 ) -- degrees + + local AngleDegText = string.format("%03d",AngleDegrees) -- 051 + --local AngleDegTextTTS = string.format("%s",AngleDegText) + + AngleDegText = string.gsub(AngleDegText,"%d","%1 ") -- "0 5 1 " + AngleDegText = string.gsub(AngleDegText," $","") -- "0 5 1" + local AngleDegTextTTS = string.gsub(AngleDegText,"0","zero") + local Distance = ToCoordinate:Get2DDistance( FromCoordinate ) --meters + local distancenm = UTILS.Round(UTILS.MetersToNM(Distance),0) + if altitude >= 1 then + BRText = string.format("%03d, %d miles, %d thousand",AngleDegrees,distancenm,altitude) + BRTextTTS = string.format("%s, %d miles, %d thousand",AngleDegText,distancenm,altitude) + if self.PathToGoogleKey then + BRTextTTS = string.format("%s, %d miles, %d thousand",AngleDegTextTTS,distancenm,altitude) + end + else + BRText = string.format("%03d, %d miles, very low",AngleDegrees,distancenm) + BRTextTTS = string.format("%s, %d miles, very low",AngleDegText,distancenm) + if self.PathToGoogleKey then + BRTextTTS = string.format("%s, %d miles, very low",AngleDegTextTTS,distancenm) + end + end + self:T(BRText,BRTextTTS) + return BRText,BRTextTTS +end + +--- [Internal] Get BR text for TTS - ie "Rock 214, 24 miles" and TTS "Rock 2 1 4, 24 miles" +-- @param #AWACS self +-- @param Core.Point#COORDINATE clustercoordinate +-- @return #string BRAText +-- @return #string BRATextTTS +function AWACS:_GetBRAfromBullsOrAO(clustercoordinate) + self:T(self.lid.."_GetBRAfromBullsOrAO") + local refcoord = self.AOCoordinate -- Core.Point#COORDINATE + local BRAText = "" + local BRATextTTS = "" + -- get BR from AO + local bullsname = self.AOName or "Rock" + local stringbr, stringbrtts = self:_ToStringBR(refcoord,clustercoordinate) + BRAText = string.format("%s %s",bullsname,stringbr) + BRATextTTS = string.format("%s %s",bullsname,stringbrtts) + self:T(BRAText,BRATextTTS) + return BRAText,BRATextTTS +end + +--- [Internal] Register Task for Group by GID +-- @param #AWACS self +-- @param #number GroupID ManagedGroup ID +-- @param #AWACS.TaskDescription Description Short Description Task Type +-- @param #string ScreenText Long task description for screen output +-- @param #table Object Object for Ops.Target#TARGET assignment +-- @param #AWACS.TaskStatus TaskStatus Status of this task +-- @param Ops.Auftrag#AUFTRAG Auftrag The Auftrag for this task if any +-- @param Ops.Intelligence#INTEL.Cluster Cluster Intel Cluster for this task +-- @param Ops.Intelligence#INTEL.Contact Contact Intel Contact for this task +-- @return #number TID Task ID created +function AWACS:_CreateTaskForGroup(GroupID,Description,ScreenText,Object,TaskStatus,Auftrag,Cluster,Contact) + self:T(self.lid.."_CreateTaskForGroup "..GroupID .." Description: "..Description) + + local managedgroup = self.ManagedGrps[GroupID] -- #AWACS.ManagedGroup + local task = {} -- #AWACS.ManagedTask + self.ManagedTaskID = self.ManagedTaskID + 1 + task.TID = self.ManagedTaskID + task.AssignedGroupID = GroupID + task.Status = TaskStatus or AWACS.TaskStatus.ASSIGNED + task.ToDo = Description + task.Auftrag = Auftrag + task.Cluster = Cluster + task.Contact = Contact + task.IsPlayerTask = managedgroup.IsPlayer + task.IsUnassigned = TaskStatus == AWACS.TaskStatus.UNASSIGNED and false or true + -- task. + if Object and Object:IsInstanceOf("TARGET") then + task.Target = Object + else + task.Target = TARGET:New(Object) + end + task.ScreenText = ScreenText + if Description == AWACS.TaskDescription.ANCHOR or Description == AWACS.TaskDescription.REANCHOR then + task.Target.Type = TARGET.ObjectType.ZONE + end + task.RequestedTimestamp = timer.getTime() + + self.ManagedTasks:Push(task,task.TID) + + managedgroup.HasAssignedTask = true + managedgroup.CurrentTask = task.TID + --managedgroup.TaskQueue:Push(task.TID) + + self:T({managedgroup}) + self.ManagedGrps[GroupID] = managedgroup + + return task.TID +end + +--- [Internal] Read registered Task for Group by its ID +-- @param #AWACS self +-- @param #number GroupID ManagedGroup ID +-- @return #AWACS.ManagedTask Task or nil if n/e +function AWACS:_ReadAssignedTaskFromGID(GroupID) + self:T(self.lid.."_GetAssignedTaskFromGID "..GroupID) + local managedgroup = self.ManagedGrps[GroupID] -- #AWACS.ManagedGroup + if managedgroup and managedgroup.HasAssignedTask and managedgroup.CurrentTask ~= 0 then + local TaskID = managedgroup.CurrentTask + if self.ManagedTasks:HasUniqueID(TaskID) then + return self.ManagedTasks:ReadByID(TaskID) + end + end + return nil +end + +--- [Internal] Read assigned Group from a TaskID +-- @param #AWACS self +-- @param #number TaskID ManagedTask ID +-- @return #AWACS.ManagedGroup Group structure or nil if n/e +function AWACS:_ReadAssignedGroupFromTID(TaskID) + self:T(self.lid.."_ReadAssignedGroupFromTID "..TaskID) + if self.ManagedTasks:HasUniqueID(TaskID) then + local task = self.ManagedTasks:ReadByID(TaskID) -- #AWACS.ManagedTask + if task and task.AssignedGroupID and task.AssignedGroupID > 0 then + return self.ManagedGrps[task.AssignedGroupID] + end + end + return nil +end + +--- [Internal] Create new idle task from contact to pick up later +-- @param #AWACS self +-- @param #string Description Task Type +-- @param #table Object Object of TARGET +-- @param Ops.Intelligence#INTEL.Contact Contact +-- @return #AWACS self +function AWACS:_CreateIdleTaskForContact(Description,Object,Contact) + self:T(self.lid.."_CreateIdleTaskForContact "..Description) + local task = {} -- #AWACS.ManagedTask + self.ManagedTaskID = self.ManagedTaskID + 1 + task.TID = self.ManagedTaskID + task.AssignedGroupID = 0 + task.Status = AWACS.TaskStatus.IDLE + task.ToDo = Description + task.Target = TARGET:New(Object) + task.Contact = Contact + --task.IsContact = true + task.ScreenText = Description + if Description == AWACS.TaskDescription.ANCHOR or Description == AWACS.TaskDescription.REANCHOR then + task.Target.Type = TARGET.ObjectType.ZONE + end + self.ManagedTasks:Push(task,task.TID) + return self +end + +--- [Internal] Create new idle task from cluster to pick up later +-- @param #AWACS self +-- @param #string Description Task Type +-- @param #table Object Object of TARGET +-- @param Ops.Intelligence#INTEL.Cluster Cluster +-- @return #AWACS self +function AWACS:_CreateIdleTaskForCluster(Description,Object,Cluster) + self:T(self.lid.."_CreateIdleTaskForCluster "..Description) + local task = {} -- #AWACS.ManagedTask + self.ManagedTaskID = self.ManagedTaskID + 1 + task.TID = self.ManagedTaskID + task.AssignedGroupID = 0 + task.Status = AWACS.TaskStatus.IDLE + task.ToDo = Description + --self:T({Cluster.Contacts}) + --task.Target = TARGET:New(Cluster.Contacts[1]) + task.Target = TARGET:New(self.intel:GetClusterCoordinate(Cluster)) + task.Cluster = Cluster + --task.IsCluster = true + task.ScreenText = Description + if Description == AWACS.TaskDescription.ANCHOR or Description == AWACS.TaskDescription.REANCHOR then + task.Target.Type = TARGET.ObjectType.ZONE + end + self.ManagedTasks:Push(task,task.TID) + return self +end + +--- [Internal] Create radio entry to tell players that CAP is on station in Anchor +-- @param #AWACS self +-- @param #number GID Group ID +-- @return #AWACS self +function AWACS:_MessageAIReadyForTasking(GID) + self:T(self.lid.."_MessageAIReadyForTasking") + -- obtain group details + if GID >0 and self.ManagedGrps[GID] then + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + local GFCallsign = self:_GetCallSign(managedgroup.Group) + local TextTTS = string.format("%s. %s. On station over anchor %d at angels %d. Ready for tasking.",GFCallsign,self.callsigntxt,managedgroup.AnchorStackNo or 1,managedgroup.AnchorStackAngels or 25) + self:_NewRadioEntry(TextTTS,TextTTS,GID,false,false,true,true) + end + return self +end + +--- [Internal] Update Contact Tag +-- @param #AWACS self +-- @param #number CID Contact ID +-- @param #string Text Text to be used +-- @param #boolean TAC TAC Call done +-- @param #boolean MELD MELD Call done +-- @param #string TaskStatus Overwrite status with #AWACS.TaskStatus Status +-- @return #AWACS self +function AWACS:_UpdateContactEngagementTag(CID,Text,TAC,MELD,TaskStatus) + self:T(self.lid.."_UpdateContactEngagementTag") + local text = Text or "" + -- get contact + local contact = self.Contacts:PullByID(CID) -- #AWACS.ManagedContact + if contact then + contact.EngagementTag = text + contact.TACCallDone = TAC or false + contact.MeldCallDone = MELD or false + contact.Status = TaskStatus or AWACS.TaskStatus.UNASSIGNED + self.Contacts:Push(contact,CID) + end + return self +end + +--- [Internal] Check available tasks and status +-- @param #AWACS self +-- @return #AWACS self +function AWACS:_CheckTaskQueue() + self:T(self.lid.."_CheckTaskQueue") + local opentasks = 0 + local assignedtasks = 0 + + -- update last known positions + for _id,_managedgroup in pairs(self.ManagedGrps) do + local group = _managedgroup -- #AWACS.ManagedGroup + if group.Group and group.Group:IsAlive() then + local coordinate = group.Group:GetCoordinate() + if coordinate then + local NewCoordinate = COORDINATE:New(0,0,0) + group.LastKnownPosition = group.LastKnownPosition:UpdateFromCoordinate(coordinate) + self.ManagedGrps[_id] = group + end + end + end + + ---------------------------------------- + -- ANCHOR + ---------------------------------------- + + if self.ManagedTasks:IsNotEmpty() then + opentasks = self.ManagedTasks:GetSize() + self:T("Assigned Tasks: " .. opentasks) + local taskstack = self.ManagedTasks:GetPointerStack() + for _id,_entry in pairs(taskstack) do + local data = _entry -- Utilities.FiFo#FIFO.IDEntry + local entry = data.data -- #AWACS.ManagedTask + local target = entry.Target -- Ops.Target#TARGET + local description = entry.ToDo + if description == AWACS.TaskDescription.ANCHOR or description == AWACS.TaskDescription.REANCHOR then + self:T("Open Task ANCHOR/REANCHOR") + -- see if we have reached the anchor zone + local managedgroup = self.ManagedGrps[entry.AssignedGroupID] -- #AWACS.ManagedGroup + if managedgroup then + local group = managedgroup.Group + if group and group:IsAlive() then + local groupcoord = group:GetCoordinate() + local zone = target:GetObject() -- Core.Zone#ZONE + self:T({zone}) + if group:IsInZone(zone) then + self:T("Open Task ANCHOR/REANCHOR success for GroupID "..entry.AssignedGroupID) + -- made it + target:Stop() + -- add group to idle stack + if managedgroup.IsAI then + -- message AI on station + self:_MessageAIReadyForTasking(managedgroup.GID) + elseif managedgroup.IsPlayer then + --self.TaskedCAPHuman:PullByPointer(entry.AssignedGroupID) + --self.CAPIdleHuman:Push(entry.AssignedGroupID) + end -- end isAI + managedgroup.HasAssignedTask = false + self.ManagedGrps[entry.AssignedGroupID] = managedgroup + -- pull task from OpenTasks + self.ManagedTasks:PullByID(entry.TID) + else --inzone + -- not there yet + self:T("Open Task ANCHOR/REANCHOR executing for GroupID "..entry.AssignedGroupID) + end + else + -- group dead, pull task + self.ManagedTasks:PullByID(entry.TID) + end + end + + ---------------------------------------- + -- INTERCEPT + ---------------------------------------- + + elseif description == AWACS.TaskDescription.INTERCEPT then + -- DONE + self:T("Open Tasks INTERCEPT") + local taskstatus = entry.Status + local targetstatus = entry.Target:GetState() + + if taskstatus == AWACS.TaskStatus.UNASSIGNED then + -- thou shallst not be in this list! + self.ManagedTasks:PullByID(entry.TID) + break + end + + local managedgroup = self.ManagedGrps[entry.AssignedGroupID] -- #AWACS.ManagedGroup + + -- Check ranges for TAC and MELD + -- postions relative to CAP position + + local targetgrp = entry.Contact.group + local position = entry.Contact.position or entry.Cluster.coordinate + if targetgrp and targetgrp:IsAlive() and managedgroup then + --position = targetgrp:GetCoordinate() + if position and managedgroup.Group and managedgroup.Group:IsAlive() then + local grouposition = managedgroup.Group:GetCoordinate() or managedgroup.Group:GetCoordinate() + local distance = 1000 + if grouposition then + distance = grouposition:Get2DDistance(position) + distance = UTILS.Round(UTILS.MetersToNM(distance),0) + end + self:T("TAC/MELD distance check: "..distance.."NM!") + if distance <= self.TacDistance and distance >= self.MeldDistance then + -- TAC distance + self:T("TAC distance: "..distance.."NM!") + local Contact = self.Contacts:ReadByID(entry.Contact.CID) + self:_TACRangeCall(entry.AssignedGroupID,Contact) + elseif distance <= self.MeldDistance and distance >= self.ThreatDistance then + -- MELD distance + self:T("MELD distance: "..distance.."NM!") + local Contact = self.Contacts:ReadByID(entry.Contact.CID) + self:_MeldRangeCall(entry.AssignedGroupID,Contact) + end + end + end + + local auftrag = entry.Auftrag -- Ops.Auftrag#AUFTRAG + local auftragstatus = "Not Known" + if auftrag then + auftragstatus = auftrag:GetState() + end + local text = string.format("ID=%d | Status=%s | TargetState=%s | AuftragState=%s",entry.TID,taskstatus,targetstatus,auftragstatus) + self:T(text) + if auftrag then + if auftrag:IsExecuting() then + entry.Status = AWACS.TaskStatus.EXECUTING + elseif auftrag:IsSuccess() then + entry.Status = AWACS.TaskStatus.SUCCESS + elseif auftrag:GetState() == AUFTRAG.Status.FAILED then + entry.Status = AWACS.TaskStatus.FAILED + end + if targetstatus == "Dead" then + entry.Status = AWACS.TaskStatus.SUCCESS + elseif targetstatus == "Alive" and auftrag:IsOver() then + entry.Status = AWACS.TaskStatus.FAILED + end + elseif entry.IsPlayerTask then + -- Player task + -- TODO + if entry.Target:IsDead() or entry.Target:IsDestroyed() then + -- success! + entry.Status = AWACS.TaskStatus.SUCCESS + elseif entry.Target:IsAlive() then + -- still alive + -- out of zones? + local targetpos = entry.Target:GetCoordinate() + -- success == out of our controlled zones + local outofzones = false + self.RejectZoneSet:ForEachZone( + function(Zone,Position) + local zone = Zone -- Core.Zone#ZONE + local pos = Position -- Core.Point#VEC2 + if pos and zone:IsVec2InZone(pos) then + -- crossed the border + outofzones = true + end + end, + targetpos:GetVec2() + ) + if not outofzones then + outofzones = true + self.ZoneSet:ForEachZone( + function(Zone,Position) + local zone = Zone -- Core.Zone#ZONE + local pos = Position -- Core.Point#VEC2 + if pos and zone:IsVec2InZone(pos) then + -- in any zone + outofzones = false + end + end, + targetpos:GetVec2() + ) + end + if outofzones then + entry.Status = AWACS.TaskStatus.SUCCESS + end + end + end + + if entry.Status == AWACS.TaskStatus.SUCCESS then + self:T("Open Tasks INTERCEPT success for GroupID "..entry.AssignedGroupID) + if managedgroup then + + self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",true,true,AWACS.TaskStatus.SUCCESS) + + managedgroup.HasAssignedTask = false + managedgroup.ContactCID = 0 + managedgroup.LastTasking = timer.getTime() + + if managedgroup.IsAI then + managedgroup.CurrentAuftrag = 0 + else + managedgroup.CurrentTask = 0 + end + + self.ManagedGrps[entry.AssignedGroupID] = managedgroup + self.ManagedTasks:PullByID(entry.TID) + + self:__InterceptSuccess(1) + self:__ReAnchor(5,managedgroup.GID) + end + + elseif entry.Status == AWACS.TaskStatus.FAILED then + self:T("Open Tasks INTERCEPT failed for GroupID "..entry.AssignedGroupID) + if managedgroup then + managedgroup.HasAssignedTask = false + self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",false,false,AWACS.TaskStatus.UNASSIGNED) + managedgroup.ContactCID = 0 + managedgroup.LastTasking = timer.getTime() + if managedgroup.IsAI then + managedgroup.CurrentAuftrag = 0 + else + managedgroup.CurrentTask = 0 + end + if managedgroup.IsPlayer then + entry.IsPlayerTask = false + end + self.ManagedGrps[entry.AssignedGroupID] = managedgroup + if managedgroup.Group:IsAlive() or (managedgroup.FlightGroup and managedgroup.FlightGroup:IsAlive()) then + self:__ReAnchor(5,managedgroup.GID) + end + end + -- remove + self.ManagedTasks:PullByID(entry.TID) + self:__InterceptFailure(1) + + elseif entry.Status == AWACS.TaskStatus.REQUESTED then + -- requested - player tasks only! + self:T("Open Tasks INTERCEPT REQUESTED for GroupID "..entry.AssignedGroupID) + local created = entry.RequestedTimestamp or timer.getTime() - 120 + local Tnow = timer.getTime() + local Trunning = (Tnow-created) / 60 -- mins + local text = string.format("Task TID %s Requested %d minutes ago.",entry.TID,Trunning) + if Trunning > self.ReassignmentPause then + -- reassign if player didn't react within 3 mins + entry.Status = AWACS.TaskStatus.UNASSIGNED + self.ManagedTasks:PullByID(entry.TID) + end + self:T(text) + end + + ---------------------------------------- + -- VID/POLICE + ---------------------------------------- + + elseif description == AWACS.TaskDescription.VID then + -- TODO - how to do this with AI? + -- humans only ATM + local managedgroup = self.ManagedGrps[entry.AssignedGroupID] -- #AWACS.ManagedGroup + -- check we're alive + if (not managedgroup) or (not managedgroup.Group:IsAlive()) then + self.ManagedTasks:PullByID(entry.TID) + --entry.Status = AWACS.TaskStatus.FAILED + return self + end + + -- target dead or out of bounds? + if entry.Target:IsDead() or entry.Target:IsDestroyed() then + -- success! + entry.Status = AWACS.TaskStatus.SUCCESS + elseif entry.Target:IsAlive() then + -- still alive + -- out of zones? + self:T("Checking VID target out of bounds") + local targetpos = entry.Target:GetCoordinate() + -- success == out of our controlled zones + local outofzones = false + self.RejectZoneSet:ForEachZone( + function(Zone,Position) + local zone = Zone -- Core.Zone#ZONE + local pos = Position -- Core.Point#VEC2 + if pos and zone:IsVec2InZone(pos) then + -- crossed the border + outofzones = true + end + end, + targetpos:GetVec2() + ) + if not outofzones then + outofzones = true + self.ZoneSet:ForEachZone( + function(Zone,Position) + local zone = Zone -- Core.Zone#ZONE + local pos = Position -- Core.Point#VEC2 + if pos and zone:IsVec2InZone(pos) then + -- in any zone + outofzones = false + end + end, + targetpos:GetVec2() + ) + end + if outofzones then + entry.Status = AWACS.TaskStatus.SUCCESS + self:T("Out of bounds - SUCCESS") + end + end + + if entry.Status == AWACS.TaskStatus.REQUESTED then + -- requested - player tasks only! + self:T("Open Tasks VID REQUESTED for GroupID "..entry.AssignedGroupID) + local created = entry.RequestedTimestamp or timer.getTime() - 120 + local Tnow = timer.getTime() + local Trunning = (Tnow-created) / 60 -- mins + local text = string.format("Task TID %s Requested %d minutes ago.",entry.TID,Trunning) + if Trunning > self.ReassignmentPause then + -- reassign if player didn't react within 3 mins + entry.Status = AWACS.TaskStatus.UNASSIGNED + self.ManagedTasks:PullByID(entry.TID) + end + self:T(text) + elseif entry.Status == AWACS.TaskStatus.ASSIGNED then + self:T("Open Tasks VID ASSIGNED for GroupID "..entry.AssignedGroupID) + -- check TAC/MELD ranges + local targetgrp = entry.Contact.group + local position = entry.Contact.position or entry.Cluster.coordinate + if targetgrp and targetgrp:IsAlive() and managedgroup then + --position = targetgrp:GetCoordinate() + if position and managedgroup.Group and managedgroup.Group:IsAlive() then + local grouposition = managedgroup.Group:GetCoordinate() or managedgroup.Group:GetCoordinate() + local distance = 1000 + if grouposition then + distance = grouposition:Get2DDistance(position) + distance = UTILS.Round(UTILS.MetersToNM(distance),0) + end + self:T("TAC/MELD distance check: "..distance.."NM!") + if distance <= self.TacDistance and distance >= self.MeldDistance then + -- TAC distance + self:T("TAC distance: "..distance.."NM!") + local Contact = self.Contacts:ReadByID(entry.Contact.CID) + self:_TACRangeCall(entry.AssignedGroupID,Contact) + elseif distance <= self.MeldDistance and distance >= self.ThreatDistance then + -- MELD distance + self:T("MELD distance: "..distance.."NM!") + local Contact = self.Contacts:ReadByID(entry.Contact.CID) + self:_MeldRangeCall(entry.AssignedGroupID,Contact) + end + end + end + elseif entry.Status == AWACS.TaskStatus.SUCCESS then + self:T("Open Tasks VID success for GroupID "..entry.AssignedGroupID) + -- outcomes - player ID'd + -- target dead or left zones handled above + -- target ID'd --> if hostile, assign INTERCEPT TASK + self.ManagedTasks:PullByID(entry.TID) + local Contact = self.Contacts:ReadByID(entry.Contact.CID) -- #AWACS.ManagedContact + if Contact and (Contact.IFF == AWACS.IFF.FRIENDLY or Contact.IFF == AWACS.IFF.NEUTRAL) then + self:T("IFF outcome friendly/neutral for GroupID "..entry.AssignedGroupID) + -- nothing todo, re-anchor + if managedgroup then + managedgroup.HasAssignedTask = false + self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",false,false,AWACS.TaskStatus.UNASSIGNED) + managedgroup.ContactCID = 0 + managedgroup.LastTasking = timer.getTime() + if managedgroup.IsAI then + managedgroup.CurrentAuftrag = 0 + else + managedgroup.CurrentTask = 0 + end + if managedgroup.IsPlayer then + entry.IsPlayerTask = false + end + --self.ManagedTasks:PullByID(entry.TID) + self.ManagedGrps[entry.AssignedGroupID] = managedgroup + self:__ReAnchor(5,managedgroup.GID) + end + elseif Contact and Contact.IFF == AWACS.IFF.ENEMY then + self:T("IFF outcome hostile for GroupID "..entry.AssignedGroupID) + -- change to intercept + --self.ManagedTasks:PullByID(entry.TID) + entry.ToDo = AWACS.TaskDescription.INTERCEPT + entry.Status = AWACS.TaskStatus.ASSIGNED + local cname = Contact.TargetGroupNaming + entry.ScreenText = string.format("Engage hostile %s group.",cname) + self.ManagedTasks:Push(entry,entry.TID) + local TextTTS = string.format("%s, %s. Engage hostile target!",managedgroup.CallSign,self.callsigntxt) + self:_NewRadioEntry(TextTTS,TextTTS,managedgroup.GID,true,self.debug,true,false,true) + elseif not Contact then + self:T("IFF outcome target DEAD for GroupID "..entry.AssignedGroupID) + -- nothing todo, re-anchor + if managedgroup then + managedgroup.HasAssignedTask = false + self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",false,false,AWACS.TaskStatus.UNASSIGNED) + managedgroup.ContactCID = 0 + managedgroup.LastTasking = timer.getTime() + if managedgroup.IsAI then + managedgroup.CurrentAuftrag = 0 + else + managedgroup.CurrentTask = 0 + end + if managedgroup.IsPlayer then + entry.IsPlayerTask = false + end + --self.ManagedTasks:PullByID(entry.TID) + self.ManagedGrps[entry.AssignedGroupID] = managedgroup + if managedgroup.Group:IsAlive() or managedgroup.FlightGroup:IsAlive() then + self:__ReAnchor(5,managedgroup.GID) + end + end + end + elseif entry.Status == AWACS.TaskStatus.FAILED then + -- outcomes - player unable/abort + -- Player dead managed above + -- Remove task + self:T("Open Tasks VID failed for GroupID "..entry.AssignedGroupID) + if managedgroup then + managedgroup.HasAssignedTask = false + self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",false,false,AWACS.TaskStatus.UNASSIGNED) + managedgroup.ContactCID = 0 + managedgroup.LastTasking = timer.getTime() + if managedgroup.IsAI then + managedgroup.CurrentAuftrag = 0 + else + managedgroup.CurrentTask = 0 + end + if managedgroup.IsPlayer then + entry.IsPlayerTask = false + end + self.ManagedGrps[entry.AssignedGroupID] = managedgroup + if managedgroup.Group:IsAlive() or managedgroup.FlightGroup:IsAlive() then + self:__ReAnchor(5,managedgroup.GID) + end + end + -- remove + self.ManagedTasks:PullByID(entry.TID) + self:__InterceptFailure(1) + end + end + + end + end + + return self +end + +--- [Internal] Write stats to log +-- @param #AWACS self +-- @return #AWACS self +function AWACS:_LogStatistics() + self:T(self.lid.."_LogStatistics") + local text = string.gsub(UTILS.OneLineSerialize(self.MonitoringData),",","\n") + local text = string.gsub(text,"{","\n") + local text = string.gsub(text,"}","") + local text = string.gsub(text,"="," = ") + self:T(text) + if self.MonitoringOn then + MESSAGE:New(text,20,"AWACS",false):ToAll() + end + return self +end + +--- [User] Add another AirWing for AI CAP Flights under management +-- @param #AWACS self +-- @param Ops.AirWing#AIRWING AirWing The AirWing to (also) obtain CAP flights from +-- @param Core.Zone#ZONE_RADIUS Zone (optional) This AirWing has it's own station zone, AI CAP will be send there +-- @return #AWACS self +function AWACS:AddCAPAirWing(AirWing,Zone) + self:T(self.lid.."AddCAPAirWing") + if AirWing then + AirWing:SetUsingOpsAwacs(self) + local distance = self.AOCoordinate:Get2DDistance(AirWing:GetCoordinate()) + if Zone then + -- create AnchorStack + local stackscreated = self.AnchorStacks:GetSize() + if stackscreated == self.AnchorMaxAnchors then + -- only create self.AnchorMaxAnchors Anchors + self:E(self.lid.."Max number of stacks already created!") + else + local AnchorStackOne = {} -- #AWACS.AnchorData + AnchorStackOne.AnchorBaseAngels = self.AnchorBaseAngels + AnchorStackOne.Anchors = FIFO:New() -- Utilities.FiFo#FIFO + AnchorStackOne.AnchorAssignedID = FIFO:New() -- Utilities.FiFo#FIFO + + local newname = Zone:GetName() + + for i=1,self.AnchorMaxStacks do + AnchorStackOne.Anchors:Push((i-1)*self.AnchorStackDistance+self.AnchorBaseAngels) + end + + local newsubname = AWACS.AnchorNames[stackscreated+1] or tostring(stackscreated+1) + newname = Zone:GetName() .. "-"..newsubname + AnchorStackOne.StationZone = Zone + AnchorStackOne.StationZoneCoordinate = Zone:GetCoordinate() + AnchorStackOne.StationZoneCoordinateText = Zone:GetCoordinate():ToStringLLDDM() + AnchorStackOne.StationName = newname + --push to AnchorStacks + if self.debug then + --self.AnchorStacks:Flush() + AnchorStackOne.StationZone:DrawZone(-1,{0,0,1},1,{0,0,1},0.2,5,true) + local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) + AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToAll() + else + local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) + AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToAll() + end + self.AnchorStacks:Push(AnchorStackOne,newname) + AirWing.HasOwnStation = true + AirWing.StationName = newname + end + end + self.CAPAirwings:Push(AirWing,distance) + end + return self +end + +--- [Internal] Announce a new contact +-- @param #AWACS self +-- @param #AWACS.ManagedContact Contact +-- @param #boolean IsNew Is a new contact +-- @param Wrapper.Group#GROUP Group Announce to Group if not nil +-- @param #boolean IsBogeyDope If true, this is a bogey dope announcement +-- @param #string Tag Tag name for this contact. Alpha, Brave, Charlie ... +-- @param #boolean IsPopup This is a pop-up group +-- @param #string ReportingName The NATO code reporting name for the contact, e.g. "Foxbat". "Bogey" if unknown. +-- @return #AWACS self +function AWACS:_AnnounceContact(Contact,IsNew,Group,IsBogeyDope,Tag,IsPopup,ReportingName) + self:T(self.lid.."_AnnounceContact") + --self:T({Contact}) + -- do we have a group to talk to? + local tag = "" + local Tag = Tag + local CID = 0 + if not Tag then + -- injected data available? + CID = Contact.CID or 0 + Tag = Contact.TargetGroupNaming or "" + --self:T({CID,Tag}) + end + if self.NoGroupTags then + Tag = nil + end + local isGroup = false + local GID = 0 + local grpcallsign = "Ghost 1" + if Group and Group:IsAlive() then + GID, isGroup,grpcallsign = self:_GetManagedGrpID(Group) + self:T("GID="..GID.." CheckedIn = "..tostring(isGroup)) + --grpcallsign = self:_GetCallSign(Group,GID) or "Ghost 1" + end + + local cluster = Contact.Cluster + local intel = self.intel -- Ops.Intelligence#INTEL + + local size = self.intel:ClusterCountUnits(cluster) + local threatsize, threatsizetext = self:_GetBlurredSize(size) + + local clustercoordinate = Contact.Cluster.coordinate or Contact.Contact.position + + local heading = Contact.Contact.group:GetHeading() or self.intel:CalcClusterDirection(cluster) + + clustercoordinate:SetHeading(Contact.Contact.group:GetHeading()) + + local BRAfromBulls, BRAfromBullsTTS = self:_GetBRAfromBullsOrAO(clustercoordinate) + + + self:T(BRAfromBulls) + self:T(BRAfromBullsTTS) + BRAfromBulls=BRAfromBulls.."." + BRAfromBullsTTS=BRAfromBullsTTS.."." + + if isGroup then + BRAfromBulls = clustercoordinate:ToStringBRAANATO(Group:GetCoordinate(),true,true) + BRAfromBullsTTS = string.gsub(BRAfromBulls,"BRAA","brah") + BRAfromBullsTTS = string.gsub(BRAfromBullsTTS,"BRA","brah") + if self.PathToGoogleKey then + BRAfromBullsTTS = clustercoordinate:ToStringBRAANATO(Group:GetCoordinate(),true,true,true,false,true) + end + end + + -- "Uzi 1-1, Magic, BRA, 183 for 10 at 2000, hot" + -- ", , /, , BRA for at angels , , " + + local BRAText = "" + local TextScreen = "" + + if isGroup then + BRAText = string.format("%s, %s.",grpcallsign,self.callsigntxt) + TextScreen = string.format("%s, %s.",grpcallsign,self.callsigntxt) + else + BRAText = string.format("%s.",self.callsigntxt) + TextScreen = string.format("%s.",self.callsigntxt) + end + + if IsNew and self.PlayerGuidance then + BRAText = BRAText .. " New group." + TextScreen = TextScreen .. " New group." + elseif IsPopup then + BRAText = BRAText .. " Pop-up group." + TextScreen = TextScreen .. " Pop-up group." + elseif IsBogeyDope and Tag and Tag ~= "" then + BRAText = BRAText .. " "..Tag.." group." + TextScreen = TextScreen .. " "..Tag.." group." + else + BRAText = BRAText .. " Group." + TextScreen = TextScreen .. " Group." + end + + if not IsBogeyDope then + if Tag and Tag ~= "" then + BRAText = BRAText .. " "..Tag.."." + TextScreen = TextScreen .. " "..Tag.."." + end + end + + if threatsize > 1 then + --BRAText = BRAText .. " "..threatsizetext..". "..BRAfromBullsTTS + BRAText = BRAText .. " "..BRAfromBullsTTS.." "..threatsizetext.."." + --TextScreen = TextScreen .. " "..threatsizetext..". "..BRAfromBulls + TextScreen = TextScreen .. " "..BRAfromBulls.." "..threatsizetext.."." + else + --BRAText = BRAText .. " "..threatsizetext..". "..BRAfromBullsTTS + BRAText = BRAText .. " "..BRAfromBullsTTS + --TextScreen = TextScreen .. " "..threatsizetext..". "..BRAfromBulls + TextScreen = TextScreen .. " "..BRAfromBulls + end + + if self.ModernEra then + -- Platform + if ReportingName and ReportingName ~= "Bogey" then + ReportingName = string.gsub(ReportingName,"_"," ") + BRAText = BRAText .. " "..ReportingName.."." + TextScreen = TextScreen .. " "..ReportingName.."." + end + -- High - > 40k feet + local height = Contact.Contact.group:GetHeight() + local height = UTILS.Round(UTILS.MetersToFeet(height)/1000,0) -- e.g, 25 + if height >= 40 then + BRAText = BRAText .. " High." + TextScreen = TextScreen .. " High." + end + -- Fast (>600kn) or Very fast (>900kn) + local speed = Contact.Contact.group:GetVelocityKNOTS() + if speed > 900 then + BRAText = BRAText .. " Very Fast." + TextScreen = TextScreen .. " Very Fast." + elseif speed >= 600 and speed <= 900 then + BRAText = BRAText .. " Fast." + TextScreen = TextScreen .. " Fast." + end + end + + string.gsub(BRAText,"BRAA","brah") + string.gsub(BRAText,"BRA","brah") + + --self:T(BRAText) + local prio = IsNew or IsBogeyDope + self:_NewRadioEntry(BRAText,TextScreen,GID,isGroup,true,IsNew,false,prio) + + return self +end + +--- [Internal] Check for alive OpsGroup from Mission OpsGroups table +-- @param #AWACS self +-- @param #table OpsGroups +-- @return Ops.OpsGroup#OPSGROUP or nil +function AWACS:_GetAliveOpsGroupFromTable(OpsGroups) + self:T(self.lid.."_GetAliveOpsGroupFromTable") + local handback = nil + for _,_OG in pairs(OpsGroups or {}) do + local OG = _OG -- Ops.OpsGroup#OPSGROUP + if OG and OG:IsAlive() then + handback = OG + --self:T("Handing back OG: " .. OG:GetName()) + break + end + end + return handback +end + +--- [Internal] Clean up mission stack +-- @param #AWACS self +-- @return #number CAPMissions +-- @return #number Alert5Missions +-- @return #number InterceptMissions +function AWACS:_CleanUpAIMissionStack() + self:T(self.lid.."_CleanUpAIMissionStack") + + local CAPMissions = 0 + local Alert5Missions = 0 + local InterceptMissions = 0 + + local MissionStack = FIFO:New() + + self:T("Checking MissionStack") + for _,_mission in pairs(self.CatchAllMissions) do + -- looking for missions of type CAP and ALERT5 + local mission = _mission -- Ops.Auftrag#AUFTRAG + local type = mission:GetType() + if type == AUFTRAG.Type.ALERT5 and mission:IsNotOver() then + MissionStack:Push(mission,mission.auftragsnummer) + Alert5Missions = Alert5Missions + 1 + elseif type == AUFTRAG.Type.CAP and mission:IsNotOver() then + MissionStack:Push(mission,mission.auftragsnummer) + CAPMissions = CAPMissions + 1 + elseif type == AUFTRAG.Type.INTERCEPT and mission:IsNotOver() then + MissionStack:Push(mission,mission.auftragsnummer) + InterceptMissions = InterceptMissions + 1 + end + end + + self.AICAPMissions = nil + self.AICAPMissions = MissionStack + + return CAPMissions, Alert5Missions, InterceptMissions + +end + +function AWACS:_ConsistencyCheck() + self:T(self.lid.."_ConsistencyCheck") + if self.debug then + self:T("CatchAllMissions") + local catchallm = {} + local report1 = REPORT:New("CatchAll") + report1:Add("====================") + report1:Add("CatchAllMissions") + report1:Add("====================") + for _,_mission in pairs(self.CatchAllMissions) do + local mission = _mission -- Ops.Auftrag#AUFTRAG + local nummer = mission.auftragsnummer or 0 + local type = mission:GetType() + local state = mission:GetState() + local FG = mission:GetOpsGroups() + local OG = self:_GetAliveOpsGroupFromTable(FG) + local OGName = "UnknownFromMission" + if OG then + OGName=OG:GetName() + end + report1:Add(string.format("Auftrag Nr %d Type %s State %s FlightGroup %s",nummer,type,state,OGName)) + if mission:IsNotOver() then + catchallm[#catchallm+1] = mission + end + end + + self.CatchAllMissions = nil + self.CatchAllMissions = catchallm + + local catchallfg = {} + + self:T("CatchAllFGs") + report1:Add("====================") + report1:Add("CatchAllFGs") + report1:Add("====================") + for _,_fg in pairs(self.CatchAllFGs) do + local FG = _fg -- Ops.FlightGroup#FLIGHTGROUP + local mission = FG:GetMissionCurrent() + local OGName = FG:GetName() or "UnknownFromFG" + local nummer = 0 + local type = "No Type" + local state = "None" + if mission then + type = mission:GetType() + nummer = mission.auftragsnummer or 0 + state = mission:GetState() + end + report1:Add(string.format("Auftrag Nr %d Type %s State %s FlightGroup %s",nummer,type,state,OGName)) + if FG:IsAlive() then + catchallfg[#catchallfg+1] = FG + end + end + report1:Add("====================") + self:T(report1:Text()) + + self.CatchAllFGs = nil + self.CatchAllFGs = catchallfg + + end + return self +end + +--- [Internal] Check Enough AI CAP on Station +-- @param #AWACS self +-- @return #AWACS self +function AWACS:_CheckAICAPOnStation() + self:T(self.lid.."_CheckAICAPOnStation") + + self:_ConsistencyCheck() + + local capmissions, alert5missions, interceptmissions = self:_CleanUpAIMissionStack() + self:I("CAP="..capmissions.." ALERT5="..alert5missions.." Requested="..self.AIRequested) + + if self.MaxAIonCAP > 0 then + + local onstation = capmissions + alert5missions + + --if self.AIRequested > self.MaxAIonCAP then + if capmissions > self.MaxAIonCAP then + -- too many, send one home + self:T(string.format("*** Onstation %d > MaxAIOnCAP %d",onstation,self.MaxAIonCAP)) + local mission = self.AICAPMissions:Pull() -- Ops.Auftrag#AUFTRAG + local Groups = mission:GetOpsGroups() + local OpsGroup = self:_GetAliveOpsGroupFromTable(Groups) + local GID,checkedin = self:_GetManagedGrpID(OpsGroup) + mission:__Cancel(30) + self.AIRequested = self.AIRequested - 1 + if checkedin then + self:_CheckOut(OpsGroup,GID) + end + end + + -- control number of AI CAP Flights + if capmissions < self.MaxAIonCAP then + -- not enough + local AnchorStackNo,free = self:_GetFreeAnchorStack() + if free then + -- create Alert5 and assign to ONE of our AWs + -- TODO better selection due to resource shortage? + local mission = AUFTRAG:NewALERT5(AUFTRAG.Type.CAP) + self.CatchAllMissions[#self.CatchAllMissions+1] = mission + local availableAWS = self.CAPAirwings:Count() + local AWS = self.CAPAirwings:GetDataTable() + -- round robin + self.AIRequested = self.AIRequested + 1 + local selectedAW = AWS[(((self.AIRequested-1) % availableAWS)+1)] + selectedAW:AddMission(mission) + self:I("CAP="..capmissions.." ALERT5="..alert5missions.." Requested="..self.AIRequested) + end + end + + -- Check CAP Mission states + if onstation > 0 and capmissions < self.MaxAIonCAP then + local missions = self.AICAPMissions:GetDataTable() + -- get mission type and state + for _,_Mission in pairs(missions) do + + local mission = _Mission -- Ops.Auftrag#AUFTRAG + self:T("Looking at AuftragsNr " .. mission.auftragsnummer) + local type = mission:GetType() + local state = mission:GetState() + + if type == AUFTRAG.Type.ALERT5 then + -- parked up for CAP + local OpsGroups = mission:GetOpsGroups() + local OpsGroup = self:_GetAliveOpsGroupFromTable(OpsGroups) + local FGstate = mission:GetGroupStatus(OpsGroup) + if OpsGroup then + FGstate = OpsGroup:GetState() + self:T("FG Object in state: " .. FGstate) + end + -- FG ready? + + if OpsGroup and (FGstate == "Parking" or FGstate == "Cruising") then + -- has this group checked in already? Avoid double tasking + local GID, CheckedInAlready = self:_GetManagedGrpID(OpsGroup:GetGroup()) + if not CheckedInAlready then + self:_SetAIROE(OpsGroup,OpsGroup:GetGroup()) + self:_CheckInAI(OpsGroup,OpsGroup:GetGroup(),mission.auftragsnummer) + end + end + end + end + end + + -- cycle mission status + if onstation > 0 then + local report = REPORT:New("CAP Mission Status") + report:Add("===============") + --local missionIDs = self.AICAPMissions:GetIDStackSorted() + local missions = self.AICAPMissions:GetDataTable() + local i = 1 + for _,_Mission in pairs(missions) do + local mission = _Mission -- Ops.Auftrag#AUFTRAG + if mission then + i = i + 1 + report:Add(string.format("Entry %d",i)) + report:Add(string.format("Mission No %d",mission.auftragsnummer)) + report:Add(string.format("Mission Type %s",mission:GetType())) + report:Add(string.format("Mission State %s",mission:GetState())) + local OpsGroups = mission:GetOpsGroups() + local OpsGroup = self:_GetAliveOpsGroupFromTable(OpsGroups) -- Ops.OpsGroup#OPSGROUP + if OpsGroup then + local OpsName = OpsGroup:GetName() or "Unknown" + --local OpsCallSign = OpsGroup:GetCallsignName() or "Unknown" + local found,GID,OpsCallSign = self:_GetGIDFromGroupOrName(OpsGroup) + report:Add(string.format("Mission FG %s",OpsName)) + report:Add(string.format("Callsign %s",OpsCallSign)) + report:Add(string.format("Mission FG State %s",OpsGroup:GetState())) + else + report:Add("***** Cannot obtain (yet) this missions OpsGroup!") + end + report:Add(string.format("Target Type %s",mission:GetTargetType())) + end + report:Add("===============") + end + if self.debug then + self:I(report:Text()) + end + end + end + return self +end + +--- [Internal] Set ROE for AI CAP +-- @param #AWACS self +-- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup +-- @param Wrapper.Group#GROUP Group +-- @return #AWACS self +function AWACS:_SetAIROE(FlightGroup,Group) + self:T(self.lid.."_SetAIROE") + local ROE = self.AwacsROE or AWACS.ROE.POLICE + local ROT = self.AwacsROT or AWACS.ROT.PASSIVE + + -- TODO adjust to AWACS set ROE + -- for the time being set to be defensive + Group:OptionAlarmStateGreen() + Group:OptionECM_OnlyLockByRadar() + Group:OptionROEHoldFire() + Group:OptionROTEvadeFire() + Group:OptionRTBBingoFuel(true) + Group:OptionKeepWeaponsOnThreat() + local callname = self.AICAPCAllName or CALLSIGN.Aircraft.Colt + self.AICAPCAllNumber = self.AICAPCAllNumber + 1 + Group:CommandSetCallsign(callname,math.fmod(self.AICAPCAllNumber,9)) + -- FG level + FlightGroup:SetDefaultAlarmstate(AI.Option.Ground.val.ALARM_STATE.GREEN) + FlightGroup:SetDefaultCallsign(callname,math.fmod(self.AICAPCAllNumber,9)) + if ROE == AWACS.ROE.POLICE or ROE == AWACS.ROE.VID then + FlightGroup:SetDefaultROE(ENUMS.ROE.WeaponHold) + elseif ROE == AWACS.ROE.IFF then + FlightGroup:SetDefaultROE(ENUMS.ROE.ReturnFire) + elseif ROE == AWACS.ROE.BVR then + FlightGroup:SetDefaultROE(ENUMS.ROE.OpenFire) + end + if ROT == AWACS.ROT.BYPASSESCAPE or ROT == AWACS.ROT.PASSIVE then + FlightGroup:SetDefaultROT(ENUMS.ROT.PassiveDefense) + elseif ROT == AWACS.ROT.OPENFIRE or ROT == AWACS.ROT.RETURNFIRE then + FlightGroup:SetDefaultROT(ENUMS.ROT.BypassAndEscape) + elseif ROT == AWACS.ROT.EVADE then + FlightGroup:SetDefaultROT(ENUMS.ROT.EvadeFire) + end + FlightGroup:SetFuelLowRTB(true) + FlightGroup:SetFuelLowThreshold(0.2) + FlightGroup:SetEngageDetectedOff() + FlightGroup:SetOutOfAAMRTB(true) + return self +end + +--- [Internal] TAC Range Call to Pilot +-- @param #AWACS self +-- @param #number GID GID +-- @param #AWACS.ManagedContact Contact +-- @return #AWACS self +function AWACS:_TACRangeCall(GID,Contact) + self:T(self.lid.."_TACRangeCall") + -- AIC: “Enforcer 11, single group, 30 miles.” + if not Contact then return self end + local pilotcallsign = self:_GetCallSign(nil,GID) + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + local contact = Contact.Contact -- Ops.Intelligence#INTEL.Contact + local contacttag = Contact.TargetGroupNaming + if contact and not Contact.TACCallDone then + local position = contact.position -- Core.Point#COORDINATE + if position then + local distance = position:Get2DDistance(managedgroup.Group:GetCoordinate()) + distance = UTILS.Round(UTILS.MetersToNM(distance)) -- 30nm - hopefully + local text = string.format("%s. %s. %s group, %d miles.",self.callsigntxt,pilotcallsign,contacttag,distance) + self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true) + self:_UpdateContactEngagementTag(Contact.CID,Contact.EngagementTag,true,false,AWACS.TaskStatus.EXECUTING) + end + end + return self +end + +--- [Internal] Meld Range Call to Pilot +-- @param #AWACS self +-- @param #number GID GID +-- @param #AWACS.ManagedContact Contact +-- @return #AWACS self +function AWACS:_MeldRangeCall(GID,Contact) + self:T(self.lid.."_MeldRangeCall") + if not Contact then return self end + -- AIC: “Heat 11, single group, BRAA 089/28, 32 thousand, hot, hostile, crow.” + local pilotcallsign = self:_GetCallSign(nil,GID) + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + local flightpos = managedgroup.Group:GetCoordinate() + local contact = Contact.Contact -- Ops.Intelligence#INTEL.Contact + local contacttag = Contact.TargetGroupNaming + if contact and not Contact.MeldCallDone then + local position = contact.position -- Core.Point#COORDINATE + if position then + local BRATExt = "" + if self.PathToGoogleKey then + BRATExt = position:ToStringBRAANATO(flightpos,false,false,true,false,true) + else + BRATExt = position:ToStringBRAANATO(flightpos,false,false) + end + local text = string.format("%s. %s. %s group, %s",self.callsigntxt,pilotcallsign,contacttag,BRATExt) + self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true) + self:_UpdateContactEngagementTag(Contact.CID,Contact.EngagementTag,true,true,AWACS.TaskStatus.EXECUTING) + end + end + return self +end + +--- [Internal] Threat Range Call to Pilot +-- @param #AWACS self +-- @return #AWACS self +function AWACS:_ThreatRangeCall(GID,Contact) + self:T(self.lid.."_ThreatRangeCall") + if not Contact then return self end + -- AIC: “Enforcer 11 12, east group, THREAT, BRAA 260/15, 29 thousand, hot, hostile, robin.” + local pilotcallsign = self:_GetCallSign(nil,GID) + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + local flightpos = managedgroup.Group:GetCoordinate() or managedgroup.LastKnownPosition + local contact = Contact.Contact -- Ops.Intelligence#INTEL.Contact + local contacttag = Contact.TargetGroupNaming + if contact then + local position = contact.position or contact.group:GetCoordinate() -- Core.Point#COORDINATE + if position then + local BRATExt = "" + if self.PathToGoogleKey then + BRATExt = position:ToStringBRAANATO(flightpos,false,false,true,false,true) + else + BRATExt = position:ToStringBRAANATO(flightpos,false,false) + end + local text = string.format("%s. %s. %s group, Threat. %s",self.callsigntxt,pilotcallsign,contacttag,BRATExt) + self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true) + end + end + return self +end + +--- [Internal] Merged Call to Pilot +-- @param #AWACS self +-- @param #number GID +-- @return #AWACS self +function AWACS:_MergedCall(GID) + self:T(self.lid.."_MergedCall") + -- AIC: “Enforcer, mergedb” + local pilotcallsign = self:_GetCallSign(nil,GID) + --local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + local text = string.format("%s. %s. Merged.",self.callsigntxt,pilotcallsign) + self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true) + return self +end + +--- [Internal] Assign a Pilot to a target +-- @param #AWACS self +-- @param #table Pilots Table of #AWACS.ManagedGroup Pilot +-- @param Utilities.FiFo#FIFO Targets FiFo of #AWACS.ManagedContact Targets +-- @return #AWACS self +function AWACS:_AssignPilotToTarget(Pilots,Targets) + self:T(self.lid.."_AssignPilotToTarget") + + local inreach = false + local Pilot = nil -- #AWACS.ManagedGroup + + + local closest = UTILS.NMToMeters(self.maxassigndistance+1) + local targets = Targets:GetDataTable() + local Target = nil + + for _,_target in pairs(targets) do + -- Check Distance + local targetgroupcoord = _target.Contact.position + -- get closest pilot from target + for _,_Pilot in pairs(Pilots) do + local pilotcoord = _Pilot.Group:GetCoordinate() + local targetdist = targetgroupcoord:Get2DDistance(pilotcoord) + if UTILS.MetersToNM(targetdist) < self.maxassigndistance and targetdist < closest then + self:T(string.format("%sTarget distance %d! Assignment %s!",self.lid,UTILS.Round(UTILS.MetersToNM(targetdist),0),_Pilot.CallSign)) + inreach = true + closest = targetdist + Pilot = _Pilot + Target = _target + Targets:PullByID(_target.CID) + break + else + self:T(self.lid .. "Target distance > "..self.maxassigndistance.."NM! No Assignment!") + end + end + end + + -- DONE Check Human assignment working + if inreach and Pilot and Pilot.IsPlayer then + local callsign = Pilot.CallSign + -- update pilot TaskSheet + self.ManagedTasks:PullByID(Pilot.CurrentTask) + + Pilot.HasAssignedTask = true + local TargetPosition = Target.Target:GetCoordinate() + local PlayerPositon = Pilot.LastKnownPosition + local TargetAlt = Target.Contact.altitude or Target.Cluster.altitude or Target.Contact.group:GetAltitude() + local TargetDirections, TargetDirectionsTTS = self:_ToStringBRA(PlayerPositon,TargetPosition,TargetAlt) + local ScreenText = "" + local TaskType = AWACS.TaskDescription.INTERCEPT + if self.AwacsROE == AWACS.ROE.POLICE or self.AwacsROE == AWACS.ROE.VID then + ScreenText = string.format("Intercept and VID %s group.",Target.TargetGroupNaming) + TaskType = AWACS.TaskDescription.VID + else + ScreenText = string.format("Intercept %s group.",Target.TargetGroupNaming) + end + Pilot.CurrentTask = self:_CreateTaskForGroup(Pilot.GID,TaskType,ScreenText,Target.Target,AWACS.TaskStatus.REQUESTED,nil,Target.Cluster,Target.Contact) + + Pilot.ContactCID = Target.CID + + -- update managed group + self.ManagedGrps[Pilot.GID] = Pilot + + -- Update Contact Status + Target.LinkedTask = Pilot.CurrentTask + Target.LinkedGroup = Pilot.GID + Target.Status = AWACS.TaskStatus.REQUESTED + Target.EngagementTag = string.format("Targeted by %s.",Pilot.CallSign) + + self.Contacts:PullByID(Target.CID) + self.Contacts:Push(Target,Target.CID) + + local text = string.format("%s. %s group. %s. %s, request commit.", self.callsigntxt,Target.TargetGroupNaming,TargetDirectionsTTS,Pilot.CallSign) + local textScreen = string.format("%s. %s group. %s. %s, request commit.", self.callsigntxt,Target.TargetGroupNaming,TargetDirections,Pilot.CallSign) + + self:_NewRadioEntry(text,textScreen,Pilot.GID,true,self.debug,true,false,true) + + elseif inreach and Pilot and Pilot.IsAI then + -- Target information + local callsign = Pilot.CallSign + local FGStatus = Pilot.FlightGroup:GetState() + self:T("Pilot AI Callsign: " .. callsign) + self:T("Pilot FG State: " .. FGStatus) + local targetstatus = Target.Target:GetState() + self:T("Target State: " .. targetstatus) + + -- + local currmission = Pilot.FlightGroup:GetMissionCurrent() + if currmission then + self:T("Current Mission: " .. currmission:GetType()) + end + -- create one intercept Auftrag and one to return to CAP post this one + local ZoneSet = self.ZoneSet + local RejectZoneSet = self.RejectZoneSet + + local intercept = AUFTRAG:NewINTERCEPT(Target.Target) + intercept:SetWeaponExpend(AI.Task.WeaponExpend.ALL) + intercept:SetWeaponType(ENUMS.WeaponFlag.Auto) + + -- TODO + -- now this is going to be interesting... + -- Check if the target left the "hot" area or is dead already + intercept:AddConditionSuccess( + function(target,zoneset,rzoneset) + -- BASE:I("AUFTRAG Condition Succes Eval Running") + local success = true + local target = target -- Ops.Target#TARGET + if target:IsDestroyed() then return true end + local tgtcoord = target:GetCoordinate() + local tgtvec2 = nil + if tgtcoord then + tgtvec2 = tgtcoord:GetVec2() + end + local zones = zoneset -- Core.Set#SET_ZONE + local rzones = rzoneset -- Core.Set#SET_ZONE + if tgtvec2 then + zones:ForEachZone( + function(zone) + -- BASE:I("AUFTRAG Condition Succes ZONE Eval Running") + if zone:IsVec2InZone(tgtvec2) then + success = false + end + end + ) + rzones:ForEachZone( + function(zone) + -- BASE:I("AUFTRAG Condition Succes REJECT ZONE Eval Running") + if zone:IsVec2InZone(tgtvec2) then + success = true + end + end + ) + end + return success + end, + Target.Target, + ZoneSet, + RejectZoneSet + ) + + Pilot.FlightGroup:AddMission(intercept) + + local Angels = Pilot.AnchorStackAngels or 25 + Angels = Angels * 1000 + local AnchorSpeed = self.CapSpeedBase or 270 + AnchorSpeed = UTILS.KnotsToAltKIAS(AnchorSpeed,Angels) + local Anchor = self.AnchorStacks:ReadByPointer(Pilot.AnchorStackNo) -- #AWACS.AnchorData + local capauftrag = AUFTRAG:NewCAP(Anchor.StationZone,Angels,AnchorSpeed,Anchor.StationZoneCoordinate,0,15,{}) + capauftrag:SetTime(nil,((self.CAPTimeOnStation*3600)+(15*60))) + Pilot.FlightGroup:AddMission(capauftrag) + + -- cancel current mission + if currmission then + currmission:__Cancel(3) + end + + -- update known mission list + self.CatchAllMissions[#self.CatchAllMissions+1] = intercept + self.CatchAllMissions[#self.CatchAllMissions+1] = capauftrag + + -- update pilot TaskSheet + self.ManagedTasks:PullByID(Pilot.CurrentTask) + + Pilot.HasAssignedTask = true + Pilot.CurrentTask = self:_CreateTaskForGroup(Pilot.GID,AWACS.TaskDescription.INTERCEPT,"Intercept Task",Target.Target,AWACS.TaskStatus.ASSIGNED,intercept,Target.Cluster,Target.Contact) + Pilot.CurrentAuftrag = intercept.auftragsnummer + Pilot.ContactCID = Target.CID + + -- update managed group + self.ManagedGrps[Pilot.GID] = Pilot + + -- Update Contact Status + Target.LinkedTask = Pilot.CurrentTask + Target.LinkedGroup = Pilot.GID + Target.Status = AWACS.TaskStatus.ASSIGNED + Target.EngagementTag = string.format("Targeted by %s.",Pilot.CallSign) + + self.Contacts:PullByID(Target.CID) + self.Contacts:Push(Target,Target.CID) + + -- message commit and return commit from AI + --local bratext = Target.Contact.position:ToStringBRA(Pilot.Group:GetCoordinate()) + + local altitude = Target.Contact.altitude or Target.Contact.group:GetAltitude() + local position = Target.Cluster.coordinate or Target.Contact.position + if not position then + self.intel:GetClusterCoordinate(Target.Cluster,true) + end + local bratext, bratexttts = self:_ToStringBRA(Pilot.Group:GetCoordinate(),position,altitude or 8000) + + local text = string.format("%s. %s group. %s. %s, commit.", self.callsigntxt,Target.TargetGroupNaming,bratexttts,Pilot.CallSign) + local textScreen = string.format("%s. %s group. %s. %s, request commit.", self.callsigntxt,Target.TargetGroupNaming,bratext,Pilot.CallSign) + + self:_NewRadioEntry(text,textScreen,Pilot.GID,true,self.debug,true,false,true) + + local text = string.format("%s. Commit.",Pilot.CallSign) + + self:_NewRadioEntry(text,text,Pilot.GID,true,self.debug,true,true,true) + + self:__Intercept(2) + + end + + return self +end + +-- TODO FSMs +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- [Internal] onafterStart +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AWACS self +function AWACS:onafterStart(From, Event, To) + self:T({From, Event, To}) + + -- Set up control zone + local controlzonename = "FEZ-"..self.AOName + self.ControlZone = ZONE_RADIUS:New(controlzonename,self.OpsZone:GetVec2(),UTILS.NMToMeters(self.ControlZoneRadius)) + if self.debug then + self.ControlZone:DrawZone(-1,{0,1,0},1,{1,0,0},0.05,3,true) + --MARKER:New(self.ControlZone:GetCoordinate(),"Radar Zone"):ToAll() + self.OpsZone:DrawZone(-1,{1,0,0},1,{1,0,0},0.2,5,true) + local AOCoordString = self.AOCoordinate:ToStringLLDDM() + local Rocktag = string.format("FEZ: %s\nBulls Coordinate: %s",self.AOName,AOCoordString) + MARKER:New(self.AOCoordinate,Rocktag):ToAll() + self.StationZone:DrawZone(-1,{0,0,1},1,{0,0,1},0.2,5,true) + local stationtag = string.format("Station: %s\nCoordinate: %s",self.StationZoneName,self.StationZone:GetCoordinate():ToStringLLDDM()) + if not self.GCI then + MARKER:New(self.StationZone:GetCoordinate(),stationtag):ToAll() + self.OrbitZone:DrawZone(-1,{0,1,0},1,{0,1,0},0.2,5,true) + MARKER:New(self.OrbitZone:GetCoordinate(),"AIC Orbit Zone"):ToAll() + end + else + local AOCoordString = self.AOCoordinate:ToStringLLDDM() + local Rocktag = string.format("FEZ: %s\nBulls Coordinate: %s",self.AOName,AOCoordString) + MARKER:New(self.AOCoordinate,Rocktag):ToAll() + if not self.GCI then + MARKER:New(self.OrbitZone:GetCoordinate(),"AIC Orbit Zone"):ToAll() + end + local stationtag = string.format("Station: %s\nCoordinate: %s",self.StationZoneName,self.StationZone:GetCoordinate():ToStringLLDDM()) + MARKER:New(self.StationZone:GetCoordinate(),stationtag):ToAll() + end + + if not self.GCI then + -- set up the AWACS and let it orbit + local AwacsAW = self.AirWing -- Ops.AirWing#AIRWING + local mission = AUFTRAG:NewORBIT_RACETRACK(self.OrbitZone:GetCoordinate(),self.AwacsAngels*1000,self.Speed,self.Heading,self.Leg) + local timeonstation = (self.AwacsTimeOnStation + self.ShiftChangeTime) * 3600 + mission:SetTime(nil,timeonstation) + self.CatchAllMissions[#self.CatchAllMissions+1] = mission + + AwacsAW:AddMission(mission) + + self.AwacsMission = mission + self.AwacsInZone = false -- not yet arrived or gone again + self.AwacsReady = false + else + self.AwacsInZone = true -- for GCI - arrived + self.AwacsReady = true + self:_StartIntel(self.GCIGroup) + + if self.GCIGroup:IsGround() then + self.AwacsFG = ARMYGROUP:New(self.GCIGroup) + self.AwacsFG:SetDefaultRadio(self.Frequency,self.Modulation) + self.AwacsFG:SwitchRadio(self.Frequency,self.Modulation) + elseif self.GCIGroup:IsShip() then + self.AwacsFG = NAVYGROUP:New(self.GCIGroup) + self.AwacsFG:SetDefaultRadio(self.Frequency,self.Modulation) + self.AwacsFG:SwitchRadio(self.Frequency,self.Modulation) + else + self:E(self.lid.."**** Group unsuitable for GCI ops! Needs to be a GROUND or SHIP type group!") + self:Stop() + return self + end + + self.AwacsFG:SetSRS(self.PathToSRS,self.Gender,self.Culture,self.Voice,self.Port,self.PathToGoogleKey,"AWACS",self.Volume) + self.callsigntxt = string.format("%s",AWACS.CallSignClear[self.CallSign]) + self:__CheckRadioQueue(-10) + + local text = string.format("%s. All stations, SUNRISE SUNRISE SUNRISE, %s.",self.callsigntxt,self.callsigntxt) + self:_NewRadioEntry(text,text,0,false,false,false,false,true) + self:T(self.lid..text) + self.sunrisedone = true + end + + local ZoneSet = SET_ZONE:New() + ZoneSet:AddZone(self.ControlZone) + + if not self.GCI then + ZoneSet:AddZone(self.OrbitZone) + end + + if self.BorderZone then + ZoneSet:AddZone(self.BorderZone) + end + + local RejectZoneSet = SET_ZONE:New() + if self.RejectZone then + RejectZoneSet:AddZone(self.RejectZone) + end + + self.ZoneSet = ZoneSet + self.RejectZoneSet = RejectZoneSet + + if self.AllowMarkers then + -- Add MarkerOps + + local MarkerOps = MARKEROPS_BASE:New("AWACS",{"Station","Delete","Move"}) + + local function Handler(Keywords,Coord,Text) + self:I(Text) + for _,_word in pairs (Keywords) do + if string.lower(_word) == "station" then + -- get the station name from the text field + local Name = string.match(Text," ([%a]+)$") + self:_CreateAnchorStackFromMarker(Name,Coord) + break + elseif string.lower(_word) == "delete" then + -- get the station name from the text field + local Name = string.match(Text," ([%a]+)$") + self:_DeleteAnchorStackFromMarker(Name,Coord) + break + elseif string.lower(_word) == "move" then + -- get the station name from the text field + local Name = string.match(Text," ([%a]+)$") + self:_MoveAnchorStackFromMarker(Name,Coord) + break + end + end + end + + -- Event functions + function MarkerOps:OnAfterMarkAdded(From,Event,To,Text,Keywords,Coord) + --local m = MESSAGE:New(string.format("AWACS %s Mark Added.", self.Tag),10,"Info",true):ToAllIf(self.debug) + BASE:I(string.format("%s Mark Added.", self.Tag)) + Handler(Keywords,Coord,Text) + end + + function MarkerOps:OnAfterMarkChanged(From,Event,To,Text,Keywords,Coord) + BASE:I(string.format("%s Mark Changed.", self.Tag)) + --local m = MESSAGE:New(string.format("AWACS %s Mark Changed.", self.Tag),10,"Info",true):ToAllIf(self.debug) + Handler(Keywords,Coord,Text) + end + + function MarkerOps:OnAfterMarkDeleted(From,Event,To) + BASE:I(string.format("%s Mark Deleted.", self.Tag)) + --local m = MESSAGE:New(string.format("AWACS %s Mark Deleted.", self.Tag),10,"Info",true):ToAllIf(self.debug) + end + + self.MarkerOps = MarkerOps + + end + + if self.GCI then + -- set FSM to started + self:__Started(-5) + end + + self:__Status(-30) + return self +end + +function AWACS:_CheckAwacsStatus() + self:T(self.lid.."_CheckAwacsStatus") + + local awacs = nil -- Wrapper.Group#GROUP + if self.AwacsFG then + awacs = self.AwacsFG:GetGroup() -- Wrapper.Group#GROUP + end + + local monitoringdata = self.MonitoringData -- #AWACS.MonitoringData + + if not self.GCI then + if awacs and awacs:IsAlive() and not self.AwacsInZone then + -- check if we arrived + local orbitzone = self.OrbitZone -- Core.Zone#ZONE + if awacs:IsInZone(orbitzone) then + -- arrived + self.AwacsInZone = true + self:T(self.lid.."Arrived in Orbit Zone: " .. orbitzone:GetName()) + local text = string.format("%s on station for %s control.",self.callsigntxt,self.AOName or "Rock") + local textScreen = string.format("%s on station for %s control.",self.callsigntxt,self.AOName or "Rock") + self:_NewRadioEntry(text,textScreen,0,false,true,true,false,true) + end + end + end + -------------------------------- + -- AWACS + -------------------------------- + + if (awacs and awacs:IsAlive()) then + + if not self.intelstarted then + local alt = UTILS.Round(UTILS.MetersToFeet(awacs:GetAltitude())/1000,0) + if alt >= 10 then + self:_StartIntel(awacs) + end + end + + if self.intelstarted and not self.sunrisedone then + -- TODO Sunrise call on after airborne at ca 10k feet + local alt = UTILS.Round(UTILS.MetersToFeet(awacs:GetAltitude())/1000,0) + if alt >= 10 then + local text = string.format("%s. All stations, SUNRISE SUNRISE SUNRISE, %s.",self.callsigntxt,self.callsigntxt) + self:_NewRadioEntry(text,text,0,false,false,false,false,true) + --self.AwacsFG:RadioTransmission(text,1,false) + self:T(self.lid..text) + self.sunrisedone = true + end + end + + -- Check on Awacs Mission Status + local AWmission = self.AwacsMission -- Ops.Auftrag#AUFTRAG + local awstatus = AWmission:GetState() + local AWmissiontime = (timer.getTime() - self.AwacsTimeStamp) + + local AWTOSLeft = UTILS.Round((((self.AwacsTimeOnStation+self.ShiftChangeTime)*3600) - AWmissiontime),0) -- seconds + + AWTOSLeft = UTILS.Round(AWTOSLeft/60,0) -- minutes + + local ChangeTime = UTILS.Round(((self.ShiftChangeTime * 3600)/60),0) + + local Changedue = "No" + + if not self.ShiftChangeAwacsFlag and (AWTOSLeft <= ChangeTime or AWmission:IsOver()) then + Changedue = "Yes" + self.ShiftChangeAwacsFlag = true + self:__AwacsShiftChange(2) + end + + local report = REPORT:New("AWACS:") + report:Add("====================") + report:Add("AWACS:") + report:Add(string.format("Auftrag Status: %s",awstatus)) + report:Add(string.format("TOS Left: %d min",AWTOSLeft)) + report:Add(string.format("Needs ShiftChange: %s",Changedue)) + + local OpsGroups = AWmission:GetOpsGroups() + local OpsGroup = self:_GetAliveOpsGroupFromTable(OpsGroups) -- Ops.OpsGroup#OPSGROUP + if OpsGroup then + local OpsName = OpsGroup:GetName() or "Unknown" + local OpsCallSign = OpsGroup:GetCallsignName() or "Unknown" + report:Add(string.format("Mission FG %s",OpsName)) + report:Add(string.format("Callsign %s",OpsCallSign)) + report:Add(string.format("Mission FG State %s",OpsGroup:GetState())) + else + report:Add("***** Cannot obtain (yet) this missions OpsGroup!") + end + + -- Check for replacement mission - if any + if self.ShiftChangeAwacsFlag and self.ShiftChangeAwacsRequested then -- Ops.Auftrag#AUFTRAG + AWmission = self.AwacsMissionReplacement + local esstatus = AWmission:GetState() + local ESmissiontime = (timer.getTime() - self.AwacsTimeStamp) + local ESTOSLeft = UTILS.Round((((self.AwacsTimeOnStation+self.ShiftChangeTime)*3600) - ESmissiontime),0) -- seconds + ESTOSLeft = UTILS.Round(ESTOSLeft/60,0) -- minutes + local ChangeTime = UTILS.Round(((self.ShiftChangeTime * 3600)/60),0) + --local Changedue = "No" + + --report:Add("====================") + report:Add("AWACS REPLACEMENT:") + report:Add(string.format("Auftrag Status: %s",esstatus)) + report:Add(string.format("TOS Left: %d min",ESTOSLeft)) + --report:Add(string.format("Needs ShiftChange: %s",Changedue)) + + local OpsGroups = AWmission:GetOpsGroups() + local OpsGroup = self:_GetAliveOpsGroupFromTable(OpsGroups) -- Ops.OpsGroup#OPSGROUP + if OpsGroup then + local OpsName = OpsGroup:GetName() or "Unknown" + local OpsCallSign = OpsGroup:GetCallsignName() or "Unknown" + report:Add(string.format("Mission FG %s",OpsName)) + report:Add(string.format("Callsign %s",OpsCallSign)) + report:Add(string.format("Mission FG State %s",OpsGroup:GetState())) + else + report:Add("***** Cannot obtain (yet) this missions OpsGroup!") + end + + if AWmission:IsExecuting() then + -- make the actual change in the queue + self.ShiftChangeAwacsFlag = false + self.ShiftChangeAwacsRequested = false + self.sunrisedone = false + -- cancel old mission + if self.AwacsMission and self.AwacsMission:IsNotOver() then + self.AwacsMission:Cancel() + end + self.AwacsMission = self.AwacsMissionReplacement + self.AwacsMissionReplacement = nil + self.AwacsTimeStamp = timer.getTime() + report:Add("*** Replacement DONE ***") + end + report:Add("====================") + end + + -------------------------------- + -- ESCORTS + -------------------------------- + + if self.HasEscorts then + for i=1, self.EscortNumber do + local ESmission = self.EscortMission[i] -- Ops.Auftrag#AUFTRAG + if not ESmission then break end + local esstatus = ESmission:GetState() + local ESmissiontime = (timer.getTime() - self.EscortsTimeStamp) + local ESTOSLeft = UTILS.Round((((self.EscortsTimeOnStation+self.ShiftChangeTime)*3600) - ESmissiontime),0) -- seconds + ESTOSLeft = UTILS.Round(ESTOSLeft/60,0) -- minutes + local ChangeTime = UTILS.Round(((self.ShiftChangeTime * 3600)/60),0) + local Changedue = "No" + + if (ESTOSLeft <= ChangeTime and not self.ShiftChangeEscortsFlag) or (ESmission:IsOver() and not self.ShiftChangeEscortsFlag) then + Changedue = "Yes" + self.ShiftChangeEscortsFlag = true -- set this back when new Escorts arrived + self:__EscortShiftChange(2) + end + + report:Add("====================") + report:Add("ESCORTS:") + report:Add(string.format("Auftrag Status: %s",esstatus)) + report:Add(string.format("TOS Left: %d min",ESTOSLeft)) + report:Add(string.format("Needs ShiftChange: %s",Changedue)) + + local OpsGroups = ESmission:GetOpsGroups() + local OpsGroup = self:_GetAliveOpsGroupFromTable(OpsGroups) -- Ops.OpsGroup#OPSGROUP + if OpsGroup then + local OpsName = OpsGroup:GetName() or "Unknown" + local OpsCallSign = OpsGroup:GetCallsignName() or "Unknown" + report:Add(string.format("Mission FG %s",OpsName)) + report:Add(string.format("Callsign %s",OpsCallSign)) + report:Add(string.format("Mission FG State %s",OpsGroup:GetState())) + monitoringdata.EscortsStateMission[i] = esstatus + monitoringdata.EscortsStateFG[i] = OpsGroup:GetState() + else + report:Add("***** Cannot obtain (yet) this missions OpsGroup!") + end + + report:Add("====================") + + -- Check for replacement mission - if any + if self.ShiftChangeEscortsFlag and self.ShiftChangeEscortsRequested then -- Ops.Auftrag#AUFTRAG + ESmission = self.EscortMissionReplacement[i] + local esstatus = ESmission:GetState() + local ESmissiontime = (timer.getTime() - self.EscortsTimeStamp) + local ESTOSLeft = UTILS.Round((((self.EscortsTimeOnStation+self.ShiftChangeTime)*3600) - ESmissiontime),0) -- seconds + ESTOSLeft = UTILS.Round(ESTOSLeft/60,0) -- minutes + local ChangeTime = UTILS.Round(((self.ShiftChangeTime * 3600)/60),0) + --local Changedue = "No" + + --report:Add("====================") + report:Add("ESCORTS REPLACEMENT:") + report:Add(string.format("Auftrag Status: %s",esstatus)) + report:Add(string.format("TOS Left: %d min",ESTOSLeft)) + --report:Add(string.format("Needs ShiftChange: %s",Changedue)) + + local OpsGroups = ESmission:GetOpsGroups() + local OpsGroup = self:_GetAliveOpsGroupFromTable(OpsGroups) -- Ops.OpsGroup#OPSGROUP + if OpsGroup then + local OpsName = OpsGroup:GetName() or "Unknown" + local OpsCallSign = OpsGroup:GetCallsignName() or "Unknown" + report:Add(string.format("Mission FG %s",OpsName)) + report:Add(string.format("Callsign %s",OpsCallSign)) + report:Add(string.format("Mission FG State %s",OpsGroup:GetState())) + else + report:Add("***** Cannot obtain (yet) this missions OpsGroup!") + end + + if ESmission:IsExecuting() then + -- make the actual change in the queue + self.ShiftChangeEscortsFlag = false + self.ShiftChangeEscortsRequested = false + -- cancel old mission + if ESmission and ESmission:IsNotOver() then + ESmission:Cancel() + end + self.EscortMission[i] = self.EscortMissionReplacement[i] + self.EscortMissionReplacement[i] = nil + self.EscortsTimeStamp = timer.getTime() + report:Add("*** Replacement DONE ***") + end + report:Add("====================") + end + end + end + + if self.debug then + self:T(report:Text()) + end + + else + -- Check on Awacs Mission Status + local AWmission = self.AwacsMission -- Ops.Auftrag#AUFTRAG + local awstatus = AWmission:GetState() + if AWmission:IsOver() then + -- yup we're dead + self:I(self.lid.."*****AWACS is dead!*****") + self.ShiftChangeAwacsFlag = true + self:__AwacsShiftChange(2) + end + end + + return monitoringdata +end + +--- [Internal] onafterStatus +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AWACS self +function AWACS:onafterStatus(From, Event, To) + self:I({From, Event, To}) + + self:_SetClientMenus() + + local monitoringdata = self.MonitoringData -- #AWACS.MonitoringData + + if not self.GCI then + monitoringdata = self:_CheckAwacsStatus() + end + + local awacsalive = false + if self.AwacsFG then + local awacs = self.AwacsFG:GetGroup() -- Wrapper.Group#GROUP + if awacs and awacs:IsAlive() then + awacsalive= true + end + end + + -- Check on AUFTRAG status for CAP AI + if self:Is("Running") and (awacsalive or self.AwacsInZone) then + + self:_CheckAICAPOnStation() + + self:_CleanUpContacts() + + self:_CheckMerges() + + if self.debug then + --local outcome, targets = self:_TargetSelectionProcess() -- TODO for debug ATM + end + + local outcome, targets = self:_TargetSelectionProcess(true) + + self:_CheckTaskQueue() + + local AI, Humans = self:_GetIdlePilots() + -- assign Pilot if there are targets and available Pilots, prefer Humans to AI + -- TODO - Implemented AI First, Humans laters - need to work out how to loop the targets to assign a pilot + if outcome and #Humans > 0 and self.PlayerCapAssigment then + -- add a task for AI + self:_AssignPilotToTarget(Humans,targets) + end + if outcome and #AI > 0 then + -- add a task for AI + self:_AssignPilotToTarget(AI,targets) + end + end + + if not self.GCI then + monitoringdata.AwacsShiftChange = self.ShiftChangeAwacsFlag + + if self.AwacsFG then + monitoringdata.AwacsStateFG = self.AwacsFG:GetState() + end + + monitoringdata.AwacsStateMission = self.AwacsMission:GetState() + monitoringdata.EscortsShiftChange = self.ShiftChangeEscortsFlag + end + + monitoringdata.AICAPCurrent = self.AICAPMissions:Count() + monitoringdata.AICAPMax = self.MaxAIonCAP + monitoringdata.Airwings = self.CAPAirwings:Count() + + self.MonitoringData = monitoringdata + + if self.debug then + self:_LogStatistics() + end + + local picturetime = timer.getTime() - self.PictureTimeStamp + + if self.AwacsInZone and picturetime > self.PictureInterval then + -- reset timer + self.PictureTimeStamp = timer.getTime() + self:_Picture(nil,true) + end + + self:__Status(30) + + return self +end + +--- [Internal] onafterStop +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AWACS self +function AWACS:onafterStop(From, Event, To) + self:T({From, Event, To}) + -- unhandle stuff, exit intel + + self.intel:Stop() + + local AWFiFo = self.CAPAirwings -- Utilities.FiFo#FIFO + local AWStack = AWFiFo:GetPointerStack() + for _ID,_AWID in pairs(AWStack) do + local SubAW = self.CAPAirwings:ReadByPointer(_ID) + if SubAW then + SubAW:RemoveUsingOpsAwacs() + end + end + -- Events + -- Player joins + self:UnHandleEvent(EVENTS.PlayerEnterAircraft) + self:UnHandleEvent(EVENTS.PlayerEnterUnit) + -- Player leaves + self:UnHandleEvent(EVENTS.PlayerLeaveUnit) + self:UnHandleEvent(EVENTS.Ejection) + self:UnHandleEvent(EVENTS.Crash) + self:UnHandleEvent(EVENTS.Dead) + self:UnHandleEvent(EVENTS.UnitLost) + self:UnHandleEvent(EVENTS.BDA) + self:UnHandleEvent(EVENTS.PilotDead) + -- Missile warning + self:UnHandleEvent(EVENTS.Shot) + + return self +end + +--- [Internal] onafterAssignAnchor +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param #number GID Group ID +-- @param #boolean HasOwnStation +-- @param #string HasOwnStation +-- @return #AWACS self +function AWACS:onafterAssignAnchor(From, Event, To, GID, HasOwnStation, StationName) + self:T({From, Event, To, "GID = " .. GID}) + self:_AssignAnchorToID(GID, HasOwnStation, StationName) + return self +end + +--- [Internal] onafterCheckedOut +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param #AWACS.ManagedGroup.GID Group ID +-- @param #number AnchorStackNo +-- @param #number Angels +-- @return #AWACS self +function AWACS:onafterCheckedOut(From, Event, To, GID, AnchorStackNo, Angels) + self:T({From, Event, To, "GID = " .. GID}) + self:_RemoveIDFromAnchor(GID,AnchorStackNo,Angels) + return self +end + +--- [Internal] onafterAssignedAnchor +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param #number GID Managed Group ID +-- @param #AWACS.AnchorData Anchor +-- @param #number AnchorStackNo +-- @return #AWACS self +function AWACS:onafterAssignedAnchor(From, Event, To, GID, Anchor, AnchorStackNo, AnchorAngels) + self:T({From, Event, To, "GID=" .. GID, "Stack=" .. AnchorStackNo}) + -- TODO + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + if not managedgroup then + self:E(self.lid .. "**** GID "..GID.." Not Registered!") + return self + end + managedgroup.AnchorStackNo = AnchorStackNo + managedgroup.AnchorStackAngels = AnchorAngels + managedgroup.Blocked = false + local isPlayer = managedgroup.IsPlayer + local isAI = managedgroup.IsAI + local Group = managedgroup.Group + local CallSign = managedgroup.CallSign or "Ghost 1" + --local AnchorName = Anchor.StationZone:GetName() or "unknown" + local AnchorName = Anchor.StationName or "unknown" + local AnchorCoordTxt = Anchor.StationZoneCoordinateText or "unknown" + local Angels = AnchorAngels or 25 + local AnchorSpeed = self.CapSpeedBase or 270 + local AuftragsNr = managedgroup.CurrentAuftrag + + local textTTS = string.format("%s. %s. Station at %s at angels %d doing %d knots.",CallSign,self.callsigntxt,AnchorName,Angels,AnchorSpeed) + local ROEROT = self.AwacsROE..", "..self.AwacsROT + local textScreen = string.format("%s. %s.\nStation at %s\nAngels %d\nSpeed %d knots\nCoord %s\nROE %s.",CallSign,self.callsigntxt,AnchorName,Angels,AnchorSpeed,AnchorCoordTxt,ROEROT) + local TextTasking = string.format("Station at %s\nAngels %d\nSpeed %d knots\nCoord %s\nROE %s",AnchorName,Angels,AnchorSpeed,AnchorCoordTxt,ROEROT) + + self:_NewRadioEntry(textTTS,textScreen,GID,isPlayer,isPlayer,true,false) + + managedgroup.CurrentTask = self:_CreateTaskForGroup(GID,AWACS.TaskDescription.ANCHOR,TextTasking,Anchor.StationZone) + + -- if it's a Alert5, we want to push CAP instead + if isAI then + local auftrag = managedgroup.FlightGroup:GetMissionCurrent() -- Ops.Auftrag#AUFTRAG + if auftrag then + local auftragtype = auftrag:GetType() + if auftragtype == AUFTRAG.Type.ALERT5 then + -- all correct + local capauftrag = AUFTRAG:NewCAP(Anchor.StationZone,Angels*1000,AnchorSpeed,Anchor.StationZone:GetCoordinate(),0,15,{}) + capauftrag:SetTime(nil,((self.CAPTimeOnStation*3600)+(15*60))) + capauftrag:AddAsset(managedgroup.FlightGroup) + self.CatchAllMissions[#self.CatchAllMissions+1] = capauftrag + --local AirWing = managedgroup.FlightGroup:GetAirWing() + managedgroup.FlightGroup:AddMission(capauftrag) + auftrag:Cancel() + --AirWing:AddMission(capauftrag) + else + self:E("**** AssignedAnchor but Auftrag NOT ALERT5!") + end + else + self:E("**** AssignedAnchor but NO Auftrag!") + end + end + + self.ManagedGrps[GID] = managedgroup + + return self +end + +--- [Internal] onafterNewCluster +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.Intelligence#INTEL.Cluster Cluster +-- @return #AWACS self +function AWACS:onafterNewCluster(From,Event,To,Cluster) + self:T({From, Event, To, Cluster.index}) + + self.CID = self.CID + 1 + self.Countactcounter = self.Countactcounter + 1 + + local ContactTable = Cluster.Contacts or {} + + local function GetFirstAliveContact(table) + for _,_contact in pairs (table) do + local contact = _contact -- Ops.Intelligence#INTEL.Contact + if contact and contact.group and contact.group:IsAlive() then + return contact + end + end + return nil + end + + local Contact = GetFirstAliveContact(ContactTable) -- Ops.Intelligence#INTEL.Contact + + if not Contact then return self end + + local targetset = SET_GROUP:New() + -- SET for TARGET + for _,_grp in pairs(ContactTable) do + local grp = _grp -- Ops.Intelligence#INTEL.Contact + targetset:AddGroup(grp.group, true) + end + local managedcontact = {} -- #AWACS.ManagedContact + managedcontact.CID = self.CID + managedcontact.Contact = Contact + managedcontact.Cluster = Cluster + -- TODO set as per tech / engagement / alarm level age... + managedcontact.IFF = AWACS.IFF.BOGEY -- no IFF yet + managedcontact.Target = TARGET:New(targetset) + managedcontact.LinkedGroup = 0 + managedcontact.LinkedTask = 0 + managedcontact.Status = AWACS.TaskStatus.IDLE + local phoneid = math.fmod(self.Countactcounter,27) + if phoneid == 0 then phoneid = 1 end + managedcontact.TargetGroupNaming = AWACS.Phonetic[phoneid] + managedcontact.ReportingName = Contact.group:GetNatoReportingName() -- e.g. Foxbat. Bogey if unknown + managedcontact.TACCallDone = false + managedcontact.MeldCallDone = false + managedcontact.EngagementTag = "" + + local IsPopup = false + -- is this a pop-up group? i.e. appeared inside AO + if self.OpsZone:IsVec2InZone(Contact.position:GetVec2()) then + IsPopup = true + end + + -- let's see if we can inject some info into Contact + Contact.CID = managedcontact.CID + Contact.TargetGroupNaming = managedcontact.TargetGroupNaming + Cluster.CID = managedcontact.CID + Cluster.TargetGroupNaming = managedcontact.TargetGroupNaming + + self.Contacts:Push(managedcontact,self.CID) + + -- only announce if in right distance to HVT/AIC or in ControlZone or in BorderZone + local ContactCoordinate = Contact.position:GetVec2() + local incontrolzone = self.ControlZone:IsVec2InZone(ContactCoordinate) + + -- distance check to HVT + local distance = 1000000 + if not self.GCI then + distance = Contact.position:Get2DDistance(self.OrbitZone:GetCoordinate()) + end + + local inborderzone = false + if self.BorderZone then + inborderzone = self.BorderZone:IsVec2InZone(ContactCoordinate) + end + + if incontrolzone or inborderzone or (distance <= UTILS.NMToMeters(55)) or IsPopup then + self:_AnnounceContact(managedcontact,true,nil,false,managedcontact.TargetGroupNaming,IsPopup,managedcontact.ReportingName) + end + + return self +end + +--- [Internal] onafterNewContact +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.Intelligence#INTEL.Contact Contact +-- @return #AWACS self +function AWACS:onafterNewContact(From,Event,To,Contact) + self:T({From, Event, To, Contact}) + local tdist = self.ThreatDistance -- NM + -- is any plane near-by? + for _gid,_mgroup in pairs(self.ManagedGrps) do + local managedgroup = _mgroup -- #AWACS.ManagedGroup + local group = managedgroup.Group + if group and group:IsAlive() then + -- contact distance + local cpos = Contact.position or Contact.group:GetCoordinate() -- Core.Point#COORDINATE + local mpos = group:GetCoordinate() + local dist = cpos:Get2DDistance(mpos) -- meter + dist = UTILS.Round(UTILS.MetersToNM(dist),0) + if dist <= tdist then + -- threat call + self:_ThreatRangeCall(_gid,Contact) + end + end + end + return self +end + +--- [Internal] onafterLostContact +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.Intelligence#INTEL.Contact Contact +-- @return #AWACS self +function AWACS:onafterLostContact(From,Event,To,Contact) + self:T({From, Event, To, Contact}) + --self:_CleanUpContacts() + return self +end + +--- [Internal] onafterLostCluster +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.Intelligence#INTEL.Cluster Cluster +-- @param Ops.Auftrag#AUFTRAG Mission +-- @return #AWACS self +function AWACS:onafterLostCluster(From,Event,To,Cluster,Mission) + self:T({From, Event, To}) + --self:_CleanUpContacts() + return self +end + +--- [Internal] onafterCheckRadioQueue +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AWACS self +function AWACS:onafterCheckRadioQueue(From,Event,To) + self:T({From, Event, To}) + -- do we have messages queued? + + local nextcall = 10 + if (self.RadioQueue:IsNotEmpty() or self.PrioRadioQueue:IsNotEmpty()) then + + local RadioEntry = nil + + if self.PrioRadioQueue:IsNotEmpty() then + RadioEntry = self.PrioRadioQueue:Pull() -- #AWACS.RadioEntry + else + RadioEntry = self.RadioQueue:Pull() -- #AWACS.RadioEntry + end + self:T({RadioEntry}) + + if self.clientset:CountAlive() == 0 then + self:I(self.lid.."No player connected.") + self:__CheckRadioQueue(-5) + return self + end + + if not RadioEntry.FromAI then + -- AI AWACS Speaking + if self.PathToGoogleKey then + local gtext = RadioEntry.TextTTS + gtext = string.format("%s",gtext) + self.AwacsFG:RadioTransmission(gtext,1,false) + else + self.AwacsFG:RadioTransmission(RadioEntry.TextTTS,1,false) + end + self:T(RadioEntry.TextTTS) + else + -- CAP AI speaking + if RadioEntry.GroupID and RadioEntry.GroupID ~= 0 then + local managedgroup = self.ManagedGrps[RadioEntry.GroupID] -- #AWACS.ManagedGroup + if managedgroup and managedgroup.FlightGroup and managedgroup.FlightGroup:IsAlive() then + if self.PathToGoogleKey then + local gtext = RadioEntry.TextTTS + gtext = string.format("%s",gtext) + managedgroup.FlightGroup:RadioTransmission(gtext,1,false) + else + managedgroup.FlightGroup:RadioTransmission(RadioEntry.TextTTS,1,false) + end + self:T(RadioEntry.TextTTS) + end + end + end + + if RadioEntry.Duration then nextcall = RadioEntry.Duration end + + if RadioEntry.ToScreen and RadioEntry.TextScreen and (not self.SuppressScreenOutput) then + if RadioEntry.GroupID and RadioEntry.GroupID ~= 0 then + local managedgroup = self.ManagedGrps[RadioEntry.GroupID] -- #AWACS.ManagedGroup + if managedgroup and managedgroup.Group and managedgroup.Group:IsAlive() then + MESSAGE:New(RadioEntry.TextScreen,20,"AWACS"):ToGroup(managedgroup.Group) + self:T(RadioEntry.TextScreen) + end + else + MESSAGE:New(RadioEntry.TextScreen,20,"AWACS"):ToCoalition(self.coalition) + end + end + end + + if self:Is("Running") then + -- exit if stopped + if self.PathToGoogleKey then + nextcall = nextcall + self.GoogleTTSPadding + else + nextcall = nextcall + self.WindowsTTSPadding + end + self:__CheckRadioQueue(-nextcall) + end + return self +end + +--- [Internal] onafterEscortShiftChange +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AWACS self +function AWACS:onafterEscortShiftChange(From,Event,To) + self:T({From, Event, To}) + -- request new Escorts, check if AWACS-FG still alive first! + if self.AwacsFG and self.ShiftChangeEscortsFlag and not self.ShiftChangeEscortsRequested then + local awacs = self.AwacsFG:GetGroup() -- Wrapper.Group#GROUP + if awacs and awacs:IsAlive() then + -- ok we're good to re-request + self.ShiftChangeEscortsRequested = true + self.EscortsTimeStamp = timer.getTime() + self:_StartEscorts(true) + else + -- should not happen + self:E("**** AWACS group dead at onafterEscortShiftChange!") + end + end + return self +end + +--- [Internal] onafterAwacsShiftChange +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AWACS self +function AWACS:onafterAwacsShiftChange(From,Event,To) + self:T({From, Event, To}) + -- request new AWACS + if self.AwacsFG and self.ShiftChangeAwacsFlag and not self.ShiftChangeAwacsRequested then + + -- ok we're good to re-request + self.ShiftChangeAwacsRequested = true + self.AwacsTimeStamp = timer.getTime() + + -- set up the AWACS and let it orbit + local AwacsAW = self.AirWing -- Ops.AirWing#AIRWING + local mission = AUFTRAG:NewORBIT_RACETRACK(self.OrbitZone:GetCoordinate(),self.AwacsAngels*1000,self.Speed,self.Heading,self.Leg) + self.CatchAllMissions[#self.CatchAllMissions+1] = mission + local timeonstation = (self.AwacsTimeOnStation + self.ShiftChangeTime) * 3600 + mission:SetTime(nil,timeonstation) + + AwacsAW:AddMission(mission) + + self.AwacsMissionReplacement = mission + + end + return self +end + +--- On after "FlightOnMission". +-- @param #AWACS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup on mission. +-- @param Ops.Auftrag#AUFTRAG Mission The requested mission. +-- @return #AWACS self +function AWACS:onafterFlightOnMission(From, Event, To, FlightGroup, Mission) + self:T({From, Event, To}) + -- coming back from AW, set up the flight + self:T("FlightGroup " .. FlightGroup:GetName() .. " Mission " .. Mission:GetName() .. " Type "..Mission:GetType()) + self.CatchAllFGs[#self.CatchAllFGs+1] = FlightGroup + if not self:Is("Stopped") then + if not self.AwacsReady or self.ShiftChangeAwacsFlag or self.ShiftChangeEscortsFlag then + self:_StartSettings(FlightGroup,Mission) + elseif Mission and (Mission:GetType() == AUFTRAG.Type.CAP or Mission:GetType() == AUFTRAG.Type.ALERT5 or Mission:GetType() == AUFTRAG.Type.ORBIT) then + if not self.FlightGroups:HasUniqueID(FlightGroup:GetName()) then + self:T("Pushing FG " .. FlightGroup:GetName() .. " to stack!") + self.FlightGroups:Push(FlightGroup,FlightGroup:GetName()) + end + end + end + return self +end + +--- On after "ReAnchor". +-- @param #AWACS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number GID Group ID to check and re-anchor if possible +-- @return #AWACS self +function AWACS:onafterReAnchor(From, Event, To, GID) + self:T({From, Event, To, GID}) + -- get managedgroup + -- check AI FG state + -- check weapon state + -- check fuel state + -- vector back to anchor or RTB + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + if managedgroup then + if managedgroup.IsAI then + -- AI will now have a new CAP AUFTRAG and head back to the stack anyway + local AIFG = managedgroup.FlightGroup -- Ops.FlightGroup#FLIGHTGROUP + if AIFG and AIFG:IsAlive() then + -- check state + if AIFG:IsFuelLow() or AIFG:IsOutOfMissiles() or AIFG:IsOutOfAmmo() then + local destbase = AIFG.homebase + if not destbase then destbase = self.Airbase end + -- RTB call needs an AIRBASE + AIFG:RTB(destbase) + -- Check out + self:_CheckOut(AIFG:GetGroup(),GID) + self.AIRequested = self.AIRequested - 1 + else + -- re-establish anchor task + -- get anchor zone data + local Anchor = self.AnchorStacks:ReadByPointer(managedgroup.AnchorStackNo) -- #AWACS.AnchorData + local StationZone = Anchor.StationZone -- Core.Zone#ZONE + managedgroup.CurrentTask = self:_CreateTaskForGroup(GID,AWACS.TaskDescription.ANCHOR,"Re-Station AI",StationZone) + managedgroup.HasAssignedTask = true + local mission = AIFG:GetMissionCurrent() -- Ops.Auftrag#AUFTRAG + if mission then + managedgroup.CurrentAuftrag = mission.auftragsnummer or 0 + else + managedgroup.CurrentAuftrag = 0 + end + managedgroup.ContactCID = 0 + self.ManagedGrps[GID] = managedgroup + self:_MessageVector(GID," to Station",Anchor.StationZoneCoordinate,managedgroup.AnchorStackAngels) + end + else + -- lost group, remove from known groups, declare vanished + -- AI - remove from known FGs! -- done in status loop + -- ALL remove from managedgrps + + -- message loss + local savedcallsign = managedgroup.CallSign + --vanished/friendly flight faded/lost contact with C/S/CSAR Scramble + -- Magic, RIGHTGUARD, RIGHTGUARD, Dodge 41, Bullseye X/Y + local textoptions = { + [1] = "Lost friendly flight", + [2] = "Vanished friendly flight", + [3] = "Faded friendly contact", + [4] = "Lost contact with", + } + + -- DONE - need to save last known coordinate + + if managedgroup.LastKnownPosition then + local lastknown = UTILS.DeepCopy(managedgroup.LastKnownPosition) + local faded = textoptions[math.random(1,4)] + local text = string.format("All stations. %s. %s %s.",self.callsigntxt, faded, savedcallsign) + local textScreen = string.format("All stations, %s. %s %s.", self.callsigntxt, faded, savedcallsign) + + local brtext = self:_ToStringBULLS(lastknown) + local brtexttts = self:_ToStringBULLS(lastknown,false,true) + --if self.PathToGoogleKey then + --brtexttts = self:_ToStringBULLS(lastknown,true) + --end + text = text .. " "..brtexttts.." miles." + textScreen = textScreen .. " "..brtext.." miles." + + self:_NewRadioEntry(text,textScreen,0,false,self.debug,true,false,true) + end + self.ManagedGrps[GID] = nil + end + elseif managedgroup.IsPlayer then + -- TODO + local PLFG = managedgroup.Group -- Wrapper.Group#GROUP + if PLFG and PLFG:IsAlive() then + -- re-establish anchor task + -- get anchor zone data + local Anchor = self.AnchorStacks:ReadByPointer(managedgroup.AnchorStackNo) -- #AWACS.AnchorData + local AnchorName = Anchor.StationName or "unknown" + local AnchorCoordTxt = Anchor.StationZoneCoordinateText or "unknown" + local Angels = managedgroup.AnchorStackAngels or 25 + local AnchorSpeed = self.CapSpeedBase or 270 + local StationZone = Anchor.StationZone -- Core.Zone#ZONE + local ROEROT = self.AwacsROE.." "..self.AwacsROT + local TextTasking = string.format("Station at %s\nAngels %d\nSpeed %d knots\nCoord %s\nROE %s",AnchorName,Angels,AnchorSpeed,AnchorCoordTxt,ROEROT) + managedgroup.CurrentTask = self:_CreateTaskForGroup(GID,AWACS.TaskDescription.ANCHOR,TextTasking,StationZone) + managedgroup.HasAssignedTask = true + managedgroup.ContactCID = 0 + self.ManagedGrps[GID] = managedgroup + self:_MessageVector(GID," to Station",Anchor.StationZoneCoordinate,managedgroup.AnchorStackAngels) + else + -- lost group, remove from known groups, declare vanished + -- ALL remove from managedgrps + -- message loss + local savedcallsign = managedgroup.CallSign + --vanished/friendly flight faded/lost contact with C/S/CSAR Scramble + -- Magic, RIGHTGUARD, RIGHTGUARD, Dodge 41, Bullseye X/Y + local textoptions = { + [1] = "Lost friendly flight", + [2] = "Vanished friendly flight", + [3] = "Faded friendly contact", + [4] = "Lost contact with", + } + + -- DONE - need to save last known coordinate + local faded = textoptions[math.random(1,4)] + local text = string.format("All stations. %s. %s %s.",self.callsigntxt, faded, savedcallsign) + local textScreen = string.format("All stations, %s. %s %s.", self.callsigntxt, faded, savedcallsign) + if managedgroup.LastKnownPosition then + local lastknown = UTILS.DeepCopy(managedgroup.LastKnownPosition) + local brtext = self:_ToStringBULLS(lastknown) + local brtexttts = self:_ToStringBULLS(lastknown,false,true) + --if self.PathToGoogleKey then + --brtexttts = self:_ToStringBULLS(lastknown,true) + --end + text = text .. " "..brtexttts.." miles." + textScreen = textScreen .. " "..brtext.." miles." + + self:_NewRadioEntry(text,textScreen,0,false,self.debug,true,false,true) + end + self.ManagedGrps[GID] = nil + end + end + end +end + +end -- end do +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- END AWACS +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- **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 + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- **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#MSRSQUEUE msrsqueue Queue for TTS transmissions using MSRS class. +-- @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.1" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- 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() + + -- Init msrs queue. + self.msrsqueue=MSRSQUEUE:New(self.alias) + + -- 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 + ]] + + local Tqueue=self.msrsqueue:CalcTransmisstionDuration() + + if Tqueue>0 then + -- Debug info. + local text=string.format("Still got %d messages in the radio queue. Will call status again in %.1f sec", #self.msrsqueue, Tqueue) + self:I(self.lid..text) + + -- Call status again in dt seconds. + self:__StatusUpdate(-Tqueue) + + -- Deny transition. + return false + 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 + + -- Runway. + local runway=self:GetActiveRunwayText() + + -- Message text. + local text=string.format("%s, affirmative, runway %s. Confirm approach!", callsign, runway) + + -- 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:T(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 + + -- Callsign. + local callsign=self:_GetCallsignName(flight) + + -- Radio text. + local text=string.format("%s, slow down, you are taxiing too fast!", callsign) + + -- 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) + + -- "Subtitle". + local subgroups=nil + if Flight and not Flight.isAI then + local playerData=Flight:_GetPlayerData() + if playerData.subtitles then + subgroups=subgroups or {} + table.insert(subgroups, Flight.group) + end + end + + -- New transmission. + local transmission=self.msrsqueue:NewTransmission(text, nil, self.msrsTower, nil, 1, subgroups, Text) + + -- 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) + + -- MSRS instance to use. + local msrs=self.msrsPilot + + if Flight.useSRS and Flight.msrs then + + -- Pilot radio call using settings of the FLIGHTGROUP. We just overwrite the frequency. + msrs=Flight.msrs + + end + + -- "Subtitle". + local subgroups=nil + if Flight and not Flight.isAI then + local playerData=Flight:_GetPlayerData() + if playerData.subtitles then + subgroups=subgroups or {} + table.insert(subgroups, Flight.group) + end + end + + -- Add transmission to msrsqueue. + self.msrsqueue:NewTransmission(text, nil, msrs, nil, 1, subgroups, Text, nil, self.frequency, self.modulation) + + 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 Original 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 + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- **Ops** - PlayerTask (mission) for Players. +-- +-- ## Main Features: +-- +-- * Simplifies defining and executing Player tasks +-- * FSM events when a mission is done, successful or failed +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/). +-- +-- === +-- +-- ### Author: **Applevangelist** +-- +-- === +-- @module Ops.PlayerTask +-- @image OPS_PlayerTask.png + + +--- PLAYERTASK class. +-- @type PLAYERTASK +-- @field #string ClassName Name of the class. +-- @field #boolean verbose Switch verbosity. +-- @field #string lid Class id string for output to DCS log file. +-- @field #number PlayerTaskNr (Globally unique) Number of the task. +-- @field Ops.Auftrag#AUFTRAG.Type Type The type of the task +-- @field Ops.Target#TARGET Target The target for this Task +-- @field Utilities.FiFo#FIFO Clients FiFo of Wrapper.Client#CLIENT planes executing this task +-- @field #boolean Repeat +-- @field #number repeats +-- @field #number RepeatNo +-- @field Wrapper.Marker#MARKER TargetMarker +-- @field #number SmokeColor +-- @field #number FlareColor +-- @field #table conditionSuccess = {}, +-- @field #table conditionFailure = {}, +-- @field Ops.PlayerTask#PLAYERTASKCONTROLLER TaskController +-- +-- @extends Core.Fsm#FSM + +--- Global PlayerTaskNr counter +_PlayerTaskNr = 0 + +--- +-- @field #PLAYERTASK +PLAYERTASK = { + ClassName = "PLAYERTASK", + verbose = true, + lid = nil, + PlayerTaskNr = nil, + Type = nil, + Target = nil, + Clients = nil, + Repeat = false, + repeats = 0, + RepeatNo = 1, + TargetMarker = nil, + SmokeColor = nil, + FlareColor = nil, + conditionSuccess = {}, + conditionFailure = {}, + TaskController = nil, + } + +--- PLAYERTASK class version. +-- @field #string version +PLAYERTASK.version="0.0.7" + +--- Generic task condition. +-- @type PLAYERTASK.Condition +-- @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. + +--- Constructor +-- @param #PLAYERTASK self +-- @param Ops.Auftrag#AUFTRAG.Type Type Type of this task +-- @param Ops.Target#TARGET Target Target for this task +-- @param #boolean Repeat Repeat this task if true (default = false) +-- @param #number Times Repeat on failure this many times if Repeat is true (default = 1) +-- @return #PLAYERTASK self +function PLAYERTASK:New(Type, Target, Repeat, Times) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #PLAYERTASK + + self.Type = Type + + self.Repeat = false + self.repeats = 0 + self.RepeatNo = 1 + self.Clients = FIFO:New() -- Utilities.FiFo#FIFO + self.TargetMarker = nil -- Wrapper.Marker#MARKER + self.SmokeColor = SMOKECOLOR.Red + self.conditionSuccess = {} + self.conditionFailure = {} + self.TaskController = nil -- Ops.PlayerTask#PLAYERTASKCONTROLLER + + if Repeat then + self.Repeat = true + self.RepeatNo = Times or 1 + end + + _PlayerTaskNr = _PlayerTaskNr + 1 + + self.PlayerTaskNr = _PlayerTaskNr + + self.lid=string.format("PlayerTask #%d %s | ", self.PlayerTaskNr, tostring(self.Type)) + + if Target and Target.ClassName and Target.ClassName == "TARGET" then + self.Target = Target + elseif Target and Target.ClassName then + self.Target = TARGET:New(Target) + else + self:E(self.lid.."*** NO VALID TARGET!") + return self + end + + self:I(self.lid.."Created.") + + -- FMS start state is PLANNED. + self:SetStartState("Planned") + + -- PLANNED --> REQUESTED --> EXECUTING --> DONE + self:AddTransition("*", "Planned", "Planned") -- Task is in planning stage. + self:AddTransition("*", "Requested", "Requested") -- Task clients have been requested to join. + self:AddTransition("*", "ClientAdded", "*") -- Client has been added to the task + self:AddTransition("*", "ClientRemoved", "*") -- Client has been added to the task + self:AddTransition("*", "Executing", "Executing") -- First client is executing the Task. + self:AddTransition("*", "Done", "Done") -- All clients have reported that Task is done. + self:AddTransition("*", "Cancel", "Done") -- Command to cancel the Task. + self:AddTransition("*", "Success", "Done") + self:AddTransition("*", "ClientAborted", "*") + self:AddTransition("*", "Failed", "*") -- Done or repeat --> PLANNED + self:AddTransition("*", "Status", "*") + self:AddTransition("*", "Stop", "Stopped") + + self:__Status(-5) + return self +end + +--- [Internal] Add a PLAYERTASKCONTROLLER for this task +-- @param #PLAYERTASK self +-- @param Ops.PlayerTask#PLAYERTASKCONTROLLER Controller +-- @return #PLAYERTASK self +function PLAYERTASK:_SetController(Controller) + self:I(self.lid.."_SetController") + self.TaskController = Controller + return self +end + +--- [User] Check if task is done +-- @param #PLAYERTASK self +-- @return #boolean done +function PLAYERTASK:IsDone() + self:I(self.lid.."IsDone?") + local IsDone = false + local state = self:GetState() + if state == "Done" or state == "Stopped" then + IsDone = true + end + return IsDone +end + +--- [User] Get clients assigned list as table +-- @param #PLAYERTASK self +-- @return #table clients +function PLAYERTASK:GetClients() + self:I(self.lid.."GetClients?") + local clientlist = self.Clients:GetIDStackSorted() or {} + return clientlist +end + +--- [User] Check if a player name is assigned to this task +-- @param #PLAYERTASK self +-- @param #string Name +-- @return #boolean HasName +function PLAYERTASK:HasPlayerName(Name) + self:I(self.lid.."HasPlayerName?") + return self.Clients:HasUniqueID(Name) +end + +--- [User] Add a client to this task +-- @param #PLAYERTASK self +-- @param Wrapper.Client#CLIENT Client +-- @return #PLAYERTASK self +function PLAYERTASK:AddClient(Client) + self:I(self.lid.."AddClient") + local name = Client:GetPlayerName() + if not self.Clients:HasUniqueID(name) then + self.Clients:Push(Client,name) + self:__ClientAdded(-2,Client) + end + return self +end + +--- [User] Remove a client from this task +-- @param #PLAYERTASK self +-- @param Wrapper.Client#CLIENT Client +-- @return #PLAYERTASK self +function PLAYERTASK:RemoveClient(Client) + self:I(self.lid.."RemoveClient") + local name = Client:GetPlayerName() + if self.Clients:HasUniqueID(name) then + self.Clients:PullByID(name) + if self.verbose then + self.Clients:Flush() + end + self:__ClientRemoved(-2,Client) + if self.Clients:Count() == 0 then + self:__Failed(-1) + end + end + return self +end + +--- [User] Client has aborted task this task +-- @param #PLAYERTASK self +-- @param Wrapper.Client#CLIENT Client (optional) +-- @return #PLAYERTASK self +function PLAYERTASK:ClientAbort(Client) + self:I(self.lid.."ClientAbort") + if Client and Client:IsAlive() then + self:RemoveClient(Client) + self:__ClientAborted(-1,Client) + return self + else + -- no client given, abort whole task if no one else is assigned + if self.Clients:Count() == 0 then + -- return to planned state if repeat + self:__Failed(-1) + end + end + return self +end + +--- [User] Create target mark on F10 map +-- @param #PLAYERTASK self +-- @return #PLAYERTASK self +function PLAYERTASK:MarkTargetOnF10Map() + self:I(self.lid.."MarkTargetOnF10Map") + if self.Target then + local coordinate = self.Target:GetCoordinate() + if coordinate then + if self.TargetMarker then + -- Marker exists, delete one first + self.TargetMarker:Remove() + end + self.TargetMarker = MARKER:New(coordinate,"Target of "..self.lid) + self.TargetMarker:ReadOnly() + self.TargetMarker:ToAll() + end + end + return self +end + +--- [User] Smoke Target +-- @param #PLAYERTASK self +-- @param #number Color, defaults to SMOKECOLOR.Red +-- @return #PLAYERTASK self +function PLAYERTASK:SmokeTarget(Color) + self:I(self.lid.."SmokeTarget") + local color = Color or SMOKECOLOR.Red + if self.Target then + local coordinate = self.Target:GetCoordinate() + if coordinate then + coordinate:Smoke(color) + end + end + return self +end + +--- [User] Flare Target +-- @param #PLAYERTASK self +-- @param #number Color, defaults to FLARECOLOR.Red +-- @return #PLAYERTASK self +function PLAYERTASK:FlareTarget(Color) + self:I(self.lid.."SmokeTarget") + local color = Color or FLARECOLOR.Red + if self.Target then + local coordinate = self.Target:GetCoordinate() + if coordinate then + coordinate:Flare(color,0) + end + end + return self +end + +-- success / failure function addion courtesy @FunkyFranky. + +--- [User] Add success condition. +-- @param #PLAYERTASK self +-- @param #function ConditionFunction If this function returns `true`, the mission is cancelled. +-- @param ... Condition function arguments if any. +-- @return #PLAYERTASK self +function PLAYERTASK:AddConditionSuccess(ConditionFunction, ...) + + local condition={} --#PLAYERTASK.Condition + + condition.func=ConditionFunction + condition.arg={} + if arg then + condition.arg=arg + end + + table.insert(self.conditionSuccess, condition) + + return self +end + +--- [User] Add failure condition. +-- @param #PLAYERTASK self +-- @param #function ConditionFunction If this function returns `true`, the task is cancelled. +-- @param ... Condition function arguments if any. +-- @return #PLAYERTASK self +function PLAYERTASK:AddConditionFailure(ConditionFunction, ...) + + local condition={} --#PLAYERTASK.Condition + + condition.func=ConditionFunction + condition.arg={} + if arg then + condition.arg=arg + end + + table.insert(self.conditionFailure, condition) + + return self +end + +--- [Internal] Check if any of the given conditions is true. +-- @param #PLAYERTASK self +-- @param #table Conditions Table of conditions. +-- @return #boolean If true, at least one condition is true. +function PLAYERTASK:_EvalConditionsAny(Conditions) + + -- Any stop condition must be true. + for _,_condition in pairs(Conditions or {}) do + local condition=_condition --#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 + +--- [Internal] On after status call +-- @param #PLAYERTASK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASK self +function PLAYERTASK:onafterStatus(From, Event, To) + self:I({From, Event, To}) + self:I(self.lid.."onafterStatus") + + local status = self:GetState() + + -- Check Target status + local targetdead = false + if self.Target:IsDead() or self.Target:IsDestroyed() then + targetdead = true + self:__Success(-2) + status = "Success" + return self + end + + if status == "Executing" then + -- Check Clients alive + local clientsalive = false + local ClientTable = self.Clients:GetDataTable() + for _,_client in pairs(ClientTable) do + local client = _client -- Wrapper.Client#CLIENT + if client:IsAlive() then + clientsalive=true -- one or more clients alive + end + end + + -- Failed? + if status == "Executing" and (not clientsalive) and (not targetdead) then + self:__Failed(-2) + status = "Failed" + end + + -- Any success condition true? + local successCondition=self:_EvalConditionsAny(self.conditionSuccess) + + -- Any failure condition true? + local failureCondition=self:_EvalConditionsAny(self.conditionFailure) + + if failureCondition then + self:__Failed(-2) + status = "Failed" + elseif successCondition then + self:__Success(-2) + status = "Success" + end + + if self.verbose then + self:I(self.lid.."Target dead: "..tostring(targetdead).." | Clients alive: " .. tostring(clientsalive)) + end + end + + -- Continue if we are not done + if status ~= "Done" then + self:__Status(-20) + else + self:__Stop(-1) + end + + return self +end + + +--- [Internal] On after planned call +-- @param #PLAYERTASK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASK self +function PLAYERTASK:onafterPlanned(From, Event, To) + self:I({From, Event, To}) + return self +end + +--- [Internal] On after requested call +-- @param #PLAYERTASK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASK self +function PLAYERTASK:onafterRequested(From, Event, To) + self:I({From, Event, To}) + return self +end + +--- [Internal] On after executing call +-- @param #PLAYERTASK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASK self +function PLAYERTASK:onafterExecuting(From, Event, To) + self:I({From, Event, To}) + return self +end + +--- [Internal] On after status call +-- @param #PLAYERTASK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASK self +function PLAYERTASK:onafterStop(From, Event, To) + self:I({From, Event, To}) + return self +end + +--- [Internal] On after client added call +-- @param #PLAYERTASK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Wrapper.Client#CLIENT Client +-- @return #PLAYERTASK self +function PLAYERTASK:onafterClientAdded(From, Event, To, Client) + self:I({From, Event, To}) + if Client then + local text = string.format("Player %s joined task %d!",Client:GetPlayerName() or "Generic",self.PlayerTaskNr) + self:I(self.lid..text) + end + return self +end + +--- [Internal] On after done call +-- @param #PLAYERTASK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASK self +function PLAYERTASK:onafterDone(From, Event, To) + self:I({From, Event, To}) + if self.TaskController then + self.TaskController:__TaskDone(-1,self) + end + self:__Stop(-1) + return self +end + +--- [Internal] On after cancel call +-- @param #PLAYERTASK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASK self +function PLAYERTASK:onafterCancel(From, Event, To) + self:I({From, Event, To}) + if self.TaskController then + self.TaskController:__TaskCancelled(-1,self) + end + self:__Done(-1) + return self +end + +--- [Internal] On after success call +-- @param #PLAYERTASK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASK self +function PLAYERTASK:onafterSuccess(From, Event, To) + self:I({From, Event, To}) + if self.TaskController then + self.TaskController:__TaskSuccess(-1,self) + end + if self.TargetMarker then + self.TargetMarker:Remove() + end + self:__Done(-1) + return self +end + +--- [Internal] On after failed call +-- @param #PLAYERTASK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASK self +function PLAYERTASK:onafterFailed(From, Event, To) + self:I({From, Event, To}) + self.repeats = self.repeats + 1 + -- repeat on failed? + if self.Repeat and (self.repeats <= self.RepeatNo) then + if self.TaskController then + self.TaskController:__TaskRepeatOnFailed(-1,self) + end + self:__Planned(-1) + return self + else + if self.TargetMarker then + self.TargetMarker:Remove() + end + if self.TaskController then + self.TaskController:__TaskFailed(-1,self) + end + self:__Done(-1) + end + return self +end + +------------------------------------------------------------------------------------------------------------------- +-- PLAYERTASKCONTROLLER +-- TODO: PLAYERTASKCONTROLLER +------------------------------------------------------------------------------------------------------------------- + +--- PLAYERTASKCONTROLLER class. +-- @type PLAYERTASKCONTROLLER +-- @field #string ClassName Name of the class. +-- @field #boolean verbose Switch verbosity. +-- @field #string lid Class id string for output to DCS log file. +-- @field Utilities.FiFo#FIFO TargetQueue +-- @field Utilities.FiFo#FIFO TaskQueue +-- @field Utilities.FiFo#FIFO TasksPerPlayer +-- @field Core.Set#SET_CLIENT ClientSet +-- @field #string ClientFilter +-- @field #string Name +-- @field #string Type +-- @field #boolean UseGroupNames +-- @field #table PlayerMenu +-- + + +--- +-- @field #PLAYERTASKCONTROLLER +PLAYERTASKCONTROLLER = { + ClassName = "PLAYERTASKCONTROLLER", + verbose = true, + lid = nil, + TargetQueue = nil, + ClientSet = nil, + UseGroupNames = true, + PlayerMenu = {}, + } + +--- +-- @field Type +PLAYERTASKCONTROLLER.Type = { + A2A = "Air-To-Air", + A2G = "Air-To-Ground", + A2S = "Air-To-Sea", +} + +--- PLAYERTASK class version. +-- @field #string version +PLAYERTASKCONTROLLER.version="0.0.7" + +--- Constructor +-- @param #PLAYERTASKCONTROLLER self +-- @param #string Name Name of this controller +-- @param #number Coalition of this controller, e.g. coalition.side.BLUE +-- @param #string Type Type of the tasks controlled, defaults to PLAYERTASKCONTROLLER.Type.A2G +-- @param #string ClientFilter (optional) Additional prefix filter for the SET_CLIENT +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:New(Name, Coalition, Type, ClientFilter) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #PLAYERTASKCONTROLLER + + self.Name = Name or "CentCom" + self.Coalition = Coalition or coalition.side.BLUE + self.CoalitionName = UTILS.GetCoalitionName(Coalition) + self.Type = Type or PLAYERTASKCONTROLLER.Type.A2G + self.ClientFilter = ClientFilter or "" + + self.TargetQueue = FIFO:New() -- Utilities.FiFo#FIFO + self.TaskQueue = FIFO:New() -- Utilities.FiFo#FIFO + self.TasksPerPlayer = FIFO:New() -- Utilities.FiFo#FIFO + self.PlayerMenu = {} -- #table + + self.repeatonfailed = true + self.repeattimes = 5 + self.UseGroupNames = true + + if ClientFilter then + self.ClientSet = SET_CLIENT:New():FilterCoalitions(string.lower(self.CoalitionName)):FilterActive(true):FilterPrefixes(ClientFilter):FilterStart() + else + self.ClientSet = SET_CLIENT:New():FilterCoalitions(string.lower(self.CoalitionName)):FilterActive(true):FilterStart() + end + + self.lid=string.format("PlayerTaskController %s %s | ", self.Name, tostring(self.Type)) + + -- FSM start state is STOPPED. + self:SetStartState("Stopped") + + self:AddTransition("Stopped", "Start", "Running") + self:AddTransition("*", "Status", "*") + self:AddTransition("*", "TaskAdded", "*") + self:AddTransition("*", "TaskDone", "*") + self:AddTransition("*", "TaskCancelled", "*") + self:AddTransition("*", "TaskSuccess", "*") + self:AddTransition("*", "TaskFailed", "*") + self:AddTransition("*", "TaskRepeatOnFailed", "*") + self:AddTransition("*", "Stop", "Stopped") + + self:__Start(-1) + self:__Status(-2) + + -- Player leaves + self:HandleEvent(EVENTS.PlayerLeaveUnit, self._EventHandler) + self:HandleEvent(EVENTS.Ejection, self._EventHandler) + self:HandleEvent(EVENTS.Crash, self._EventHandler) + self:HandleEvent(EVENTS.PilotDead, self._EventHandler) + + self:I(self.lid.."Started.") + + return self +end + +--- [internal] Event handling +-- @param #PLAYERTASKCONTROLLER self +-- @param Core.Event#EVENTDATA EventData +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_EventHandler(EventData) + self:I(self.lid.."_EventHandler: "..EventData.id) + if EventData.id == EVENTS.PlayerLeaveUnit or EventData.id == EVENTS.Ejection or EventData.id == EVENTS.Crash or EventData.id == EVENTS.PilotDead then + if EventData.IniPlayerName then + self:I(self.lid.."Event for player: "..EventData.IniPlayerName) + if self.PlayerMenu[EventData.IniPlayerName] then + self.PlayerMenu[EventData.IniPlayerName]:Remove() + self.PlayerMenu[EventData.IniPlayerName] = nil + end + local text = "" + if self.TasksPerPlayer:HasUniqueID(EventData.IniPlayerName) then + local task = self.TasksPerPlayer:PullByID(EventData.IniPlayerName) -- Ops.PlayerTask#PLAYERTASK + local Client = _DATABASE:FindClient( EventData.IniPlayerName ) + if Client then + task:RemoveClient(Client) + text = "Task aborted!" + end + else + text = "No active task!" + end + self:I(self.lid..text) + end + end + return self +end + +function PLAYERTASKCONTROLLER:_DummyMenu(group) + self:I(self.lid.."_DummyMenu") + return self +end + +--- [user] Switch usage of target names for menu entries on or off +-- @param #PLAYERTASKCONTROLLER self +-- @param #boolean OnOff If true, set to on (default), if nil or false, set to off +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SwitchUseGroupNames(OnOff) + self:I(self.lid.."SwitchUseGroupNames") + if OnOff then + self.UseGroupNames = true + else + self.UseGroupNames = false + end + return self +end + +--- [Internal] Get task types for the menu +-- @param #PLAYERTASKCONTROLLER self +-- @return #table TaskTypes +function PLAYERTASKCONTROLLER:_GetAvailableTaskTypes() + self:I(self.lid.."_GetAvailableTaskTypes") + local tasktypes = {} + self.TaskQueue:ForEach( + function (Task) + local task = Task -- Ops.PlayerTask#PLAYERTASK + local type = Task.Type + tasktypes[type] = {} + end + ) + return tasktypes +end + +--- [Internal] Get task per type for the menu +-- @param #PLAYERTASKCONTROLLER self +-- @return #table TasksPerTypes +function PLAYERTASKCONTROLLER:_GetTasksPerType() + self:I(self.lid.."_GetTasksPerType") + local tasktypes = self:_GetAvailableTaskTypes() + + self:I({tasktypes}) + + self.TaskQueue:ForEach( + function (Task) + local task = Task -- Ops.PlayerTask#PLAYERTASK + local type = Task.Type + if task:GetState() ~= "Executing" and not task:IsDone() then + table.insert(tasktypes[type],task) + end + end + ) + + return tasktypes +end + +--- [Internal] Check target queue +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_CheckTargetQueue() + self:I(self.lid.."_CheckTargetQueue") + if self.TargetQueue:Count() > 0 then + local object = self.TargetQueue:Pull() + local target = TARGET:New(object) + self:_AddTask(target) + end + return self +end + +--- [Internal] Check task queue +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_CheckTaskQueue() + self:I(self.lid.."_CheckTaskQueue") + if self.TaskQueue:Count() > 0 then + -- remove done tasks + local tasks = self.TaskQueue:GetIDStack() + for _id,_entry in pairs(tasks) do + local data = _entry.data -- Ops.PlayerTask#PLAYERTASK + self:I("Looking at Task: "..data.PlayerTaskNr.." Type: "..data.Type.." State: "..data:GetState()) + if data:GetState() == "Done" or data:GetState() == "Stopped" then + local task = self.TaskQueue:ReadByID(_id) -- Ops.PlayerTask#PLAYERTASK + -- DEBUG: Remove clients from the task + local clientsattask = task.Clients:GetIDStackSorted() + for _,_id in pairs(clientsattask) do + self:I("*****Removing player " .. _id) + self.TasksPerPlayer:PullByID(_id) + end + local task = self.TaskQueue:PullByID(_id) -- Ops.PlayerTask#PLAYERTASK + task = nil + end + end + end + return self +end + +--- [user] Add a target object to the target queue +-- @param #PLAYERTASKCONTROLLER self +-- @param Wrapper.Positionable#POSITIONABLE Target The target GROUP, SET_GROUP, UNIT, SET_UNIT, STATIC, AIRBASE or COORDINATE. +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:AddTarget(Target) + self:I(self.lid.."AddTarget") + self.TargetQueue:Push(Target) + return self +end + +--- [Internal] Add a task to the task queue +-- @param #PLAYERTASKCONTROLLER self +-- @param Ops.Target#TARGET Target +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_AddTask(Target) + self:I(self.lid.."_AddTask") + local cat = Target:GetCategory() + local type = AUFTRAG.Type.CAS + + if cat == TARGET.Category.GROUND then + type = AUFTRAG.Type.CAS + -- TODO: debug BAI, CAS, SEAD + local targetobject = Target:GetObject() -- Wrapper.Positionable#POSITIONABLE + if targetobject:IsInstanceOf("UNIT") then + self:I("SEAD Check UNIT") + if targetobject:HasSEAD() then + type = AUFTRAG.Type.SEAD + end + elseif targetobject:IsInstanceOf("GROUP") then + self:I("SEAD Check GROUP") + local attribute = targetobject:GetAttribute() + if attribute == GROUP.Attribute.GROUND_SAM or attribute == GROUP.Attribute.GROUND_AAA then + type = AUFTRAG.Type.SEAD + end + elseif targetobject:IsInstanceOf("SET_GROUP") then + self:I("SEAD Check SET_GROUP") + targetobject:ForEachGroup( + function (group) + local attribute = group:GetAttribute() + if attribute == GROUP.Attribute.GROUND_SAM or attribute == GROUP.Attribute.GROUND_AAA then + type = AUFTRAG.Type.SEAD + end + end + ) + elseif targetobject:IsInstanceOf("SET_UNIT") then + self:I("SEAD Check SET_UNIT") + targetobject:ForEachUnit( + function (unit) + if unit:HasSEAD() then + type = AUFTRAG.Type.SEAD + end + end + ) + end + -- if there are no friendlies nearby ~2km and task isn't SEAD, then it's BAI + local targetcoord = Target:GetCoordinate() + local targetvec2 = targetcoord:GetVec2() + local targetzone = ZONE_RADIUS:New(self.Name,targetvec2,2000) + local coalition = targetobject:GetCoalitionName() or "Blue" + coalition = string.lower(coalition) + self:I("Target coalition is "..tostring(coalition)) + local filtercoalition = "blue" + if coalition == "blue" then filtercoalition = "red" end + local friendlyset = SET_GROUP:New():FilterCategoryGround():FilterCoalitions(filtercoalition):FilterZones({targetzone}):FilterOnce() + if friendlyset:Count() == 0 and type ~= AUFTRAG.Type.SEAD then + type = AUFTRAG.Type.BAI + end + elseif cat == TARGET.Category.NAVAL then + type = AUFTRAG.Type.ANTISHIP + elseif cat == TARGET.Category.AIRCRAFT then + type = AUFTRAG.Type.INTERCEPT + elseif cat == TARGET.Category.AIRBASE then + --TODO: Define Success Criteria, AB hit? Runway blocked, how to determine? change of coalition? + type = AUFTRAG.Type.BOMBRUNWAY + elseif cat == TARGET.Category.COORDINATE or cat == TARGET.Category.ZONE then + --TODO: Define Success Criteria, void of enemies? + type = AUFTRAG.Type.BOMBING + end + + local task = PLAYERTASK:New(type,Target,self.repeatonfailed,self.repeattimes) + task:_SetController(self) + self.TaskQueue:Push(task) + self:__TaskAdded(-1,task) + return self +end + +--- [Internal] Join a player to a task +-- @param #PLAYERTASKCONTROLLER self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Client#CLIENT Client +-- @param Ops.PlayerTask#PLAYERTASK Task +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_JoinTask(Group, Client, Task) + self:I(self.lid.."_JoinTask") + local playername = Client:GetPlayerName() + if self.TasksPerPlayer:HasUniqueID(playername) then + -- Player already has a task + local m=MESSAGE:New("You already have one active task! Complete it first!","10","Info"):ToGroup(Group) + return self + end + Task:AddClient(Client) + local taskstate = Task:GetState() + --self:I(self.lid.."Taskstate = "..taskstate) + if taskstate ~= "Executing" and taskstate ~= "Done" then + Task:__Requested(-1) + Task:__Executing(-2) + local text = string.format("Player %s joined task %d in state %s", playername, Task.PlayerTaskNr, taskstate) + self:I(self.lid..text) + local m=MESSAGE:New(text,"10","Info"):ToAll() + self.TasksPerPlayer:Push(Task,playername) + -- clear menu + if self.PlayerMenu[playername] then + self.PlayerMenu[playername]:RemoveSubMenus() + end + end + return self +end + +--- [Internal] Show active task info +-- @param #PLAYERTASKCONTROLLER self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Client#CLIENT Client +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_ActiveTaskInfo(Group, Client) + self:I(self.lid.."_ActiveTaskInfo") + local playername = Client:GetPlayerName() + local text = "" + if self.TasksPerPlayer:HasUniqueID(playername) then + -- TODO: Show multiple + local task = self.TasksPerPlayer:GetIDStack() + local task = self.TasksPerPlayer:ReadByID(playername) -- Ops.PlayerTask#PLAYERTASK + local taskname = string.format("%s Task ID %02d",task.Type,task.PlayerTaskNr) + local Coordinate = task.Target:GetCoordinate() + local CoordText = Coordinate:ToStringA2G(Client) + local ThreatLevel = task.Target:GetThreatLevelMax() + local targets = task.Target:CountTargets() or 0 + local clientlist = task:GetClients() + local ThreatGraph = "[" .. string.rep( "■", ThreatLevel ) .. string.rep( "□", 10 - ThreatLevel ) .. "]: "..ThreatLevel + text = string.format("%s\nThreat: %s\nTargets left: %d\nCoord: %s", taskname, ThreatGraph, targets, CoordText) + local clienttxt = "\nPilot(s): " + for _,_name in pairs(clientlist) do + clienttxt = clienttxt .. _name .. ", " + end + clienttxt=string.gsub(clienttxt,", $",".") + text = text .. clienttxt + else + text = "No active task!" + end + local m=MESSAGE:New(text,15,"Tasking"):ToGroup(Group) + return self +end + +--- [Internal] Mark task on F10 map +-- @param #PLAYERTASKCONTROLLER self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Client#CLIENT Client +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_MarkTask(Group, Client) + self:I(self.lid.."_ActiveTaskInfo") + local playername = Client:GetPlayerName() + local text = "" + if self.TasksPerPlayer:HasUniqueID(playername) then + local task = self.TasksPerPlayer:ReadByID(playername) -- Ops.PlayerTask#PLAYERTASK + task:MarkTargetOnF10Map() + text = "Task location marked!" + else + text = "No active task!" + end + local m=MESSAGE:New(text,15,"Info"):ToGroup(Group) + return self +end + +--- [Internal] Smoke task location +-- @param #PLAYERTASKCONTROLLER self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Client#CLIENT Client +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_SmokeTask(Group, Client) + self:I(self.lid.."_SmokeTask") + local playername = Client:GetPlayerName() + local text = "" + if self.TasksPerPlayer:HasUniqueID(playername) then + local task = self.TasksPerPlayer:ReadByID(playername) -- Ops.PlayerTask#PLAYERTASK + task:SmokeTarget() + text = "Task location smoked!" + else + text = "No active task!" + end + local m=MESSAGE:New(text,15,"Info"):ToGroup(Group) + return self +end + +--- [Internal] Flare task location +-- @param #PLAYERTASKCONTROLLER self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Client#CLIENT Client +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_FlareTask(Group, Client) + self:I(self.lid.."_FlareTask") + local playername = Client:GetPlayerName() + local text = "" + if self.TasksPerPlayer:HasUniqueID(playername) then + local task = self.TasksPerPlayer:ReadByID(playername) -- Ops.PlayerTask#PLAYERTASK + task:FlareTarget() + text = "Task location illuminated!" + else + text = "No active task!" + end + local m=MESSAGE:New(text,15,"Info"):ToGroup(Group) + return self +end + +--- [Internal] Abort Task +-- @param #PLAYERTASKCONTROLLER self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Client#CLIENT Client +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_AbortTask(Group, Client) + self:I(self.lid.."_FlareTask") + local playername = Client:GetPlayerName() + local text = "" + if self.TasksPerPlayer:HasUniqueID(playername) then + local task = self.TasksPerPlayer:PullByID(playername) -- Ops.PlayerTask#PLAYERTASK + task:ClientAbort(Client) + text = "Task aborted!" + else + text = "No active task!" + end + local m=MESSAGE:New(text,15,"Info"):ToGroup(Group) + return self +end + +--- [Internal] Build client menus +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_BuildMenus() + self:I(self.lid.."_BuildMenus") + local clients = self.ClientSet:GetAliveSet() + for _,_client in pairs(clients) do + if _client then + local client = _client -- Wrapper.Client#CLIENT + local group = client:GetGroup() + local playername = client:GetPlayerName() or "Unknown" + if group and client then + local topmenu = MENU_GROUP:New(group,self.Name.." Tasking "..self.Type,nil) + local active = MENU_GROUP:New(group,"Active Task",topmenu) + local info = MENU_GROUP_COMMAND:New(group,"Info",active,self._ActiveTaskInfo,self,group,client) + local mark = MENU_GROUP_COMMAND:New(group,"Mark on map",active,self._MarkTask,self,group,client) + if self.Type ~= PLAYERTASKCONTROLLER.Type.A2A then + -- no smoking/flaring here if A2A + local smoke = MENU_GROUP_COMMAND:New(group,"Smoke",active,self._SmokeTask,self,group,client) + local flare = MENU_GROUP_COMMAND:New(group,"Flare",active,self._FlareTask,self,group,client) + end + local abort = MENU_GROUP_COMMAND:New(group,"Abort",active,self._AbortTask,self,group,client) + + if self.PlayerMenu[playername] then + self.PlayerMenu[playername]:RemoveSubMenus() + else + self.PlayerMenu[playername] = MENU_GROUP:New(group,"Join Task",topmenu) + end + + local tasktypes = self:_GetAvailableTaskTypes() + local taskpertype = self:_GetTasksPerType() + + local ttypes = {} + local taskmenu = {} + for _tasktype,_data in pairs(tasktypes) do + ttypes[_tasktype] = MENU_GROUP:New(group,_tasktype,self.PlayerMenu[playername]) + local tasks = taskpertype[_tasktype] or {} + for _,_task in pairs(tasks) do + _task = _task -- Ops.PlayerTask#PLAYERTASK + local text = string.format("TaskNo %03d",_task.PlayerTaskNr) + if self.UseGroupNames then + local name = _task.Target:GetName() + if name ~= "Unknown" then + text = string.format("%s (%03d)",name,_task.PlayerTaskNr) + end + end + if _task:GetState() == "Planned" or (not _task:HasPlayerName(playername)) then + local taskentry = MENU_GROUP_COMMAND:New(group,text,ttypes[_tasktype],self._JoinTask,self,group,client,_task) + taskentry:SetTag(playername) + taskmenu[#taskmenu+1] = taskentry + end + end + end + self.PlayerMenu[playername]:Refresh() + end + end + end + return self +end + +--- [Internal] On after Status call +-- @param #PLAYERTASKCONTROLLER self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:onafterStatus(From, Event, To) + self:I({From, Event, To}) + self:_CheckTargetQueue() + self:_CheckTaskQueue() + self:_BuildMenus() + + local targetcount = self.TargetQueue:Count() + local taskcount = self.TaskQueue:Count() + local playercount = self.ClientSet:CountAlive() + + if self.verbose then + local text = string.format("New Targets: %02d | Active Tasks: %02d | Active Players: %02d",targetcount,taskcount,playercount) + self:I(text) + end + + if self:GetState() ~= "Stopped" then + self:__Status(-30) + end + return self +end + +--- [Internal] On after task done +-- @param #PLAYERTASKCONTROLLER self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.PlayerTask#PLAYERTASK Task +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:onafterTaskDone(From, Event, To, Task) + self:I({From, Event, To}) + self:I(self.lid.."TaskDone") + return self +end + +--- [Internal] On after task cancelled +-- @param #PLAYERTASKCONTROLLER self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.PlayerTask#PLAYERTASK Task +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:onafterTaskCancelled(From, Event, To, Task) + self:I({From, Event, To}) + self:I(self.lid.."TaskCancelled") + return self +end + +--- [Internal] On after task success +-- @param #PLAYERTASKCONTROLLER self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.PlayerTask#PLAYERTASK Task +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:onafterTaskSuccess(From, Event, To, Task) + self:I({From, Event, To}) + self:I(self.lid.."TaskSuccess") + local taskname = string.format("Task #%d %s Success!", Task.PlayerTaskNr, tostring(Task.Type)) + local m = MESSAGE:New(taskname,15,"Tasking"):ToCoalition(self.Coalition) + return self +end + +--- [Internal] On after task failed +-- @param #PLAYERTASKCONTROLLER self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.PlayerTask#PLAYERTASK Task +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:onafterTaskFailed(From, Event, To, Task) + self:I({From, Event, To}) + self:I(self.lid.."TaskFailed") + local taskname = string.format("Task #%d %s Failed!", Task.PlayerTaskNr, tostring(Task.Type)) + local m = MESSAGE:New(taskname,15,"Tasking"):ToCoalition(self.Coalition) + return self +end + +--- [Internal] On after task failed, repeat planned +-- @param #PLAYERTASKCONTROLLER self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.PlayerTask#PLAYERTASK Task +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:onafterTaskRepeatOnFailed(From, Event, To, Task) + self:I({From, Event, To}) + self:I(self.lid.."RepeatOnFailed") + local taskname = string.format("Task #%d %s Failed! Replanning!", Task.PlayerTaskNr, tostring(Task.Type)) + local m = MESSAGE:New(taskname,15,"Tasking"):ToCoalition(self.Coalition) + return self +end + +--- [Internal] On after task added +-- @param #PLAYERTASKCONTROLLER self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.PlayerTask#PLAYERTASK Task +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:onafterTaskAdded(From, Event, To, Task) + self:I({From, Event, To}) + self:I(self.lid.."TaskAdded") + local taskname = string.format("%s has a new Task %s", self.Name, tostring(Task.Type)) + local m = MESSAGE:New(taskname,15,"Tasking"):ToCoalition(self.Coalition) + return self +end + +--- [Internal] On after Stop call +-- @param #PLAYERTASKCONTROLLER self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:onafterStop(From, Event, To) + self:I({From, Event, To}) + self:I(self.lid.."Stopped.") + -- Player leaves + self:UnHandleEvent(EVENTS.PlayerLeaveUnit) + self:UnHandleEvent(EVENTS.Ejection) + self:UnHandleEvent(EVENTS.Crash) + self:UnHandleEvent(EVENTS.PilotDead) + return self +end + --- **AI** -- Balance player slots with AI to create an engaging simulation environment, independent of the amount of players. -- -- **Features:** @@ -150544,7 +197422,7 @@ end -- end do -- -- === -- --- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master-release/AIB%20-%20AI%20Balancing) +-- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AIB%20-%20AI%20Balancing) -- -- === -- @@ -151095,6 +197973,9 @@ function AI_AIR:New( AIGroup ) self.IdleCount = 0 + self.RTBSpeedMaxFactor = 0.6 + self.RTBSpeedMinFactor = 0.5 + return self end @@ -151212,11 +198093,11 @@ end --- When the AI is out of fuel, it is required that a new AI is started, before the old AI can return to the home base. --- Therefore, with a parameter and a calculation of the distance to the home base, the fuel treshold is calculated. --- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, while a new AIControllable is targetted to the AI_AIR. +-- Therefore, with a parameter and a calculation of the distance to the home base, the fuel threshold is calculated. +-- When the fuel threshold is reached, the AI will continue for a given time its patrol task in orbit, while a new AIControllable is targetted to the AI_AIR. -- Once the time is finished, the old AI will return to the base. -- @param #AI_AIR self --- @param #number FuelThresholdPercentage The treshold in percentage (between 0 and 1) when the AIControllable is considered to get out of fuel. +-- @param #number FuelThresholdPercentage The threshold in percentage (between 0 and 1) when the AIControllable is considered to get out of fuel. -- @param #number OutOfFuelOrbitTime The amount of seconds the out of fuel AIControllable will orbit before returning to the base. -- @return #AI_AIR self function AI_AIR:SetFuelThreshold( FuelThresholdPercentage, OutOfFuelOrbitTime ) @@ -151229,14 +198110,14 @@ function AI_AIR:SetFuelThreshold( FuelThresholdPercentage, OutOfFuelOrbitTime ) return self end ---- When the AI is damaged beyond a certain treshold, it is required that the AI returns to the home base. +--- When the AI is damaged beyond a certain threshold, it is required that the AI returns to the home base. -- However, damage cannot be foreseen early on. --- Therefore, when the damage treshold is reached, +-- Therefore, when the damage threshold is reached, -- the AI will return immediately to the home base (RTB). -- Note that for groups, the average damage of the complete group will be calculated. --- So, in a group of 4 airplanes, 2 lost and 2 with damage 0.2, the damage treshold will be 0.25. +-- So, in a group of 4 airplanes, 2 lost and 2 with damage 0.2, the damage threshold will be 0.25. -- @param #AI_AIR self --- @param #number PatrolDamageThreshold The treshold in percentage (between 0 and 1) when the AI is considered to be damaged. +-- @param #number PatrolDamageThreshold The threshold in percentage (between 0 and 1) when the AI is considered to be damaged. -- @return #AI_AIR self function AI_AIR:SetDamageThreshold( PatrolDamageThreshold ) @@ -151318,7 +198199,7 @@ function AI_AIR:onafterStatus() local Fuel = self.Controllable:GetFuelMin() - -- If the fuel in the controllable is below the treshold percentage, + -- If the fuel in the controllable is below the threshold percentage, -- then send for refuel in case of a tanker, otherwise RTB. if Fuel < self.FuelThresholdPercentage then @@ -151418,6 +198299,19 @@ function AI_AIR.RTBHold( AIGroup, Fsm ) end +--- Set the min and max factors on RTB speed. Use this, if your planes are heading back to base too fast. Default values are 0.5 and 0.6. +-- The RTB speed is calculated as the max speed of the unit multiplied by MinFactor (lower bracket) and multiplied by MaxFactor (upper bracket). +-- A random value in this bracket is then applied in the waypoint routing generation. +-- @param #AI_AIR self +-- @param #number MinFactor Lower bracket factor. Defaults to 0.5. +-- @param #number MaxFactor Upper bracket factor. Defaults to 0.6. +-- @return #AI_AIR self +function AI_AIR:SetRTBSpeedFactors(MinFactor,MaxFactor) + self.RTBSpeedMaxFactor = MaxFactor or 0.6 + self.RTBSpeedMinFactor = MinFactor or 0.5 + return self +end + --- @param #AI_AIR self -- @param Wrapper.Group#GROUP AIGroup @@ -151427,11 +198321,13 @@ function AI_AIR:onafterRTB( AIGroup, From, Event, To ) if AIGroup and AIGroup:IsAlive() then - self:I( "Group " .. AIGroup:GetName() .. " ... RTB! ( " .. self:GetState() .. " )" ) + self:T( "Group " .. AIGroup:GetName() .. " ... RTB! ( " .. self:GetState() .. " )" ) self:ClearTargetDistance() --AIGroup:ClearTasks() - + + AIGroup:OptionProhibitAfterburner(true) + local EngageRoute = {} --- Calculate the target route point. @@ -151439,12 +198335,14 @@ function AI_AIR:onafterRTB( AIGroup, From, Event, To ) local FromCoord = AIGroup:GetCoordinate() local ToTargetCoord = self.HomeAirbase:GetCoordinate() -- coordinate is on land height(!) local ToTargetVec3 = ToTargetCoord:GetVec3() - ToTargetVec3.y = ToTargetCoord:GetLandHeight()+1000 -- let's set this 1000m/3000 feet above ground + ToTargetVec3.y = ToTargetCoord:GetLandHeight()+3000 -- let's set this 1000m/3000 feet above ground local ToTargetCoord2 = COORDINATE:NewFromVec3( ToTargetVec3 ) if not self.RTBMinSpeed or not self.RTBMaxSpeed then local RTBSpeedMax = AIGroup:GetSpeedMax() - self:SetRTBSpeed( RTBSpeedMax * 0.5, RTBSpeedMax * 0.6 ) + local RTBSpeedMaxFactor = self.RTBSpeedMaxFactor or 0.6 + local RTBSpeedMinFactor = self.RTBSpeedMinFactor or 0.5 + self:SetRTBSpeed( RTBSpeedMax * RTBSpeedMinFactor, RTBSpeedMax * RTBSpeedMaxFactor) end local RTBSpeed = math.random( self.RTBMinSpeed, self.RTBMaxSpeed ) @@ -152583,6 +199481,10 @@ function AI_AIR_ENGAGE:onafterEngage( DefenderGroup, From, Event, To, AttackSetU DefenderCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. local TargetCoord = AttackSetUnit:GetFirst():GetPointVec3() + if not TargetCoord then + self:Return() + return + end TargetCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) @@ -153866,7 +200768,7 @@ do -- AI_A2A_DISPATCHER -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffFromParkingHot}() will spawn new aircraft in with running engines at a parking spot at the airfield. -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffFromRunway}() will spawn new aircraft at the runway at the airfield. -- - -- **The default landing method is to spawn new aircraft directly in the air.** + -- **The default take-off method is to spawn new aircraft directly in the air.** -- -- Use these methods to fine-tune for specific airfields that are known to create bottlenecks, or have reduced airbase efficiency. -- The more and the longer aircraft need to taxi at an airfield, the more risk there is that: @@ -154323,8 +201225,9 @@ do -- AI_A2A_DISPATCHER --- Enumerator for spawns at airbases -- @type AI_A2A_DISPATCHER.Takeoff -- @extends Wrapper.Group#GROUP.Takeoff - - --- @field #AI_A2A_DISPATCHER.Takeoff Takeoff + + --- + -- @field #AI_A2A_DISPATCHER.Takeoff Takeoff AI_A2A_DISPATCHER.Takeoff = GROUP.Takeoff --- Defnes Landing location. @@ -155149,7 +202052,7 @@ do -- AI_A2A_DISPATCHER end - --- Set the Squadron visible before startup of the dispatcher. + --- [DEPRECATED - Might create problems launching planes] Set the Squadron visible before startup of the dispatcher. -- All planes will be spawned as uncontrolled on the parking spot. -- They will lock the parking spot. -- @param #AI_A2A_DISPATCHER self @@ -155799,7 +202702,7 @@ do -- AI_A2A_DISPATCHER end --- Set flashing player messages on or off - -- @param #AI_A2G_DISPATCHER self + -- @param #AI_A2A_DISPATCHER self -- @param #boolean onoff Set messages on (true) or off (false) function AI_A2A_DISPATCHER:SetSendMessages( onoff ) self.SetSendPlayerMessages = onoff @@ -156487,7 +203390,20 @@ do -- AI_A2A_DISPATCHER for FriendlyDistance, AIFriendly in UTILS.spairs( DefenderFriendlies or {} ) do -- We only allow to ENGAGE targets as long as the Units on both sides are balanced. if AttackerCount > DefenderCount then - local Friendly = AIFriendly:GetGroup() -- Wrapper.Group#GROUP + --self:I("***** AI_A2A_DISPATCHER:CountDefendersToBeEngaged() *****\nThis is supposed to be a UNIT:") + if AIFriendly then + local classname = AIFriendly.ClassName or "No Class Name" + local unitname = AIFriendly.IdentifiableName or "No Unit Name" + --self:I("Class Name: " .. classname) + --self:I("Unit Name: " .. unitname) + --self:I({AIFriendly}) + end + local Friendly = nil + if AIFriendly and AIFriendly:IsAlive() then + --self:I("AIFriendly alive, getting GROUP") + Friendly = AIFriendly:GetGroup() -- Wrapper.Group#GROUP + end + if Friendly and Friendly:IsAlive() then -- Ok, so we have a friendly near the potential target. -- Now we need to check if the AIGroup has a Task. @@ -157401,6 +204317,30 @@ do function AI_A2A_DISPATCHER:SchedulerCAP( SquadronName ) self:CAP( SquadronName ) end + + --- Add resources to a Squadron + -- @param #AI_A2A_DISPATCHER self + -- @param #string Squadron The squadron name. + -- @param #number Amount Number of resources to add. + function AI_A2A_DISPATCHER:AddToSquadron(Squadron,Amount) + local Squadron = self:GetSquadron(Squadron) + if Squadron.ResourceCount then + Squadron.ResourceCount = Squadron.ResourceCount + Amount + end + self:T({Squadron = Squadron.Name,SquadronResourceCount = Squadron.ResourceCount}) + end + + --- Remove resources from a Squadron + -- @param #AI_A2A_DISPATCHER self + -- @param #string Squadron The squadron name. + -- @param #number Amount Number of resources to remove. + function AI_A2A_DISPATCHER:RemoveFromSquadron(Squadron,Amount) + local Squadron = self:GetSquadron(Squadron) + if Squadron.ResourceCount then + Squadron.ResourceCount = Squadron.ResourceCount - Amount + end + self:T({Squadron = Squadron.Name,SquadronResourceCount = Squadron.ResourceCount}) + end end @@ -163064,7 +210004,29 @@ do end end ---- **AI** -- Perform Air Patrolling for airplanes. + --- Add resources to a Squadron + -- @param #AI_A2G_DISPATCHER self + -- @param #string Squadron The squadron name. + -- @param #number Amount Number of resources to add. + function AI_A2G_DISPATCHER:AddToSquadron(Squadron,Amount) + local Squadron = self:GetSquadron(Squadron) + if Squadron.ResourceCount then + Squadron.ResourceCount = Squadron.ResourceCount + Amount + end + self:T({Squadron = Squadron.Name,SquadronResourceCount = Squadron.ResourceCount}) + end + + --- Remove resources from a Squadron + -- @param #AI_A2G_DISPATCHER self + -- @param #string Squadron The squadron name. + -- @param #number Amount Number of resources to remove. + function AI_A2G_DISPATCHER:RemoveFromSquadron(Squadron,Amount) + local Squadron = self:GetSquadron(Squadron) + if Squadron.ResourceCount then + Squadron.ResourceCount = Squadron.ResourceCount - Amount + end + self:T({Squadron = Squadron.Name,SquadronResourceCount = Squadron.ResourceCount}) + end--- **AI** -- Perform Air Patrolling for airplanes. -- -- **Features:** -- @@ -163082,7 +210044,7 @@ end -- -- === -- --- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master-release/PAT%20-%20Patrolling) +-- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/PAT%20-%20Patrolling) -- -- === -- @@ -163791,8 +210753,8 @@ function AI_PATROL_ZONE:onafterRoute( Controllable, From, Event, To ) return end - - if self.Controllable:IsAlive() then + local life = self.Controllable:GetLife() or 0 + if self.Controllable:IsAlive() and life > 1 then -- Determine if the AIControllable is within the PatrolZone. -- If not, make a waypoint within the to that the AIControllable will fly at maximum speed to that point. @@ -163809,8 +210771,9 @@ function AI_PATROL_ZONE:onafterRoute( Controllable, From, Event, To ) if self.Controllable:InAir() == false then self:T( "Not in the air, finding route path within PatrolZone" ) local CurrentVec2 = self.Controllable:GetVec2() - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() + if not CurrentVec2 then return end + --Done: Create GetAltitude function for GROUP, and delete GetUnit(1). + local CurrentAltitude = self.Controllable:GetAltitude() local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) local ToPatrolZoneSpeed = self.PatrolMaxSpeed local CurrentRoutePoint = CurrentPointVec3:WaypointAir( @@ -163824,8 +210787,9 @@ function AI_PATROL_ZONE:onafterRoute( Controllable, From, Event, To ) else self:T( "In the air, finding route path within PatrolZone" ) local CurrentVec2 = self.Controllable:GetVec2() - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() + if not CurrentVec2 then return end + --DONE: Create GetAltitude function for GROUP, and delete GetUnit(1). + local CurrentAltitude = self.Controllable:GetAltitude() local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) local ToPatrolZoneSpeed = self.PatrolMaxSpeed local CurrentRoutePoint = CurrentPointVec3:WaypointAir( @@ -163936,9 +210900,10 @@ function AI_PATROL_ZONE:onafterRTB() --- Calculate the current route point. local CurrentVec2 = self.Controllable:GetVec2() - - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() + if not CurrentVec2 then return end + --DONE: Create GetAltitude function for GROUP, and delete GetUnit(1). + --local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() + local CurrentAltitude = self.Controllable:GetAltitude() local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) local ToPatrolZoneSpeed = self.PatrolMaxSpeed local CurrentRoutePoint = CurrentPointVec3:WaypointAir( @@ -164007,7 +210972,7 @@ end -- -- === -- --- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master-release/CAP%20-%20Combat%20Air%20Patrol) +-- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/CAP%20-%20Combat%20Air%20Patrol) -- -- === -- @@ -164425,8 +211390,10 @@ function AI_CAP_ZONE:onafterEngage( Controllable, From, Event, To ) --- Calculate the current route point. local CurrentVec2 = self.Controllable:GetVec2() - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() + if not CurrentVec2 then return self end + + --DONE: Create GetAltitude function for GROUP, and delete GetUnit(1). + local CurrentAltitude = self.Controllable:GetAltitude() local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) local ToEngageZoneSpeed = self.PatrolMaxSpeed local CurrentRoutePoint = CurrentPointVec3:WaypointAir( @@ -164557,7 +211524,7 @@ end -- -- === -- --- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master-release/CAS%20-%20Close%20Air%20Support) +-- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/CAS%20-%20Close%20Air%20Support) -- -- === -- @@ -165005,8 +211972,8 @@ function AI_CAS_ZONE:onafterEngage( Controllable, From, Event, To, --- Calculate the current route point. local CurrentVec2 = self.Controllable:GetVec2() - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() + --DONE: Create GetAltitude function for GROUP, and delete GetUnit(1). + local CurrentAltitude = self.Controllable:GetAltitude() local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) local ToEngageZoneSpeed = self.PatrolMaxSpeed local CurrentRoutePoint = CurrentPointVec3:WaypointAir( @@ -165626,8 +212593,8 @@ function AI_BAI_ZONE:onafterEngage( Controllable, From, Event, To, --- Calculate the current route point. local CurrentVec2 = self.Controllable:GetVec2() - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() + --DONE: Create GetAltitude function for GROUP, and delete GetUnit(1). + local CurrentAltitude = self.Controllable:GetAltitude() local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) local ToEngageZoneSpeed = self.PatrolMaxSpeed local CurrentRoutePoint = CurrentPointVec3:WaypointAir( @@ -175980,7 +222947,7 @@ do -- UserSound -- @param #USERSOUND self -- @param #string UserSoundFileName The filename of the usersound. -- @return #USERSOUND - function USERSOUND:New( UserSoundFileName ) --R2.3 + function USERSOUND:New( UserSoundFileName ) local self = BASE:Inherit( self, BASE:New() ) -- #USERSOUND @@ -175998,7 +222965,7 @@ do -- UserSound -- local BlueVictory = USERSOUND:New( "BlueVictory.ogg" ) -- BlueVictory:SetFileName( "BlueVictoryLoud.ogg" ) -- Set the BlueVictory to change the file name to play a louder sound. -- - function USERSOUND:SetFileName( UserSoundFileName ) --R2.3 + function USERSOUND:SetFileName( UserSoundFileName ) self.UserSoundFileName = UserSoundFileName @@ -176015,7 +222982,7 @@ do -- UserSound -- local BlueVictory = USERSOUND:New( "BlueVictory.ogg" ) -- BlueVictory:ToAll() -- Play the sound that Blue has won. -- - function USERSOUND:ToAll() --R2.3 + function USERSOUND:ToAll() trigger.action.outSound( self.UserSoundFileName ) @@ -176031,7 +222998,7 @@ do -- UserSound -- local BlueVictory = USERSOUND:New( "BlueVictory.ogg" ) -- BlueVictory:ToCoalition( coalition.side.BLUE ) -- Play the sound that Blue has won to the blue coalition. -- - function USERSOUND:ToCoalition( Coalition ) --R2.3 + function USERSOUND:ToCoalition( Coalition ) trigger.action.outSoundForCoalition(Coalition, self.UserSoundFileName ) @@ -176047,7 +223014,7 @@ do -- UserSound -- local BlueVictory = USERSOUND:New( "BlueVictory.ogg" ) -- BlueVictory:ToCountry( country.id.USA ) -- Play the sound that Blue has won to the USA country. -- - function USERSOUND:ToCountry( Country ) --R2.3 + function USERSOUND:ToCountry( Country ) trigger.action.outSoundForCountry( Country, self.UserSoundFileName ) @@ -176063,9 +223030,9 @@ do -- UserSound -- @usage -- local BlueVictory = USERSOUND:New( "BlueVictory.ogg" ) -- local PlayerGroup = GROUP:FindByName( "PlayerGroup" ) -- Search for the active group named "PlayerGroup", that contains a human player. - -- BlueVictory:ToGroup( PlayerGroup ) -- Play the sound that Blue has won to the player group. + -- BlueVictory:ToGroup( PlayerGroup ) -- Play the victory sound to the player group. -- - function USERSOUND:ToGroup( Group, Delay ) --R2.3 + function USERSOUND:ToGroup( Group, Delay ) Delay=Delay or 0 if Delay>0 then @@ -176076,6 +223043,28 @@ do -- UserSound return self end + + --- Play the usersound to the given @{Wrapper.Unit}. + -- @param #USERSOUND self + -- @param Wrapper.Unit#UNIT Unit The @{Wrapper.Unit} to play the usersound to. + -- @param #number Delay (Optional) Delay in seconds, before the sound is played. Default 0. + -- @return #USERSOUND The usersound instance. + -- @usage + -- local BlueVictory = USERSOUND:New( "BlueVictory.ogg" ) + -- local PlayerUnit = UNIT:FindByName( "PlayerUnit" ) -- Search for the active unit named "PlayerUnit", a human player. + -- BlueVictory:ToUnit( PlayerUnit ) -- Play the victory sound to the player unit. + -- + function USERSOUND:ToUnit( Unit, Delay ) + + Delay=Delay or 0 + if Delay>0 then + SCHEDULER:New(nil, USERSOUND.ToUnit,{self, Unit}, Delay) + else + trigger.action.outSoundForUnit( Unit:GetID(), self.UserSoundFileName ) + end + + return self + end end--- **Sound** - Sound output classes. -- @@ -176262,13 +223251,17 @@ do -- Sound File --- Set path, where the sound file is located. -- @param #SOUNDFILE self - -- @param #string Path Path to the directory, where the sound file is located. + -- @param #string Path Path to the directory, where the sound file is located. In case this is nil, it defaults to the DCS mission temp directory. -- @return #SOUNDFILE self function SOUNDFILE:SetPath(Path) -- Init path. self.path=Path or "l10n/DEFAULT/" - + + if not Path and self.useSRS then -- use path to mission temp dir + self.path = os.getenv('TMP') .. "\\DCS\\Mission\\l10n\\DEFAULT" + end + -- Remove (back)slashes. local nmax=1000 ; local n=1 while (self.path:sub(-1)=="/" or self.path:sub(-1)==[[\]]) and n<=nmax do @@ -177962,14 +224955,13 @@ end -- @field Core.Point#COORDINATE coordinate Coordinate from where the transmission is send. -- @field #string path Path to the SRS exe. This includes the final slash "/". -- @field #string google Full path google credentials JSON file, e.g. "C:\Users\username\Downloads\service-account-file.json". +-- @field #string Label Label showing up on the SRS radio overlay. Default is "ROBOT". No spaces allowed. -- @extends Core.Base#BASE --- *It is a very sad thing that nowadays there is so little useless information.* - Oscar Wilde -- -- === -- --- ![Banner Image](..\Presentations\ATIS\ATIS_Main.png) --- -- # The MSRS Concept -- -- This class allows to broadcast sound files or text via Simple Radio Standalone (SRS). @@ -178005,15 +224997,40 @@ end -- -- Use a specific "culture" with the @{#MSRS.SetCulture} function, e.g. `:SetCulture("en-US")` or `:SetCulture("de-DE")`. -- +-- ## Set Google +-- +-- Use Google's text-to-speech engine with the @{#MSRS.SetGoogle} function, e.g. ':SetGoogle()'. +-- By enabling this it also allows you to utilize SSML in your text for added flexibilty. +-- For more information on setting up a cloud account, visit: https://cloud.google.com/text-to-speech +-- Google's supported SSML reference: https://cloud.google.com/text-to-speech/docs/ssml +-- +-- **NOTE on using GOOGLE TTS with SRS:** You need to have the C# library installed in your SRS folder for Google to work. +-- You can obtain it e.g. here: [NuGet](https://www.nuget.org/packages/Grpc.Core) +-- +-- **Pro-Tipp** - use the command line with power shell to call DCS-SR-ExternalAudio.exe - it will tell you what is missing. +-- and also the Google Console error, in case you have missed a step in setting up your Google TTS. +-- E.g. `.\DCS-SR-ExternalAudio.exe -t "Text Message" -f 255 -m AM -c 2 -s 2 -z -G "Path_To_You_Google.Json"` +-- Plays a message on 255AM for the blue coalition in-game. +-- -- ## Set Voice -- -- Use a specifc voice with the @{#MSRS.SetVoice} function, e.g, `:SetVoice("Microsoft Hedda Desktop")`. -- Note that this must be installed on your windows system. +-- If enabling SetGoogle(), you can use voices provided by Google +-- Google's supported voices: https://cloud.google.com/text-to-speech/docs/voices -- -- ## Set Coordinate -- -- Use @{#MSRS.SetCoordinate} to define the origin from where the transmission is broadcasted. -- +-- ## Set SRS Port +-- +-- Use @{#MSRS.SetPort} to define the SRS port. Defaults to 5002. +-- +-- ## Set SRS Volume +-- +-- Use @{#MSRS.SetVolume} to define the SRS volume. Defaults to 1.0. Allowed values are between 0.0 and 1.0, from silent to loudest. +-- -- @field #MSRS MSRS = { ClassName = "MSRS", @@ -178029,11 +225046,12 @@ MSRS = { volume = 1, speed = 1, coordinate = nil, + Label = "ROBOT", } --- MSRS class version. -- @field #string version -MSRS.version="0.0.3" +MSRS.version="0.1.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -178052,8 +225070,9 @@ MSRS.version="0.0.3" -- @param #string PathToSRS Path to the directory, where SRS is located. -- @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 #number Volume Volume - 1.0 is max, 0.0 is silence -- @return #MSRS self -function MSRS:New(PathToSRS, Frequency, Modulation) +function MSRS:New(PathToSRS, Frequency, Modulation, Volume) -- Defaults. Frequency =Frequency or 143 @@ -178068,6 +225087,13 @@ function MSRS:New(PathToSRS, Frequency, Modulation) self:SetModulations(Modulation) self:SetGender() self:SetCoalition() + self:SetLabel() + self:SetVolume() + self.lid = string.format("%s-%s | ", self.name, self.version) + + if not io or not os then + self:E(self.lid.."***** ERROR - io or os NOT desanitized! MSRS will not work!") + end return self end @@ -178110,12 +225136,47 @@ function MSRS:GetPath() return self.path end +--- Set SRS volume. +-- @param #MSRS self +-- @param #number Volume Volume - 1.0 is max, 0.0 is silence +-- @return #MSRS self +function MSRS:SetVolume(Volume) + local volume = Volume or 1 + if volume > 1 then volume = 1 elseif volume < 0 then volume = 0 end + self.volume = volume + return self +end + +--- Get SRS volume. +-- @param #MSRS self +-- @return #number Volume Volume - 1.0 is max, 0.0 is silence +function MSRS:GetVolume() + return self.volume +end + +--- Set label. +-- @param #MSRS self +-- @param #number Label. Default "ROBOT" +-- @return #MSRS self +function MSRS:SetLabel(Label) + self.Label=Label or "ROBOT" + return self +end + +--- Get label. +-- @param #MSRS self +-- @return #number Label. +function MSRS:GetLabel() + return self.Label +end + --- Set port. -- @param #MSRS self -- @param #number Port Port. Default 5002. -- @return #MSRS self function MSRS:SetPort(Port) self.port=Port or 5002 + return self end --- Get port. @@ -178131,6 +225192,7 @@ end -- @return #MSRS self function MSRS:SetCoalition(Coalition) self.coalition=Coalition or 0 + return self end --- Get coalition. @@ -178299,21 +225361,10 @@ function MSRS:PlaySoundFile(Soundfile, Delay) local command=self:_GetCommand() -- Append file. - command=command.." --file="..tostring(soundfile) + command=command..' --file="'..tostring(soundfile)..'"' + -- Execute command. self:_ExecCommand(command) - - --[[ - - command=command.." > bla.txt" - - -- Debug output. - self:I(string.format("MSRS PlaySoundfile command=%s", command)) - - -- Execute SRS command. - local x=os.execute(command) - - ]] end @@ -178339,16 +225390,6 @@ function MSRS:PlaySoundText(SoundText, Delay) -- Execute command. self:_ExecCommand(command) - - --[[ - command=command.." > bla.txt" - - -- Debug putput. - self:I(string.format("MSRS PlaySoundfile command=%s", command)) - - -- Execute SRS command. - local x=os.execute(command) - ]] end @@ -178375,37 +225416,48 @@ 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. +-- @param #table Frequencies Radio frequencies. +-- @param #table Modulations Radio modulations. +-- @param #string Gender Gender. +-- @param #string Culture Culture. +-- @param #string Voice Voice. +-- @param #number Volume Volume. +-- @param #string Label Label. +-- @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 @@ -178542,8 +225594,9 @@ end -- @param #number volume Volume. -- @param #number speed Speed. -- @param #number port Port. +-- @param #string label Label, defaults to "ROBOT" (displayed sender name in the radio overlay of SRS) - No spaces allowed! -- @return #string Command. -function MSRS:_GetCommand(freqs, modus, coal, gender, voice, culture, volume, speed, port) +function MSRS:_GetCommand(freqs, modus, coal, gender, voice, culture, volume, speed, port,label) local path=self:GetPath() or STTS.DIRECTORY local exe=STTS.EXECUTABLE or "DCS-SR-ExternalAudio.exe" @@ -178556,6 +225609,7 @@ function MSRS:_GetCommand(freqs, modus, coal, gender, voice, culture, volume, sp volume=volume or self.volume speed=speed or self.speed port=port or self.port + label=label or self.Label -- Replace modulation modus=modus:gsub("0", "AM") @@ -178565,12 +225619,12 @@ function MSRS:_GetCommand(freqs, modus, coal, gender, voice, culture, volume, sp --local command=string.format("%s --freqs=%s --modulations=%s --coalition=%d --port=%d --volume=%.2f --speed=%d", exe, freqs, modus, coal, port, volume, speed) -- Command from orig STTS script. Works better for some unknown reason! - local command=string.format("start /min \"\" /d \"%s\" /b \"%s\" -f %s -m %s -c %s -p %s -n \"%s\" -h", path, exe, freqs, modus, coal, port, "ROBOT") + --local command=string.format("start /min \"\" /d \"%s\" /b \"%s\" -f %s -m %s -c %s -p %s -n \"%s\" -h", path, exe, freqs, modus, coal, port, "ROBOT") --local command=string.format('start /b "" /d "%s" "%s" -f %s -m %s -c %s -p %s -n "%s" > bla.txt', path, exe, freqs, modus, coal, port, "ROBOT") -- Command. - local command=string.format('%s/%s -f %s -m %s -c %s -p %s -n "%s"', path, exe, freqs, modus, coal, port, "ROBOT") + local command=string.format('"%s\\%s" -f %s -m %s -c %s -p %s -n "%s" -v "%.1f"', path, exe, freqs, modus, coal, port, label,volume) -- Set voice or gender/culture. if voice then @@ -178579,7 +225633,7 @@ function MSRS:_GetCommand(freqs, modus, coal, gender, voice, culture, volume, sp else -- Add gender. if gender and gender~="female" then - command=command..string.format(" --gender=%s", tostring(gender)) + command=command..string.format(" -g %s", tostring(gender)) end -- Add culture. if culture and culture~="en-GB" then @@ -178595,7 +225649,7 @@ function MSRS:_GetCommand(freqs, modus, coal, gender, voice, culture, volume, sp -- Set google. if self.google then - command=command..string.format(' -G "%s"', self.google) + command=command..string.format(' --ssml -G "%s"', self.google) end -- Debug output. @@ -178605,8 +225659,357 @@ function MSRS:_GetCommand(freqs, modus, coal, gender, voice, culture, volume, sp end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Manages radio transmissions. +-- +-- The purpose of the MSRSQUEUE class is to manage SRS text-to-speech (TTS) messages using the MSRS class. +-- This can be used to submit multiple TTS messages and the class takes care that they are transmitted one after the other (and not overlapping). +-- +-- @type MSRSQUEUE +-- @field #string ClassName Name of the class "MSRSQUEUE". +-- @field #string lid ID for dcs.log. +-- @field #table queue The queue of transmissions. +-- @field #string alias Name of the radio queue. +-- @field #number dt Time interval in seconds for checking the radio queue. +-- @field #number Tlast Time (abs) when the last transmission finished. +-- @field #boolean checking If `true`, the queue update function is scheduled to be called again. +-- @extends Core.Base#BASE +MSRSQUEUE = { + ClassName = "MSRSQUEUE", + Debugmode = nil, + lid = nil, + queue = {}, + alias = nil, + dt = nil, + Tlast = nil, + checking = nil, +} + +--- Radio queue transmission data. +-- @type MSRSQUEUE.Transmission +-- @field #string text Text to be transmitted. +-- @field Sound.SRS#MSRS msrs MOOSE SRS object. +-- @field #number duration Duration in seconds. +-- @field #table subgroups Groups to send subtitle to. +-- @field #string subtitle Subtitle of the transmission. +-- @field #number subduration Duration of the subtitle being displayed. +-- @field #number frequency Frequency. +-- @field #number modulation Modulation. +-- @field #number Tstarted Mission time (abs) in seconds when the transmission started. +-- @field #boolean isplaying If true, transmission is currently playing. +-- @field #number Tplay Mission time (abs) in seconds when the transmission should be played. +-- @field #number interval Interval in seconds before next transmission. + +--- Create a new MSRSQUEUE object for a given radio frequency/modulation. +-- @param #MSRSQUEUE self +-- @param #string alias (Optional) Name of the radio queue. +-- @return #MSRSQUEUE self The MSRSQUEUE object. +function MSRSQUEUE:New(alias) + + -- Inherit base + local self=BASE:Inherit(self, BASE:New()) --#MSRSQUEUE + + self.alias=alias or "My Radio" + + self.dt=1.0 + + self.lid=string.format("MSRSQUEUE %s | ", self.alias) + + return self +end + +--- Clear the radio queue. +-- @param #MSRSQUEUE self +-- @return #MSRSQUEUE self The MSRSQUEUE object. +function MSRSQUEUE:Clear() + self:I(self.lid.."Clearning MSRSQUEUE") + self.queue={} + return self +end + + +--- Add a transmission to the radio queue. +-- @param #MSRSQUEUE self +-- @param #MSRSQUEUE.Transmission transmission The transmission data table. +-- @return #MSRSQUEUE self +function MSRSQUEUE:AddTransmission(transmission) + + -- Init. + transmission.isplaying=false + transmission.Tstarted=nil + + -- Add to queue. + table.insert(self.queue, transmission) + + -- Start checking. + if not self.checking then + self:_CheckRadioQueue() + end + + return self +end + +--- Create a new transmission and add it to the radio queue. +-- @param #MSRSQUEUE self +-- @param #string text Text to play. +-- @param #number duration Duration in seconds the file lasts. Default is determined by number of characters of the text message. +-- @param Sound.SRS#MSRS msrs MOOSE SRS object. +-- @param #number tstart Start time (abs) seconds. Default now. +-- @param #number interval Interval in seconds after the last transmission finished. +-- @param #table subgroups Groups that should receive the subtiltle. +-- @param #string subtitle Subtitle displayed when the message is played. +-- @param #number subduration Duration [sec] of the subtitle being displayed. Default 5 sec. +-- @param #number frequency Radio frequency if other than MSRS default. +-- @param #number modulation Radio modulation if other then MSRS default. +-- @return #MSRSQUEUE.Transmission Radio transmission table. +function MSRSQUEUE:NewTransmission(text, duration, msrs, tstart, interval, subgroups, subtitle, subduration, frequency, modulation) + + -- Sanity checks. + if not text then + self:E(self.lid.."ERROR: No text specified.") + return nil + end + if type(text)~="string" then + self:E(self.lid.."ERROR: Text specified is NOT a string.") + return nil + end + + + -- Create a new transmission object. + local transmission={} --#MSRSQUEUE.Transmission + transmission.text=text + transmission.duration=duration or STTS.getSpeechTime(text) + transmission.msrs=msrs + transmission.Tplay=tstart or timer.getAbsTime() + transmission.subtitle=subtitle + transmission.interval=interval or 0 + transmission.frequency=frequency + transmission.modulation=modulation + transmission.subgroups=subgroups + if transmission.subtitle then + transmission.subduration=subduration or transmission.duration + else + transmission.subduration=0 --nil + end + + -- Add transmission to queue. + self:AddTransmission(transmission) + + return transmission +end + +--- Broadcast radio message. +-- @param #MSRSQUEUE self +-- @param #MSRSQUEUE.Transmission transmission The transmission. +function MSRSQUEUE:Broadcast(transmission) + + if transmission.frequency then + transmission.msrs:PlayTextExt(transmission.text, nil, transmission.frequency, transmission.modulation, Gender, Culture, Voice, Volume, Label) + else + transmission.msrs:PlayText(transmission.text) + end + + local function texttogroup(gid) + -- Text to group. + trigger.action.outTextForGroup(gid, transmission.subtitle, transmission.subduration, true) + end + + if transmission.subgroups and #transmission.subgroups>0 then + + for _,_group in pairs(transmission.subgroups) do + local group=_group --Wrapper.Group#GROUP + + if group and group:IsAlive() then + local gid=group:GetID() + + self:ScheduleOnce(4, texttogroup, gid) + end + + end + + end + +end + +--- Calculate total transmission duration of all transmission in the queue. +-- @param #MSRSQUEUE self +-- @return #number Total transmission duration. +function MSRSQUEUE:CalcTransmisstionDuration() + + local Tnow=timer.getAbsTime() + + local T=0 + for _,_transmission in pairs(self.queue) do + local transmission=_transmission --#MSRSQUEUE.Transmission + + if transmission.isplaying then + + -- Playing for dt seconds. + local dt=Tnow-transmission.Tstarted + + T=T+transmission.duration-dt + + else + T=T+transmission.duration + end + + end + + return T +end + +--- Check radio queue for transmissions to be broadcasted. +-- @param #MSRSQUEUE self +-- @param #number delay Delay in seconds before checking. +function MSRSQUEUE:_CheckRadioQueue(delay) + + -- Transmissions in queue. + local N=#self.queue + + -- Debug info. + self:T2(self.lid..string.format("Check radio queue %s: delay=%.3f sec, N=%d, checking=%s", self.alias, delay or 0, N, tostring(self.checking))) + + if delay and delay>0 then + + -- Delayed call. + self:ScheduleOnce(delay, MSRSQUEUE._CheckRadioQueue, self) + + -- Checking on. + self.checking=true + + else + + -- Check if queue is empty. + if N==0 then + + -- Debug info. + self:T(self.lid..string.format("Check radio queue %s empty ==> disable checking", self.alias)) + + -- Queue is now empty. Nothing to else to do. We start checking again, if a transmission is added. + self.checking=false + + return + end + + -- Get current abs time. + local time=timer.getAbsTime() + + -- Checking on. + self.checking=true + + -- Set dt. + local dt=self.dt + + + local playing=false + local next=nil --#MSRSQUEUE.Transmission + local remove=nil + for i,_transmission in ipairs(self.queue) do + local transmission=_transmission --#MSRSQUEUE.Transmission + + -- Check if transmission time has passed. + if time>=transmission.Tplay then + + -- Check if transmission is currently playing. + if transmission.isplaying then + + -- Check if transmission is finished. + if time>=transmission.Tstarted+transmission.duration then + + -- Transmission over. + transmission.isplaying=false + + -- Remove ith element in queue. + remove=i + + -- Store time last transmission finished. + self.Tlast=time + + else -- still playing + + -- Transmission is still playing. + playing=true + + dt=transmission.duration-(time-transmission.Tstarted) + + end + + else -- not playing yet + + local Tlast=self.Tlast + + if transmission.interval==nil then + + -- Not playing ==> this will be next. + if next==nil then + next=transmission + end + + else + + if Tlast==nil or time-Tlast>=transmission.interval then + next=transmission + else + + end + end + + -- We got a transmission or one with an interval that is not due yet. No need for anything else. + if next or Tlast then + break + end + + end + + else + + -- Transmission not due yet. + + end + end + + -- Found a new transmission. + if next~=nil and not playing then + -- Debug info. + self:T(self.lid..string.format("Broadcasting text=\"%s\" at T=%.3f", next.text, time)) + + -- Call SRS. + self:Broadcast(next) + + next.isplaying=true + next.Tstarted=time + dt=next.duration + end + + -- Remove completed call from queue. + if remove then + -- Remove from queue. + table.remove(self.queue, remove) + N=N-1 + + -- Check if queue is empty. + if #self.queue==0 then + -- Debug info. + self:T(self.lid..string.format("Check radio queue %s empty ==> disable checking", self.alias)) + + self.checking=false + + return + end + end + + -- Check queue. + self:_CheckRadioQueue(dt) + + end + +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + --- **Tasking** -- A command center governs multiple missions, and takes care of the reporting and communications. -- -- **Features:** @@ -179169,9 +226572,11 @@ function COMMANDCENTER:SetAutoAssignTasks( AutoAssign ) self.AutoAssignTasks = AutoAssign or false if self.AutoAssignTasks == true then - self:ScheduleRepeat( 10, 30, 0, nil, self.AssignTasks, self ) + self.autoAssignTasksScheduleID=self:ScheduleRepeat( 10, 30, 0, nil, self.AssignTasks, self ) else - self:ScheduleStop( self.AssignTasks ) + self:ScheduleStop() + -- FF this is not the schedule ID + --self:ScheduleStop( self.AssignTasks ) end end diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index e97f15874..6c235ce20 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -569,17 +569,16 @@ RANGE.version = "2.4.0" --- RANGE contructor. Creates a new RANGE object. -- @param #RANGE self --- @param #string rangename Name of the range. Has to be unique. Will we used to create F10 menu items etc. +-- @param #string RangeName Name of the range. Has to be unique. Will we used to create F10 menu items etc. -- @return #RANGE RANGE object. -function RANGE:New( rangename ) - BASE:F( { rangename = rangename } ) +function RANGE:New( RangeName ) -- Inherit BASE. local self = BASE:Inherit( self, FSM:New() ) -- #RANGE -- Get range name. -- TODO: make sure that the range name is not given twice. This would lead to problems in the F10 radio menu. - self.rangename = rangename or "Practice Range" + self.rangename = RangeName or "Practice Range" -- Log id. self.id = string.format( "RANGE %s | ", self.rangename ) @@ -1760,6 +1759,13 @@ function RANGE:OnEventHit( EventData ) end end +--- Range event handler for event shot (when a unit releases a rocket or bomb (but not a fast firing gun). +-- @param #RANGE self +-- @param #table weapon Weapon +function RANGE:_TrackWeapon(weapon) + +end + --- Range event handler for event shot (when a unit releases a rocket or bomb (but not a fast firing gun). -- @param #RANGE self -- @param Core.Event#EVENTDATA EventData @@ -1806,6 +1812,11 @@ function RANGE:OnEventShot( EventData ) -- Get player unit and name. local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) + + -- Attack parameters. + local attackHdg=_unit:GetHeading() + local attackAlt=_unit:GetHeight() + local attackVel=_unit:GetVelocityKNOTS() -- Set this to larger value than the threshold. local dPR = self.BombtrackThreshold * 2 @@ -1848,7 +1859,6 @@ function RANGE:OnEventShot( EventData ) -- Check again in ~0.005 seconds ==> 200 checks per second. return timer.getTime() + self.dtBombtrack - else ----------------------------- @@ -1858,7 +1868,7 @@ function RANGE:OnEventShot( EventData ) -- Get closet target to last position. local _closetTarget = nil -- #RANGE.BombTarget local _distance = nil - local _closeCoord = nil + local _closeCoord = nil --Core.Point#COORDINATE local _hitquality = "POOR" -- Get callsign. @@ -1886,6 +1896,7 @@ function RANGE:OnEventShot( EventData ) -- Loop over defined bombing targets. for _, _bombtarget in pairs( self.bombingTargets ) do + local bombtarget=_bombtarget --#RANGE.BombTarget -- Get target coordinate. local targetcoord = self:_GetBombTargetCoordinate( _bombtarget ) @@ -1898,15 +1909,15 @@ function RANGE:OnEventShot( EventData ) -- Find closest target to last known position of the bomb. if _distance == nil or _temp < _distance then _distance = _temp - _closetTarget = _bombtarget - _closeCoord = targetcoord + _closetTarget = bombtarget + _closeCoord = targetcoord if _distance <= 1.53 then -- Rangeboss Edit _hitquality = "SHACK" -- Rangeboss Edit - elseif _distance <= 0.5 * _bombtarget.goodhitrange then -- Rangeboss Edit + elseif _distance <= 0.5 * bombtarget.goodhitrange then -- Rangeboss Edit _hitquality = "EXCELLENT" - elseif _distance <= _bombtarget.goodhitrange then + elseif _distance <= bombtarget.goodhitrange then _hitquality = "GOOD" - elseif _distance <= 2 * _bombtarget.goodhitrange then + elseif _distance <= 2 * bombtarget.goodhitrange then _hitquality = "INEFFECTIVE" else _hitquality = "POOR" @@ -1927,6 +1938,7 @@ function RANGE:OnEventShot( EventData ) local _results = self.bombPlayerResults[_playername] local result = {} -- #RANGE.BombResult + result.type = "Bomb Result" result.name = _closetTarget.name or "unknown" result.distance = _distance result.radial = _closeCoord:HeadingTo( impactcoord ) @@ -1934,11 +1946,15 @@ function RANGE:OnEventShot( EventData ) result.quality = _hitquality result.player = playerData.playername result.time = timer.getAbsTime() + result.clock = UTILS.SecondsToClock(result.time) result.airframe = playerData.airframe result.roundsFired = 0 -- Rangeboss Edit result.roundsHit = 0 -- Rangeboss Edit result.roundsQuality = "N/A" -- Rangeboss Edit result.rangename = self.rangename + result.attackHdg = attackHdg + result.attackVel = attackVel + result.attackAlt = attackAlt -- Add to table. table.insert( _results, result ) @@ -2078,13 +2094,13 @@ function RANGE:onafterImpact( From, Event, To, result, player ) -- Only display target name if there is more than one bomb target. local targetname = nil if #self.bombingTargets > 1 then - local targetname = result.name + targetname = result.name end -- Send message to player. local text = string.format( "%s, impact %03d° for %d ft", player.playername, result.radial, UTILS.MetersToFeet( result.distance ) ) if targetname then - text = text .. string.format( " from bulls of target %s." ) + text = text .. string.format( " from bulls of target %s.", targetname ) else text = text .. "." end @@ -2110,11 +2126,15 @@ function RANGE:onafterImpact( From, Event, To, result, player ) end -- Unit. - local unit = UNIT:FindByName( player.unitname ) - - -- Send message. - self:_DisplayMessageToGroup( unit, text, nil, true ) - self:T( self.id .. text ) + if player.unitname then + + -- Get unit. + local unit = UNIT:FindByName( player.unitname ) + + -- Send message. + self:_DisplayMessageToGroup( unit, text, nil, true ) + self:T( self.id .. text ) + end -- Save results. if self.autosave then @@ -3045,9 +3065,11 @@ function RANGE:_CheckInZone( _unitName ) -- Strafe result. local result = {} -- #RANGE.StrafeResult + result.type="Strafe Result" result.player=_playername result.name=_result.zone.name or "unknown" result.time = timer.getAbsTime() + result.clock = UTILS.SecondsToClock(result.time) result.roundsFired = shots result.roundsHit = _result.hits result.roundsQuality = resulttext @@ -3507,7 +3529,7 @@ function RANGE:_SmokeBombImpactOnOff( unitname ) self.PlayerSettings[playername].smokebombimpact = false text = string.format( "%s, %s, smoking impact points of bombs is now OFF.", self.rangename, playername ) else - self.PlayerSettigs[playername].smokebombimpact = true + self.PlayerSettings[playername].smokebombimpact = true text = string.format( "%s, %s, smoking impact points of bombs is now ON.", self.rangename, playername ) end self:_DisplayMessageToGroup( unit, text, 5, false, true ) @@ -3528,7 +3550,7 @@ function RANGE:_SmokeBombDelayOnOff( unitname ) self.PlayerSettings[playername].delaysmoke = false text = string.format( "%s, %s, delayed smoke of bombs is now OFF.", self.rangename, playername ) else - self.PlayerSettigs[playername].delaysmoke = true + self.PlayerSettings[playername].delaysmoke = true text = string.format( "%s, %s, delayed smoke of bombs is now ON.", self.rangename, playername ) end self:_DisplayMessageToGroup( unit, text, 5, false, true ) diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index ab535cc79..6b9307f67 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -5,6 +5,7 @@ __Moose.Include( 'Scripts/Moose/Utilities/Profiler.lua' ) __Moose.Include( 'Scripts/Moose/Utilities/Templates.lua' ) __Moose.Include( 'Scripts/Moose/Utilities/STTS.lua' ) __Moose.Include( 'Scripts/Moose/Utilities/FiFo.lua' ) +__Moose.Include( 'Scripts/Moose/Utilities/Socket.lua' ) __Moose.Include( 'Scripts/Moose/Core/Base.lua' ) __Moose.Include( 'Scripts/Moose/Core/Beacon.lua' ) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 61bb3c33d..2ed31685d 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -11273,7 +11273,7 @@ end --- Get wind speed on carrier deck parallel and perpendicular to runway. -- @param #AIRBOSS self --- @param #number alt Altitude in meters. Default 15 m. (change made from 50m from Discord discussion from Sickdog) +-- @param #number alt Altitude in meters. Default 18 m. -- @return #number Wind component parallel to runway im m/s. -- @return #number Wind component perpendicular to runway in m/s. -- @return #number Total wind strength in m/s. @@ -11296,7 +11296,7 @@ function AIRBOSS:GetWindOnDeck( alt ) zc = UTILS.Rotate2D( zc, -self.carrierparam.rwyangle ) -- Wind (from) vector - local vw = cv:GetWindWithTurbulenceVec3( alt or 15 ) + local vw = cv:GetWindWithTurbulenceVec3( alt or 18 ) --(change made from 50m to 15m from Discord discussion from Sickdog, next change to 18m due to SC higher deck discord) -- Total wind velocity vector. -- Carrier velocity has to be negative. If carrier drives in the direction the wind is blowing from, we have less wind in total. diff --git a/Moose Development/Moose/Utilities/Socket.lua b/Moose Development/Moose/Utilities/Socket.lua new file mode 100644 index 000000000..dbb2b114a --- /dev/null +++ b/Moose Development/Moose/Utilities/Socket.lua @@ -0,0 +1,117 @@ +--- **Utilities** - Socket. +-- +-- **Main Features:** +-- +-- * Sockets +-- * Send messages to Discord +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Utilities.Socket +-- @image Utilities_Socket.png + + +--- SOCKET class. +-- @type SOCKET +-- @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 #table socket The socket. +-- @field #number port The port. +-- @field #string host The host. +-- @field #table json JSON. +-- @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 +-- +-- === +-- +-- # The SOCKET Concept +-- +-- Create a UDP socket server. It enables you to send messages to discord servers via discord bots. +-- +-- +-- @field #SOCKET +SOCKET = { + ClassName = "SOCKET", + verbose = 0, + lid = nil, +} + +--- SOCKET class version. +-- @field #string version +SOCKET.version="0.0.1" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: A lot! +-- TODO: Messages as spoiler. +-- TODO: Send images? + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new SOCKET object. +-- @param #SOCKET self +-- @param #number Port UDP port. Default `10123`. +-- @param #string Host Host. Default `"127.0.0.1"`. +-- @return #SOCKET self +function SOCKET:New(Port, Host) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) --#SOCKET + + package.path = package.path..";.\\LuaSocket\\?.lua;" + package.cpath = package.cpath..";.\\LuaSocket\\?.dll;" + + self.socket = require("socket") + + self.port=Port or 10123 + self.host=Host or "127.0.0.1" + + self.json=loadfile("Scripts\\JSON.lua")() + + self.UDPSendSocket=self.socket.udp() + self.UDPSendSocket:settimeout(0) + + return self +end + +--- Send a table. +-- @param #SOCKET self +-- @param #table Table Table to send. +-- @param #number Port Port. +-- @return #SOCKET self +function SOCKET:SendTable(Table, Port) + + local tbl_json_txt = self.json:encode(Table) + + Port=Port or self.port + + self.socket.try(self.UDPSendSocket:sendto(tbl_json_txt, self.host, Port)) + + return self +end + +--- Send a text message. +-- @param #SOCKET self +-- @param #string Text Test message. +-- @param #number Port Port. +-- @return #SOCKET self +function SOCKET:SendText(Text, Port) + + local message={} + + message.messageType = 1 + message.messageString = Text + + self:SendTable(message, Port) + + return self +end + + diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index 93284a621..01628d1b0 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -777,8 +777,7 @@ end --- Returns the POSITIONABLE height above sea level in meters. -- @param Wrapper.Positionable#POSITIONABLE self --- @return DCS#Vec3 The height of the positionable. --- @return #nil The POSITIONABLE is not existing or alive. +-- @return DCS#Vec3 Height of the positionable in meters (or nil, if the object does not exist). function POSITIONABLE:GetHeight() --R2.1 self:F2( self.PositionableName )